HarmonyOS 鸿蒙Next实现图片编辑裁剪

打印 上一主题 下一主题

主题 1005|帖子 1005|积分 3015

最近开发一款鸿蒙应用,需要用到图片裁剪,系统的图片编辑功能如今没有对外开放API调用,以是需要本身整一套裁剪的功能。由于刚接触这个ArkTS语言,探索的过程还是挺费劲。
废话不多说,先直接看效果:


支持自由裁剪和固定比例裁剪。
官方也提供了图片编辑功能的实现:
图片编辑-HarmonyOS NEXT-Codelabs-华为开发者同盟 (huawei.com)
官方效果:

这种方式功能比力简朴,裁剪是手动点击的,大小是固定的,并不能满足我的需求。
于是向官方求助,给了一个实现的demo,大致代码如下:

  1. model/Bean.ets
  2. export interface RectPosition {
  3.   x: number;
  4.   y: number;
  5.   height: number;
  6.   width: number;
  7. }
  8. export enum ActionType {
  9.   topLeft,
  10.   topRight,
  11.   bottomLeft,
  12.   bottomRight,
  13.   move
  14. }
  15. export interface Position {
  16.   x: number;
  17.   y: number;
  18. }
  19. export interface InitPosition {
  20.   x: number;
  21.   y: number;
  22.   width: number;
  23.   height: number;
  24. }
  25. pages/ImageCropPage.ets
  26. import image from '@ohos.multimedia.image';
  27. import { resourceManager } from '@kit.LocalizationKit';
  28. import { RectPosition, ActionType, Position, InitPosition } from '../model/Bean'
  29. const TAG = "IMAGE_CROPPING"
  30. @Entry
  31. @Component
  32. struct ImageCropPage {
  33.   @Provide pixelMap: image.PixelMap | undefined = undefined;
  34.   @Provide pixelMapBackUp: image.PixelMap | undefined = undefined;
  35.   @Provide imageInfo: image.ImageInfo | undefined = undefined;
  36.   private settings: RenderingContextSettings = new RenderingContextSettings(true);
  37.   private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  38.   private settings2: RenderingContextSettings = new RenderingContextSettings(true);
  39.   private canvasContext2: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings2);
  40.   private settings3: RenderingContextSettings = new RenderingContextSettings(true);
  41.   private canvasContext3: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings3);
  42.   private actionType: ActionType = ActionType.move;
  43.   private rotateOn: boolean = false
  44.   private sw: number = 266; //图片展示框固定宽度
  45.   private sh: number = 390; //图片展示框固定高度
  46.   @State clipImageWidth: number = 0;
  47.   @State clipImageHeight: number = 0;
  48.   @State imageArea: RectPosition = {
  49.     x: 0,
  50.     y: 0,
  51.     width: 0,
  52.     height: 0
  53.   };
  54.   private touchPosition: Position = {
  55.     x: 0,
  56.     y: 0,
  57.   };
  58.   @State initPosition: InitPosition = {
  59.     x: 0,
  60.     y: 0,
  61.     width: 0,
  62.     height: 0,
  63.   }
  64.   @State isCrop: boolean = false
  65.   @State cropImageInfo: image.ImageInfo | undefined = undefined;
  66.   @State pixelMapChange: boolean = false
  67.   @State @Watch('drawMask') clipRect: RectPosition = {
  68.     x: 0,
  69.     y: 0,
  70.     height: 0,
  71.     width: 0
  72.   };
  73.   build() {
  74.     Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
  75.       if (this.isCrop) {
  76.         if (this.pixelMapChange) {
  77.           Image(this.pixelMap)
  78.             .width(this.clipImageWidth)
  79.             .height(this.clipImageHeight)
  80.             .margin({ top: '10%' })
  81.             .objectFit(ImageFit.Fill)
  82.         } else {
  83.           Image(this.pixelMap)
  84.             .width(this.clipImageWidth)
  85.             .height(this.clipImageHeight)
  86.             .margin({ top: '10%' })
  87.             .objectFit(ImageFit.Fill)
  88.         }
  89.       } else {
  90.         Canvas(this.canvasContext)
  91.           .width(this.sw)
  92.           .height(this.sh)
  93.           .onReady(() => {
  94.             this.drawImage()
  95.           })
  96.           .onAreaChange((value: Area, newVal: Area) => {
  97.             // 获取图片位置xy
  98.             this.initPosition.x = Math.round(newVal.position.x as number)
  99.             this.initPosition.y = Math.round(newVal.position.y as number)
  100.           })
  101.         // 蒙层
  102.         Canvas(this.canvasContext3)
  103.           .position({
  104.             x: this.initPosition.x,
  105.             y: this.initPosition.y
  106.           })
  107.           .width(this.sw)
  108.           .height(this.sh)
  109.         // 裁剪框
  110.         Canvas(this.canvasContext2)
  111.           .position({
  112.             x: this.clipRect.x,
  113.             y: this.clipRect.y
  114.           })
  115.           .width(this.clipRect.width)
  116.           .height(this.clipRect.height)
  117.           .onReady(() => {
  118.             this.drawClipImage()
  119.           })
  120.           .onTouch(event => {
  121.             if (event.type === TouchType.Down) {
  122.               this.isMove(event.target.area, event.touches[0]);
  123.               this.touchPosition = {
  124.                 x: event.touches[0].screenX,
  125.                 y: event.touches[0].screenY
  126.               }
  127.             } else if (event.type === TouchType.Move) {
  128.               let moveX = event.changedTouches[0].screenX - this.touchPosition.x;
  129.               let moveY = event.changedTouches[0].screenY - this.touchPosition.y;
  130.               this.touchPosition = {
  131.                 x: event.changedTouches[0].screenX,
  132.                 y: event.changedTouches[0].screenY
  133.               }
  134.               this.moveClipCanvas(moveX, moveY);
  135.             }
  136.           })
  137.       }
  138.       Row() {
  139.         Image($rawfile('rotate.png'))
  140.           .width(40)
  141.           .height(40)
  142.           .onClick(() => {
  143.             this.rotateImage()
  144.           })
  145.       }
  146.       .margin({ top: 50 })
  147.       .height('7%')
  148.       .width('100%')
  149.       .padding(30)
  150.       Row() {
  151.         Image($rawfile('reset.png'))
  152.           .width(40)
  153.           .height(40)
  154.           .onClick(() => {
  155.             this.cancel()
  156.           })
  157.         Image($rawfile('crop.png'))
  158.           .width(40)
  159.           .height(40)
  160.           .onClick(() => {
  161.             this.clipImage()
  162.           })
  163.       }
  164.       .margin({ top: 10 })
  165.       .width('100%')
  166.       .height('7%')
  167.       .padding(30)
  168.       .justifyContent(FlexAlign.SpaceBetween)
  169.     }
  170.     .width('100%')
  171.     .height('100%')
  172.     .backgroundColor('#000000')
  173.   }
  174.   // 旋转图片
  175.   async rotateImage() {
  176.     if (this.rotateOn) {
  177.       await this.pixelMap?.rotate(90)
  178.       const info = await this.pixelMap?.getImageInfo()
  179.       this.cropImageInfo = info
  180.       if (this.pixelMapChange) {
  181.         this.pixelMapChange = false
  182.       } else {
  183.         this.pixelMapChange = true
  184.       }
  185.     }
  186.   }
  187.   // 取消剪切
  188.   cancel() {
  189.     this.pixelMap = this.pixelMapBackUp
  190.     this.isCrop = false
  191.     this.rotateOn = false
  192.   }
  193.   // 判断操作类型
  194.   isMove(area: Area, touch: TouchObject) {
  195.     if (touch.x < 60 && touch.y < 60) { // 左上角
  196.       this.actionType = ActionType.topLeft
  197.     } else if (touch.x < 60 && touch.y > (Number(area.height) - 60)) { // 左下
  198.       this.actionType = ActionType.bottomLeft
  199.     } else if (touch.x > Number(area.width) - 60 && touch.y < 60) { // 右上
  200.       this.actionType = ActionType.topRight
  201.     } else if (touch.x > Number(area.width) - 60 && touch.y > (Number(area.height) - 60)) { // 右下
  202.       this.actionType = ActionType.bottomRight
  203.     } else {
  204.       this.actionType = ActionType.move
  205.     }
  206.   }
  207.   // 绘制背景图
  208.   async drawImage() {
  209.     await this.initData('test.jpg')
  210.     if (this.imageInfo != undefined) {
  211.       // let width = px2vp(this.imageInfo.size.width);
  212.       // let height = px2vp(this.imageInfo.size.height)
  213.       // this.canvasContext.drawImage(this.pixelMap, 0, 0, px2vp(this.imageInfo.size.width),
  214.       //   px2vp(this.imageInfo.size.height));
  215.       this.canvasContext.drawImage(this.pixelMap,0,0,this.imageInfo.size.width,this.imageInfo.size.height,0,0,this.sw, this.sh)
  216.       this.canvasContext.save();
  217.     }
  218.   }
  219.   // 绘制蒙层
  220.   drawMask() {
  221.     this.canvasContext3.clearRect(0, 0, this.sw, this.sh);
  222.     this.canvasContext3.fillStyle = 'rgba(0,0,0,0.7)';
  223.     this.canvasContext3.fillRect(0, 0, this.sw, this.sh);
  224.     this.canvasContext3.clearRect(this.clipRect.x - this.initPosition.x, this.clipRect.y - this.initPosition.y,
  225.       this.clipRect.width, this.clipRect.height);
  226.   }
  227.   // 绘制裁剪框
  228.   drawClipImage() {
  229.     this.canvasContext2.clearRect(0, 0, this.clipRect.width, this.clipRect.height);
  230.     this.canvasContext2.lineWidth = 6
  231.     this.canvasContext2.strokeStyle = '#ff6be038'
  232.     this.canvasContext2.beginPath()
  233.     this.canvasContext2.moveTo(0, 20)
  234.     this.canvasContext2.lineTo(0, 0);
  235.     this.canvasContext2.lineTo(20, 0);
  236.     this.canvasContext2.moveTo(this.clipRect.width - 20, 0);
  237.     this.canvasContext2.lineTo(this.clipRect.width, 0);
  238.     this.canvasContext2.lineTo(this.clipRect.width, 20);
  239.     this.canvasContext2.moveTo(0, this.clipRect.height - 20);
  240.     this.canvasContext2.lineTo(0, this.clipRect.height);
  241.     this.canvasContext2.lineTo(20, this.clipRect.height);
  242.     this.canvasContext2.moveTo(this.clipRect.width - 20, this.clipRect.height);
  243.     this.canvasContext2.lineTo(this.clipRect.width, this.clipRect.height);
  244.     this.canvasContext2.lineTo(this.clipRect.width, this.clipRect.height - 20);
  245.     this.canvasContext2.stroke()
  246.     this.canvasContext2.beginPath();
  247.     this.canvasContext2.lineWidth = 0.5;
  248.     let height = Math.round(this.clipRect.height / 3);
  249.     for (let index = 0; index <= 3; index++) {
  250.       let y = index === 3 ? this.clipRect.height : height * index;
  251.       this.canvasContext2.moveTo(0, y);
  252.       this.canvasContext2.lineTo(this.clipRect.width, y);
  253.     }
  254.     let width = Math.round(this.clipRect.width / 3);
  255.     for (let index = 0; index <= 3; index++) {
  256.       let x = index === 3 ? this.clipRect.width : width * index;
  257.       this.canvasContext2.moveTo(x, 0);
  258.       this.canvasContext2.lineTo(x, this.clipRect.height);
  259.     }
  260.     this.canvasContext2.stroke();
  261.   }
  262.   // 获取pixelMap与imageInfo
  263.   async initData(fileName: string) {
  264.     const context: Context = getContext(this);
  265.     const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
  266.     const fileData = await resourceMgr.getRawFileContent(fileName);
  267.     const buffer = fileData.buffer;
  268.     // const imageSource: image.ImageSource = image.createImageSource(buffer);
  269.     const imageSource: image.ImageSource = image.createImageSource(getContext().filesDir + "/picture2.jpg");
  270.     const pixelMap: image.PixelMap = await imageSource.createPixelMap()
  271.     this.pixelMap = pixelMap
  272.     this.pixelMapBackUp = pixelMap
  273.     const imageInfo = await pixelMap.getImageInfo()
  274.     this.imageInfo = imageInfo
  275.     // 裁剪框初始位置
  276.     this.initPosition.width = px2vp(Math.round(this.imageInfo.size.width))
  277.     this.initPosition.height = px2vp(Math.round(this.imageInfo.size.height))
  278.     this.clipRect.height = this.sh
  279.     this.clipRect.width = this.sw
  280.     this.clipRect.x = this.initPosition.x
  281.     this.clipRect.y = this.initPosition.y
  282.   }
  283.   // 裁剪图片
  284.   async clipImage() {
  285.     let ratioX = 1;
  286.     let ratioY = 1;
  287.     if (this.imageInfo != undefined) {
  288.       ratioX = this.imageInfo.size.width / this.sw;
  289.       ratioY = this.imageInfo.size.height / this.sh;
  290.     }
  291.     let x = this.clipRect.x - this.initPosition.x;
  292.     let y = this.clipRect.y - this.initPosition.y;
  293.     console.log('clipImage() x= ' + x + '  y = ' + y + ' height = ' + this.clipRect.height + ' width = ' + this.clipRect.width)
  294.     await this.pixelMap?.crop({
  295.       x: vp2px(x),
  296.       y: vp2px(y),
  297.       size: {
  298.         height: this.clipRect.height * ratioY,
  299.         width: this.clipRect.width * ratioX
  300.       }
  301.     })
  302.     this.cropImageInfo = await this.pixelMap?.getImageInfo();
  303.     this.isCrop = true
  304.     this.rotateOn = true
  305.     this.setClipImageSize();
  306.   }
  307.   setClipImageSize(){
  308.     let maxWidth: number = 300;
  309.     let maxHeight: number = 500;
  310.     if(this.cropImageInfo!=null){
  311.       let aspectRatio: number = this.cropImageInfo.size.height / this.cropImageInfo.size.width;
  312.       if(this.cropImageInfo.size.width > px2vp(maxWidth)){ //宽度固定,计算高度
  313.         this.clipImageWidth = maxWidth;
  314.         if(maxWidth * aspectRatio > maxHeight){
  315.           this.clipImageHeight = maxHeight;
  316.         }else{
  317.           this.clipImageHeight = maxWidth * aspectRatio;
  318.         }
  319.       }else{
  320.         if(this.cropImageInfo.size.height > px2vp(maxHeight)){  //高度固定,计算宽度
  321.           this.clipImageHeight = maxHeight;
  322.           this.clipImageWidth = maxWidth / aspectRatio;
  323.         }else{
  324.           this.clipImageWidth = px2vp(this.cropImageInfo.size.width);
  325.           this.clipImageHeight = px2vp(this.cropImageInfo.size.height);
  326.         }
  327.       }
  328.     }
  329.   }
  330.   // 裁剪框位置和大小变化    初始位置为图片的初始坐标 移动的坐标
  331.   moveClipCanvas(moveX: number, moveY: number) {
  332.     let clipRect: RectPosition = {
  333.       x: this.clipRect.x,
  334.       y: this.clipRect.y,
  335.       width: this.clipRect.width,
  336.       height: this.clipRect.height
  337.     }
  338.     switch (this.actionType) {
  339.       case ActionType.move:
  340.         clipRect.x += moveX;
  341.         clipRect.y += moveY;
  342.         break;
  343.       case ActionType.topLeft:
  344.         clipRect.x += moveX;
  345.         clipRect.y += moveY;
  346.         clipRect.width += -moveX;
  347.         clipRect.height += -moveY;
  348.         break;
  349.       case ActionType.topRight:
  350.         clipRect.y += moveY;
  351.         clipRect.width += moveX;
  352.         clipRect.height += -moveY;
  353.         break;
  354.       case ActionType.bottomLeft:
  355.         clipRect.x += moveX;
  356.         clipRect.width += -moveX;
  357.         clipRect.height += moveY;
  358.         break;
  359.       case ActionType.bottomRight:
  360.         clipRect.width += moveX;
  361.         clipRect.height += moveY;
  362.         break;
  363.       default:
  364.         break;
  365.     }
  366.     // 偏移坐标小于初始位置
  367.     if (clipRect.x < this.initPosition.x) {
  368.       clipRect.x = this.initPosition.x;
  369.     }
  370.     if (clipRect.y < this.initPosition.y) {
  371.       clipRect.y = this.initPosition.y;
  372.     }
  373.     // 横坐标限制位置
  374.     if (clipRect.width + clipRect.x > this.sw + this.initPosition.x) {
  375.       if (this.actionType === ActionType.move) {
  376.         clipRect.x = this.sw + this.initPosition.x - clipRect.width;
  377.       } else {
  378.         clipRect.width = this.sw + this.initPosition.x - clipRect.x;
  379.       }
  380.     }
  381.     // 纵坐标限制
  382.     if (clipRect.height + clipRect.y > this.sh + this.initPosition.y) {
  383.       if (this.actionType === ActionType.move) {
  384.         clipRect.y = this.sh + this.initPosition.y - clipRect.height;
  385.       } else {
  386.         clipRect.height = this.sh + this.initPosition.y - clipRect.y;
  387.       }
  388.     }
  389.     this.clipRect = {
  390.       x: Math.round(clipRect.x),
  391.       y: Math.round(clipRect.y),
  392.       width: Math.round(clipRect.width),
  393.       height: Math.round(clipRect.height)
  394.     };
  395.   }
  396. }
  397. pages/Index.ets
  398. import picker from '@ohos.multimedia.cameraPicker'
  399. import camera from '@ohos.multimedia.camera';
  400. import common from '@ohos.app.ability.common';
  401. import { BusinessError } from '@ohos.base';
  402. import fileuri from '@ohos.file.fileuri';
  403. import fs from '@ohos.file.fs';
  404. import photoAccessHelper from '@ohos.file.photoAccessHelper';
  405. import { router } from '@kit.ArkUI';
  406. let context = getContext(this) as common.Context;
  407. class CameraPosition {
  408.   cameraPosition: camera.CameraPosition
  409.   saveUri: string
  410.   constructor(cameraPosition: camera.CameraPosition, saveUri: string) {
  411.     this.cameraPosition = cameraPosition
  412.     this.saveUri = saveUri
  413.   }
  414. }
  415. let pathDir = getContext().filesDir;
  416. console.log('保存路径为'+pathDir)
  417. let filePath = pathDir + '/picture.jpg'
  418. fs.createRandomAccessFileSync(filePath, fs.OpenMode.CREATE);
  419. let uri = fileuri.getUriFromPath(filePath);
  420. async function photo() {
  421.   try {
  422.     let pickerProfile = new CameraPosition(camera.CameraPosition.CAMERA_POSITION_BACK, uri)
  423.     let pickerResult: picker.PickerResult = await picker.pick(context,
  424.       [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO], pickerProfile);
  425.     console.log("the pick pickerResult is:" + JSON.stringify(pickerResult));
  426.   } catch (error) {
  427.     let err = error as BusinessError;
  428.     console.error(`the pick call failed. error code: ${err.code}`);
  429.   }
  430. }
  431. async function picture() {
  432.   let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
  433.   PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
  434.   PhotoSelectOptions.maxSelectNumber = 1;
  435.   PhotoSelectOptions.isSearchSupported = false;
  436.   PhotoSelectOptions.isEditSupported = false;
  437.   PhotoSelectOptions.isPreviewForSingleSelectionSupported = false;
  438.   let photoPicker = new photoAccessHelper.PhotoViewPicker();
  439.   photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
  440.     let photouri: Array<string> = PhotoSelectResult.photoUris
  441.     let file = fs.openSync(photouri[0], fs.OpenMode.READ_ONLY)
  442.     let file2 = fs.openSync(pathDir+'/picture2.jpg', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
  443.     fs.copyFileSync(file.fd, file2.fd)
  444.     fs.closeSync(file);
  445.     fs.closeSync(file2);
  446.     router.pushUrl({
  447.       url: "pages/ImageCropPage"
  448.     })
  449.   })
  450. }
  451. @Entry
  452. @Component
  453. struct Index {
  454.   build() {
  455.     Column() {
  456.       Button('选择并编辑').onClick(() => {
  457.         picture()
  458.       })
  459.     }
  460.     .width('100%')
  461.     .height('100%')
  462.     .alignItems(HorizontalAlign.Center)
  463.     .justifyContent(FlexAlign.Center)
  464.   }
  465. }
复制代码
实现的效果:

这一效果大致可以满足我的需求,但我在测试的时候发现裁剪框滑动的时候有问题,裁剪框和图片位置没做处理惩罚,有大神可以本身处理惩罚的,可以利用这个去修改。
以我的性格就是继续探索新的方案。
经过不停查找,终于找到了一个满足需求的demo,并且功能很齐全,给大家看看效果:

但这个demo项目部分利用的代码是TS语言,NEXT版需要改造才能用。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

花瓣小跑

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表