花瓣小跑 发表于 2024-11-15 00:52:55

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

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

支持自由裁剪和固定比例裁剪。
官方也提供了图片编辑功能的实现:
图片编辑-HarmonyOS NEXT-Codelabs-华为开发者同盟 (huawei.com)
官方效果:
https://i-blog.csdnimg.cn/direct/449192250e28458e8a4af4d6adfe8233.png
这种方式功能比力简朴,裁剪是手动点击的,大小是固定的,并不能满足我的需求。
于是向官方求助,给了一个实现的demo,大致代码如下:

model/Bean.ets


export interface RectPosition {
x: number;
y: number;
height: number;
width: number;
}

export enum ActionType {
topLeft,
topRight,
bottomLeft,
bottomRight,
move
}

export interface Position {
x: number;
y: number;
}


export interface InitPosition {
x: number;
y: number;
width: number;
height: number;
}

pages/ImageCropPage.ets


import image from '@ohos.multimedia.image';

import { resourceManager } from '@kit.LocalizationKit';
import { RectPosition, ActionType, Position, InitPosition } from '../model/Bean'

const TAG = "IMAGE_CROPPING"

@Entry
@Component
struct ImageCropPage {
@Provide pixelMap: image.PixelMap | undefined = undefined;
@Provide pixelMapBackUp: image.PixelMap | undefined = undefined;
@Provide imageInfo: image.ImageInfo | undefined = undefined;
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private settings2: RenderingContextSettings = new RenderingContextSettings(true);
private canvasContext2: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings2);
private settings3: RenderingContextSettings = new RenderingContextSettings(true);
private canvasContext3: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings3);
private actionType: ActionType = ActionType.move;
private rotateOn: boolean = false
private sw: number = 266; //图片展示框固定宽度
private sh: number = 390; //图片展示框固定高度
@State clipImageWidth: number = 0;
@State clipImageHeight: number = 0;
@State imageArea: RectPosition = {
    x: 0,
    y: 0,
    width: 0,
    height: 0
};
private touchPosition: Position = {
    x: 0,
    y: 0,
};
@State initPosition: InitPosition = {
    x: 0,
    y: 0,
    width: 0,
    height: 0,
}
@State isCrop: boolean = false
@State cropImageInfo: image.ImageInfo | undefined = undefined;
@State pixelMapChange: boolean = false
@State @Watch('drawMask') clipRect: RectPosition = {
    x: 0,
    y: 0,
    height: 0,
    width: 0
};

build() {
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      if (this.isCrop) {
      if (this.pixelMapChange) {
          Image(this.pixelMap)
            .width(this.clipImageWidth)
            .height(this.clipImageHeight)
            .margin({ top: '10%' })
            .objectFit(ImageFit.Fill)
      } else {
          Image(this.pixelMap)
            .width(this.clipImageWidth)
            .height(this.clipImageHeight)
            .margin({ top: '10%' })
            .objectFit(ImageFit.Fill)
      }

      } else {
      Canvas(this.canvasContext)
          .width(this.sw)
          .height(this.sh)
          .onReady(() => {
            this.drawImage()
          })
          .onAreaChange((value: Area, newVal: Area) => {
            // 获取图片位置xy
            this.initPosition.x = Math.round(newVal.position.x as number)
            this.initPosition.y = Math.round(newVal.position.y as number)
          })
      // 蒙层
      Canvas(this.canvasContext3)
          .position({
            x: this.initPosition.x,
            y: this.initPosition.y
          })
          .width(this.sw)
          .height(this.sh)
      // 裁剪框
      Canvas(this.canvasContext2)
          .position({
            x: this.clipRect.x,
            y: this.clipRect.y
          })
          .width(this.clipRect.width)
          .height(this.clipRect.height)
          .onReady(() => {
            this.drawClipImage()
          })
          .onTouch(event => {
            if (event.type === TouchType.Down) {
            this.isMove(event.target.area, event.touches);
            this.touchPosition = {
                x: event.touches.screenX,
                y: event.touches.screenY
            }
            } else if (event.type === TouchType.Move) {
            let moveX = event.changedTouches.screenX - this.touchPosition.x;
            let moveY = event.changedTouches.screenY - this.touchPosition.y;
            this.touchPosition = {
                x: event.changedTouches.screenX,
                y: event.changedTouches.screenY
            }
            this.moveClipCanvas(moveX, moveY);
            }
          })
      }
      Row() {
      Image($rawfile('rotate.png'))
          .width(40)
          .height(40)
          .onClick(() => {
            this.rotateImage()
          })
      }
      .margin({ top: 50 })
      .height('7%')
      .width('100%')
      .padding(30)

      Row() {
      Image($rawfile('reset.png'))
          .width(40)
          .height(40)
          .onClick(() => {
            this.cancel()
          })
      Image($rawfile('crop.png'))
          .width(40)
          .height(40)
          .onClick(() => {
            this.clipImage()
          })
      }
      .margin({ top: 10 })
      .width('100%')
      .height('7%')
      .padding(30)
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
}

// 旋转图片
async rotateImage() {
    if (this.rotateOn) {
      await this.pixelMap?.rotate(90)
      const info = await this.pixelMap?.getImageInfo()
      this.cropImageInfo = info
      if (this.pixelMapChange) {
      this.pixelMapChange = false
      } else {
      this.pixelMapChange = true
      }
    }
}

// 取消剪切
cancel() {
    this.pixelMap = this.pixelMapBackUp
    this.isCrop = false
    this.rotateOn = false
}

// 判断操作类型
isMove(area: Area, touch: TouchObject) {
    if (touch.x < 60 && touch.y < 60) { // 左上角
      this.actionType = ActionType.topLeft
    } else if (touch.x < 60 && touch.y > (Number(area.height) - 60)) { // 左下
      this.actionType = ActionType.bottomLeft
    } else if (touch.x > Number(area.width) - 60 && touch.y < 60) { // 右上
      this.actionType = ActionType.topRight
    } else if (touch.x > Number(area.width) - 60 && touch.y > (Number(area.height) - 60)) { // 右下
      this.actionType = ActionType.bottomRight
    } else {
      this.actionType = ActionType.move
    }
}

// 绘制背景图
async drawImage() {
    await this.initData('test.jpg')
    if (this.imageInfo != undefined) {

      // let width = px2vp(this.imageInfo.size.width);
      // let height = px2vp(this.imageInfo.size.height)
      // this.canvasContext.drawImage(this.pixelMap, 0, 0, px2vp(this.imageInfo.size.width),
      //   px2vp(this.imageInfo.size.height));

      this.canvasContext.drawImage(this.pixelMap,0,0,this.imageInfo.size.width,this.imageInfo.size.height,0,0,this.sw, this.sh)
      this.canvasContext.save();
    }
}

// 绘制蒙层
drawMask() {
    this.canvasContext3.clearRect(0, 0, this.sw, this.sh);
    this.canvasContext3.fillStyle = 'rgba(0,0,0,0.7)';
    this.canvasContext3.fillRect(0, 0, this.sw, this.sh);
    this.canvasContext3.clearRect(this.clipRect.x - this.initPosition.x, this.clipRect.y - this.initPosition.y,
      this.clipRect.width, this.clipRect.height);
}

// 绘制裁剪框
drawClipImage() {
    this.canvasContext2.clearRect(0, 0, this.clipRect.width, this.clipRect.height);
    this.canvasContext2.lineWidth = 6
    this.canvasContext2.strokeStyle = '#ff6be038'
    this.canvasContext2.beginPath()

    this.canvasContext2.moveTo(0, 20)
    this.canvasContext2.lineTo(0, 0);
    this.canvasContext2.lineTo(20, 0);

    this.canvasContext2.moveTo(this.clipRect.width - 20, 0);
    this.canvasContext2.lineTo(this.clipRect.width, 0);
    this.canvasContext2.lineTo(this.clipRect.width, 20);

    this.canvasContext2.moveTo(0, this.clipRect.height - 20);
    this.canvasContext2.lineTo(0, this.clipRect.height);
    this.canvasContext2.lineTo(20, this.clipRect.height);

    this.canvasContext2.moveTo(this.clipRect.width - 20, this.clipRect.height);
    this.canvasContext2.lineTo(this.clipRect.width, this.clipRect.height);
    this.canvasContext2.lineTo(this.clipRect.width, this.clipRect.height - 20);
    this.canvasContext2.stroke()

    this.canvasContext2.beginPath();
    this.canvasContext2.lineWidth = 0.5;
    let height = Math.round(this.clipRect.height / 3);
    for (let index = 0; index <= 3; index++) {
      let y = index === 3 ? this.clipRect.height : height * index;
      this.canvasContext2.moveTo(0, y);
      this.canvasContext2.lineTo(this.clipRect.width, y);
    }
    let width = Math.round(this.clipRect.width / 3);
    for (let index = 0; index <= 3; index++) {
      let x = index === 3 ? this.clipRect.width : width * index;
      this.canvasContext2.moveTo(x, 0);
      this.canvasContext2.lineTo(x, this.clipRect.height);
    }
    this.canvasContext2.stroke();
}

// 获取pixelMap与imageInfo
async initData(fileName: string) {
    const context: Context = getContext(this);
    const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
    const fileData = await resourceMgr.getRawFileContent(fileName);
    const buffer = fileData.buffer;
    // const imageSource: image.ImageSource = image.createImageSource(buffer);
    const imageSource: image.ImageSource = image.createImageSource(getContext().filesDir + "/picture2.jpg");
    const pixelMap: image.PixelMap = await imageSource.createPixelMap()
    this.pixelMap = pixelMap
    this.pixelMapBackUp = pixelMap
    const imageInfo = await pixelMap.getImageInfo()
    this.imageInfo = imageInfo
    // 裁剪框初始位置
    this.initPosition.width = px2vp(Math.round(this.imageInfo.size.width))
    this.initPosition.height = px2vp(Math.round(this.imageInfo.size.height))
    this.clipRect.height = this.sh
    this.clipRect.width = this.sw
    this.clipRect.x = this.initPosition.x
    this.clipRect.y = this.initPosition.y
}

// 裁剪图片
async clipImage() {
    let ratioX = 1;
    let ratioY = 1;

    if (this.imageInfo != undefined) {
      ratioX = this.imageInfo.size.width / this.sw;
      ratioY = this.imageInfo.size.height / this.sh;
    }

    let x = this.clipRect.x - this.initPosition.x;
    let y = this.clipRect.y - this.initPosition.y;
    console.log('clipImage() x= ' + x + 'y = ' + y + ' height = ' + this.clipRect.height + ' width = ' + this.clipRect.width)
    await this.pixelMap?.crop({
      x: vp2px(x),
      y: vp2px(y),
      size: {
      height: this.clipRect.height * ratioY,
      width: this.clipRect.width * ratioX
      }
    })
    this.cropImageInfo = await this.pixelMap?.getImageInfo();
    this.isCrop = true
    this.rotateOn = true
    this.setClipImageSize();
}

setClipImageSize(){
    let maxWidth: number = 300;
    let maxHeight: number = 500;
    if(this.cropImageInfo!=null){
      let aspectRatio: number = this.cropImageInfo.size.height / this.cropImageInfo.size.width;
      if(this.cropImageInfo.size.width > px2vp(maxWidth)){ //宽度固定,计算高度
      this.clipImageWidth = maxWidth;
      if(maxWidth * aspectRatio > maxHeight){
          this.clipImageHeight = maxHeight;
      }else{
          this.clipImageHeight = maxWidth * aspectRatio;
      }
      }else{
      if(this.cropImageInfo.size.height > px2vp(maxHeight)){//高度固定,计算宽度
          this.clipImageHeight = maxHeight;
          this.clipImageWidth = maxWidth / aspectRatio;
      }else{
          this.clipImageWidth = px2vp(this.cropImageInfo.size.width);
          this.clipImageHeight = px2vp(this.cropImageInfo.size.height);
      }
      }
    }
}

// 裁剪框位置和大小变化    初始位置为图片的初始坐标 移动的坐标
moveClipCanvas(moveX: number, moveY: number) {
    let clipRect: RectPosition = {
      x: this.clipRect.x,
      y: this.clipRect.y,
      width: this.clipRect.width,
      height: this.clipRect.height
    }
    switch (this.actionType) {
      case ActionType.move:
      clipRect.x += moveX;
      clipRect.y += moveY;
      break;
      case ActionType.topLeft:
      clipRect.x += moveX;
      clipRect.y += moveY;
      clipRect.width += -moveX;
      clipRect.height += -moveY;
      break;
      case ActionType.topRight:
      clipRect.y += moveY;
      clipRect.width += moveX;
      clipRect.height += -moveY;
      break;
      case ActionType.bottomLeft:
      clipRect.x += moveX;
      clipRect.width += -moveX;
      clipRect.height += moveY;
      break;
      case ActionType.bottomRight:
      clipRect.width += moveX;
      clipRect.height += moveY;
      break;
      default:
      break;
    }

    // 偏移坐标小于初始位置
    if (clipRect.x < this.initPosition.x) {
      clipRect.x = this.initPosition.x;
    }

    if (clipRect.y < this.initPosition.y) {
      clipRect.y = this.initPosition.y;
    }

    // 横坐标限制位置
    if (clipRect.width + clipRect.x > this.sw + this.initPosition.x) {
      if (this.actionType === ActionType.move) {
      clipRect.x = this.sw + this.initPosition.x - clipRect.width;
      } else {
      clipRect.width = this.sw + this.initPosition.x - clipRect.x;
      }
    }

    // 纵坐标限制
    if (clipRect.height + clipRect.y > this.sh + this.initPosition.y) {
      if (this.actionType === ActionType.move) {
      clipRect.y = this.sh + this.initPosition.y - clipRect.height;
      } else {

      clipRect.height = this.sh + this.initPosition.y - clipRect.y;
      }
    }

    this.clipRect = {
      x: Math.round(clipRect.x),
      y: Math.round(clipRect.y),
      width: Math.round(clipRect.width),
      height: Math.round(clipRect.height)
    };
}
}

pages/Index.ets


import picker from '@ohos.multimedia.cameraPicker'
import camera from '@ohos.multimedia.camera';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';
import fileuri from '@ohos.file.fileuri';
import fs from '@ohos.file.fs';
import photoAccessHelper from '@ohos.file.photoAccessHelper';
import { router } from '@kit.ArkUI';

let context = getContext(this) as common.Context;


class CameraPosition {
cameraPosition: camera.CameraPosition
saveUri: string

constructor(cameraPosition: camera.CameraPosition, saveUri: string) {
    this.cameraPosition = cameraPosition
    this.saveUri = saveUri
}
}

let pathDir = getContext().filesDir;
console.log('保存路径为'+pathDir)
let filePath = pathDir + '/picture.jpg'
fs.createRandomAccessFileSync(filePath, fs.OpenMode.CREATE);
let uri = fileuri.getUriFromPath(filePath);
async function photo() {
try {
    let pickerProfile = new CameraPosition(camera.CameraPosition.CAMERA_POSITION_BACK, uri)
    let pickerResult: picker.PickerResult = await picker.pick(context,
      , pickerProfile);
    console.log("the pick pickerResult is:" + JSON.stringify(pickerResult));
} catch (error) {
    let err = error as BusinessError;
    console.error(`the pick call failed. error code: ${err.code}`);
}
}
async function picture() {
let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
PhotoSelectOptions.isSearchSupported = false;
PhotoSelectOptions.isEditSupported = false;
PhotoSelectOptions.isPreviewForSingleSelectionSupported = false;
let photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
    let photouri: Array<string> = PhotoSelectResult.photoUris
    let file = fs.openSync(photouri, fs.OpenMode.READ_ONLY)
    let file2 = fs.openSync(pathDir+'/picture2.jpg', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
    fs.copyFileSync(file.fd, file2.fd)
    fs.closeSync(file);
    fs.closeSync(file2);
    router.pushUrl({
      url: "pages/ImageCropPage"
    })
})
}


@Entry
@Component
struct Index {
build() {
    Column() {
      Button('选择并编辑').onClick(() => {
      picture()
      })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
}
} 实现的效果:
https://i-blog.csdnimg.cn/direct/fc5f1e9ddf9e496d933636909bfccff5.jpeg
这一效果大致可以满足我的需求,但我在测试的时候发现裁剪框滑动的时候有问题,裁剪框和图片位置没做处理惩罚,有大神可以本身处理惩罚的,可以利用这个去修改。
以我的性格就是继续探索新的方案。
经过不停查找,终于找到了一个满足需求的demo,并且功能很齐全,给大家看看效果:
https://i-blog.csdnimg.cn/direct/9ebe7f7f97264dffaeb0504b2ca4952c.png
但这个demo项目部分利用的代码是TS语言,NEXT版需要改造才能用。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: HarmonyOS 鸿蒙Next实现图片编辑裁剪