实现图片旋转、缩放裁剪预览效果
===============

#### 问题现象

在开发中需要实现对图片的旋转、缩放，裁剪出指定的区域，如何实现？

#### 背景知识

*   [PixelMap](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-pixelmap)：图像像素类，可以有效地存储图像的原始数据，使其可以方便地进行图像变换，如裁剪、缩放、偏移、旋转、翻转等。
*   [PinchGesture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-gestures-pinchgesture)：捏合手势事件，用于图片放大缩小。
*   [PanGesture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-gestures-pangesture)：滑动手势事件，用于图片平移。
*   [Canvas](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-components-canvas-canvas)：画布组件，用于自定义绘制裁剪图形。

#### 解决方案

1.  使用Canvas组件绘制裁剪框，自定义裁剪区域大小。
    
    Canvas(this.context)
      .width('100%')
      .height('100%')
      .backgroundColor(Color.Transparent)
      .onReady(() \=\> {
        if (this.context \== null) {
          return;
        }
        let height \= this.context.height;
        let width \= this.context.width;
        this.context.fillStyle \= this.model.maskColor;
        this.context.fillRect(0, 0, width, height);
       _ // 把中间的取景框透出来_
        this.context.globalCompositeOperation \= 'destination-out';
        this.context.fillStyle \= 'white';
        let frameWidthInVp \= this.getUIContext().px2vp(this.model.frameWidth);
        let frameHeightInVp \= this.getUIContext().px2vp(this.model.getFrameHeight());
        let x \= (width \- this.getUIContext().px2vp(this.model.frameWidth)) / 2;
        let y \= (height \- this.getUIContext().px2vp(this.model.getFrameHeight())) / 2;
        this.context.fillRect(x, y, frameWidthInVp, frameHeightInVp);
     _   // 画出取景框边线，使边界明显一点_
        this.context.globalCompositeOperation \= 'source-over';
        this.context.strokeStyle \= this.model.strokeColor;
        this.context.strokeRect(x, y, frameWidthInVp, frameHeightInVp);
      })
      .enabled(false);
    
2.  给图片添加平移、捏合手势，用于移动图片指定区域到裁剪框中。
    
    .gesture(
      GestureGroup(GestureMode.Parallel,
       _ // 拖动手势_
        PanGesture({})
          .onActionStart(() \=\> {
            this.startOffsetX \= this.model.offsetX;
            this.startOffsetY \= this.model.offsetY;
          })
          .onActionUpdate((event: GestureEvent) \=\> {
            if (event) {
              if (this.model.panEnabled) {
                let distanceX: number \= this.startOffsetX;
                let distanceY: number \= this.startOffsetY;
               _ // 根据旋转角度调整移动方向_
                const rotation \= this.model.rotation;
                if (rotation \=== 0) {
               _   // 0度旋转：正常移动_
                  distanceX += this.getUIContext().vp2px(event.offsetX) / this.model.scale;
                  distanceY += this.getUIContext().vp2px(event.offsetY) / this.model.scale;
                  this.model.offsetX \= distanceX;
                  this.model.offsetY \= distanceY;
                } else if (rotation \=== 90) {
                 _ // 90度旋转：X和Y方向交换，Y方向取反_
                  distanceX += this.getUIContext().vp2px(event.offsetY) / this.model.scale;
                  distanceY \-= this.getUIContext().vp2px(event.offsetX) / this.model.scale;
                  this.model.offsetX \= distanceX;
                  this.model.offsetY \= distanceY;
                } else if (rotation \=== 180) {
                 _ // 180度旋转：X和Y方向都取反_
                  distanceX \-= this.getUIContext().vp2px(event.offsetX) / this.model.scale;
                  distanceY \-= this.getUIContext().vp2px(event.offsetY) / this.model.scale;
                  this.model.offsetX \= distanceX;
                  this.model.offsetY \= distanceY;
                } else if (rotation \=== 270) {
                  _// 270__度旋转：X和Y方向交换，X方向取反_
                  distanceX \-= this.getUIContext().vp2px(event.offsetY) / this.model.scale;
                  distanceY += this.getUIContext().vp2px(event.offsetX) / this.model.scale;
                  this.model.offsetX \= distanceX;
                  this.model.offsetY \= distanceY;
                }
                this.model.offsetX \= distanceX;
                this.model.offsetY \= distanceY;
                this.updateMatrix();
              }
            }
          })
          .onActionEnd(() \=\> {
            this.checkImageAdapt();
          }),
        _// 缩放手势处理_
        PinchGesture({ fingers: 2 })
          .onActionStart(() \=\> {
            this.tempScale \= this.model.scale;
          })
          .onActionUpdate((event) \=\> {
            if (event) {
              if (!this.model.zoomEnabled) {
                return;
              }
              this.zoomTo(this.tempScale \* event.scale, this.model.rotation);
            }
          })
          .onActionEnd(() \=\> {
            this.checkImageAdapt();
          }),
        RotationGesture({ fingers: 3 })
          .onActionStart(() \=\> {
            this.tempRotate \= this.model.rotation;
          })
          .onActionUpdate((event) \=\> {
            if (event) {
              if (!this.model.zoomEnabled) {
                return;
              }
              this.zoomTo(1, this.tempRotate \+ event.angle);
            }
          })
          .onActionEnd(() \=\> {
            this.checkImageAdapt();
          })
      )
    );
    
3.  根据手势结果，调整图片位置，确保图片填满裁剪框。
    
    private checkImageAdapt() {
      let offsetX \= this.model.offsetX;
      let offsetY \= this.model.offsetY;
      let scale \= this.model.scale;
      hilog.info(0, 'CropPage', \`offsetX: ${offsetX}, offsetY: ${offsetY}, scale: ${scale}\`);
    
      _// 图片适配控件的时候也进行了缩放，计算出这个缩放比例_
      let widthScale \= this.model.componentWidth / this.model.imageWidth;
      let heightScale \= this.model.componentHeight / this.model.imageHeight;
      let adaptScale \= Math.min(widthScale, heightScale);
      hilog.info(0, 'CropPage',
        \`Image scale ${adaptScale} while attaching the component\[${this.model.componentWidth}, ${this.model.componentHeight}\]\`);
    
      _//_ _经过两次缩放(适配控件、手势)后，图片的实际显示大小_
      let showWidth \= this.model.imageWidth \* adaptScale \* this.model.scale;
      let showHeight \= this.model.imageHeight \* adaptScale \* this.model.scale;
    
      if (this.model.rotation \=== 0) {
        showWidth \= this.model.imageWidth \* adaptScale \* this.model.scale;
        showHeight \= this.model.imageHeight \* adaptScale \* this.model.scale;
      } else if (this.model.rotation \=== 90) {
        showWidth \= this.model.imageHeight \* adaptScale \* this.model.scale;
        showHeight \= this.model.imageWidth \* adaptScale \* this.model.scale;
      } else if (this.model.rotation \=== 180) {
        showWidth \= this.model.imageWidth \* adaptScale \* this.model.scale;
        showHeight \= this.model.imageHeight \* adaptScale \* this.model.scale;
      } else if (this.model.rotation \=== 270) {
        showWidth \= this.model.imageHeight \* adaptScale \* this.model.scale;
        showHeight \= this.model.imageWidth \* adaptScale \* this.model.scale;
      }
      let imageX \= (this.model.componentWidth \- showWidth) / 2;
      let imageY \= (this.model.componentHeight \- showHeight) / 2;
      hilog.info(0, 'CropPage', \`Image left top is (${imageX}, ${imageY})\`);
    
      _// 取景框的左上角坐标_
      let frameX \= (this.model.componentWidth \- this.model.frameWidth) / 2;
      let frameY \= (this.model.componentHeight \- this.model.getFrameHeight()) / 2;
      hilog.info(0, 'CropPage', \`Frame left top is (${frameX}, ${frameY})\`);
    
      _// 图片左上角坐标_
      let showX \= imageX \+ offsetX \* scale;
      let showY \= imageY \+ offsetY \* scale;
      hilog.info(0, 'CropPage', \`Image show at (${showX}, ${showY})\`);
    
      if (this.model.rotation \=== 0) {
        showX \= imageX \+ offsetX \* scale;
        showY \= imageY \+ offsetY \* scale;
      } else if (this.model.rotation \=== 90) {
        showX \= imageX \- offsetY \* scale;
        showY \= imageY \+ offsetX \* scale;
      } else if (this.model.rotation \=== 180) {
        showX \= imageX \- offsetX \* scale;
        showY \= imageY \- offsetY \* scale;
      } else if (this.model.rotation \=== 270) {
        showX \= imageX \+ offsetY \* scale;
        showY \= imageY \- offsetX \* scale;
      }
    
      _// 图片缩放后，大小不足以填满取景框_
      if (this.model.frameWidth \> showWidth || this.model.getFrameHeight() \> showHeight) {
        let xScale \= this.model.frameWidth / showWidth;
        let yScale \= this.model.getFrameHeight() / showHeight;
        let newScale \= Math.max(xScale, yScale);
        this.model.scale \= this.model.scale \* newScale;
        showX \*= newScale;
        showY \*= newScale;
      }
    
      _// 调整x轴方向位置，使图像填满取景框_
      if (showX \> frameX) {
        showX \= frameX;
      } else if (showX \+ showWidth < frameX \+ this.model.frameWidth) {
        showX \= frameX \+ this.model.frameWidth \- showWidth;
      }
     _ // 调整y轴方向位置，使图像填满取景框_
      if (showY \> frameY) {
        showY \= frameY;
      } else if (showY \+ showHeight < frameY \+ this.model.getFrameHeight()) {
        showY \= frameY \+ this.model.getFrameHeight() \- showHeight;
      }
      this.model.offsetX \= (showX \- imageX) / scale;
      this.model.offsetY \= (showY \- imageY) / scale;
    
      if (this.model.rotation \=== 0) {
        this.model.offsetX \= (showX \- imageX) / scale;
        this.model.offsetY \= (showY \- imageY) / scale;
      } else if (this.model.rotation \=== 90) {
        this.model.offsetX \= (showY \- imageY) / scale;
        this.model.offsetY \= -(showX \- imageX) / scale;
      } else if (this.model.rotation \=== 180) {
        this.model.offsetX \= -(showX \- imageX) / scale;
        this.model.offsetY \= -(showY \- imageY) / scale;
      } else if (this.model.rotation \=== 270) {
        this.model.offsetX \= -(showY \- imageY) / scale;
        this.model.offsetY \= (showX \- imageX) / scale;
      }
      this.updateMatrix();
    }
    
    public updateMatrix(): void {
      this.matrix \= Matrix4.identity()
        .translate({ x: this.model.offsetX, y: this.model.offsetY })
        .rotate({ z: 1, angle: this.model.rotation })
        .scale({ x: this.model.scale, y: this.model.scale });
    }
    
4.  根据缩放比例、平移位置和裁剪区域坐标裁剪图片。
    
    public async crop(): Promise<image.PixelMap\> {
      if (!this.src || this.src \=== '') {
      throw new Error('Please set src first');
    }
    if (this.imageWidth \=== 0 || this.imageHeight \=== 0) {
      throw new Error('The image is not loaded');
    }
    
    _//_ _打开图片文件_
    let file \= fs.openSync(this.src, fs.OpenMode.READ\_ONLY);
    let imageSource: image.ImageSource \= image.createImageSource(file.fd);
    let decodingOptions: image.DecodingOptions \= {
      editable: true,
      desiredPixelFormat: image.PixelMapFormat.RGBA\_8888,
    };
    
    _//_ _创建pixelMap_
    let pm \= await imageSource.createPixelMap(decodingOptions);
    
    _//_ _获取原始图像尺寸_
    let originalWidth \= this.imageWidth;
    let originalHeight \= this.imageHeight;
    
    _//_ _第一步：先旋转整个图像（注意：rotateSync 会修改图像尺寸）_
    if (this.rotation !== 0) {
      pm.rotateSync(this.rotation);
    
      _// 更新图像尺寸（90度和270度旋转会交换宽高）_
      if (this.rotation \=== 90 || this.rotation \=== 270) {
        let tmp\_wh \= originalWidth;
        originalWidth \= originalHeight;
        originalHeight \= tmp\_wh;
      }
    }
    
    _//_ _第二步：计算缩放比例与裁剪区域_
    _// 图片适配控件时的缩放比例_
    let widthScale \= this.componentWidth / this.imageWidth;
    let heightScale \= this.componentHeight / this.imageHeight;
    let adaptScale \= Math.min(widthScale, heightScale);
    
    _//_ _总缩放比例 = 适配缩放 × 手势缩放_
    let totalScale \= adaptScale \* this.scale;
    
    _//_ _旋转 90° 或 270° 时，图像宽高交换，缩放比例应基于新方向_
    let totalRotateScale \= totalScale;
    
    _//_ _仅在 90°/270° 时，使用 totalRotateScale 作为裁剪坐标换算比例_
    if (this.rotation \=== 90 || this.rotation \=== 270) {
      totalRotateScale \= totalScale;
    }
    
    _//_ _计算图片在组件中的实际显示大小_
    let showWidth \= originalWidth \* totalScale;
    let showHeight \= originalHeight \* totalScale;
    
    _//_ _图片左上角坐标（基于组件坐标系）_
    let imageX \= (this.componentWidth \- showWidth) / 2;
    let imageY \= (this.componentHeight \- showHeight) / 2;
    
    _//_ _取景框左上角坐标（基于组件坐标系）_
    let frameX \= (this.componentWidth \- this.frameWidth) / 2;
    let frameY \= (this.componentHeight \- this.getFrameHeight()) / 2;
    
    _//_ _图片在组件中实际显示的位置（考虑旋转后的偏移）_
    let showX \= 0, showY \= 0;
    
    if (this.rotation \=== 0) {
      showX \= imageX \+ this.offsetX \* this.scale;
      showY \= imageY \+ this.offsetY \* this.scale;
    } else if (this.rotation \=== 90) {
      showX \= imageX \- this.offsetY \* this.scale;
      showY \= imageY \+ this.offsetX \* this.scale;
    } else if (this.rotation \=== 180) {
      showX \= imageX \- this.offsetX \* this.scale;
      showY \= imageY \- this.offsetY \* this.scale;
    } else if (this.rotation \=== 270) {
      showX \= imageX \+ this.offsetY \* this.scale;
      showY \= imageY \- this.offsetX \* this.scale;
    }
    
    _//_ _计算裁剪区域坐标（基于旋转后图像的像素坐标）_
    let x \= 0, y \= 0, cropWidth \= 0, cropHeight \= 0;
    if (this.rotation \=== 0 || this.rotation \=== 180) {
      _// 0° 和 180°：宽高不变，使用 totalScale_
      x \= (frameX \- showX) / totalScale;
      y \= (frameY \- showY) / totalScale;
      cropWidth \= this.frameWidth / totalScale;
      cropHeight \= this.getFrameHeight() / totalScale;
    } else if (this.rotation \=== 90 || this.rotation \=== 270) {
      _// 90°_ _和 270°：宽高交换，使用 totalRotateScale（即 totalScale）_
      x \= (frameX \- showX) / totalRotateScale;
      y \= (frameY \- showY) / totalRotateScale;
      cropWidth \= this.frameWidth / totalRotateScale;
      cropHeight \= this.getFrameHeight() / totalRotateScale;
    }
    
    _//_ _确保裁剪区域不超出图像边界_
    x \= Math.max(0, Math.min(x, originalWidth \- cropWidth));
    y \= Math.max(0, Math.min(y, originalHeight \- cropHeight));
    cropWidth \= Math.min(cropWidth, originalWidth);
    cropHeight \= Math.min(cropHeight, originalHeight);
    
    _//_ _裁剪图像（基于旋转后图像的像素坐标）_
    let region: image.Region \= {
      x: x,
      y: y,
      size: {
        width: cropWidth,
        height: cropHeight
      }
    };
    pm.cropSync(region);
    return pm;
    };
    

完整示例参考如下：

import { image } from '@kit.ImageKit';
import fs from '@ohos.file.fs';

interface ImageLoadedEvent {
  width: number;
  height: number;
  componentWidth: number;
  componentHeight: number;
  loadingStatus: number;
  contentWidth: number;
  contentHeight: number;
  contentOffsetX: number;
  contentOffsetY: number;
}

export interface ImageLoadEventListener {

  onImageLoaded(msg: ImageLoadedEvent): void;

  onImageLoadError(error: ImageError): void;
}

export class CropModel {
 _ /\*\*_
   _\* 图片uri_
   _\* 目前支持string，其他形式需要先转换成路径_
   _\*/_
  src: string \= '';
  _/\*\*_
   _\* 图片预览_
   _\*/_
  previewSource: string | Resource \= '';
 _ /\*\*_
   _\* 是否可以拖动_
   _\*/_
  panEnabled: boolean \= true;
  _/\*\*_
   _\* 是否可以缩放_
   _\*/_
  zoomEnabled: boolean \= true;
  _/\*\*_
   _\* 取景框宽度_
   _\*/_
  frameWidth \= 600;
  frameHeight \= 600;
 _ /\*\*_
   _\* 取景框宽高比_
   _\*/_
  frameRatio \= 1;
 _ /\*\*_
   _\* 遮罩颜色_
   _\*/_
  maskColor: string \= '#AA000000';
  _/\*\*_
   _\* 取景框边框颜色_
   _\*/_
  strokeColor: string \= '#FFFFFF';
 _ /\*\*_
   _\* 图片加载监听_
   _\*/_
  imageLoadEventListener: ImageLoadEventListener | null \= null;
 _ /// 以下变量不要手动赋值 ///_
  _/\*\*_
   _\* 图片宽度，加载完成之后才会赋值_
   _\*/_
  imageWidth: number \= 0;
  _/\*\*_
   _\* 图片高度，加载完成之后才会赋值_
   _\*/_
  imageHeight: number \= 0;
 _ /\*\*_
   _\* 控件宽度_
   _\*/_
  componentWidth: number \= 0;
  _/\*\*_
   _\* 控件高度_
   _\*/_
  componentHeight: number \= 0;
  _/\*\*_
   _\* 手势缩放比例_
   _\* 图片经过了两重缩放，一是适配控件的时候进行了缩放，二是手势操作的时候进行了缩放_
   _\*/_
  scale: number \= 1;
  rotation: number \= 0;
  _/\*\*_
   _\* x轴方向偏移量_
   _\*/_
  offsetX: number \= 0;
  _/\*\*_
   _\* y轴方向偏移量_
   _\*/_
  offsetY: number \= 0;

  constructor() {
  }

  public setImage(src: string, previewSource?: string | Resource): CropModel {
    this.src \= src;
    if (!!previewSource) {
      this.previewSource \= previewSource;
    }
    return this;
  }

  public setScale(scale: number): CropModel {
    this.scale \= scale;
    return this;
  }

  public isPanEnabled(): boolean {
    return this.panEnabled;
  }

  public setPanEnabled(panEnabled: boolean): CropModel {
    this.panEnabled \= panEnabled;
    return this;
  }

  public setZoomEnabled(zoomEnabled: boolean): CropModel {
    this.zoomEnabled \= zoomEnabled;
    return this;
  }

  public setFrameWidth(frameWidth: number): CropModel {
    this.frameWidth \= frameWidth;
    return this;
  }

  public setFrameRatio(frameRatio: number): CropModel {
    this.frameRatio \= frameRatio;
    return this;
  }

  public setMaskColor(color: string): CropModel {
    this.maskColor \= color;
    return this;
  }

  public setStrokeColor(color: string): CropModel {
    this.strokeColor \= color;
    return this;
  }

  public setImageLoadEventListener(listener: ImageLoadEventListener): CropModel {
    this.imageLoadEventListener \= listener;
    return this;
  }

  public getScale(): number {
    return this.scale;
  }

  public isZoomEnabled(): boolean {
    return this.zoomEnabled;
  }

  public getImageWidth(): number {
    return this.imageWidth;
  }

  public getImageHeight(): number {
    return this.imageHeight;
  }

  public getFrameHeight() {
    return this.frameHeight;
  }

  public reset(): void {
    this.scale \= 1;
    this.offsetX \= 0;
    this.offsetY \= 0;
  }
  public async crop(): Promise<image.PixelMap\> {
    if (!this.src || this.src \=== '') {
      throw new Error('Please set src first');
    }
    if (this.imageWidth \=== 0 || this.imageHeight \=== 0) {
      throw new Error('The image is not loaded');
    }

    _// 打开图片文件_
    let file \= fs.openSync(this.src, fs.OpenMode.READ\_ONLY);
    let imageSource: image.ImageSource \= image.createImageSource(file.fd);
    let decodingOptions: image.DecodingOptions \= {
      editable: true,
      desiredPixelFormat: image.PixelMapFormat.RGBA\_8888,
    };

   _ // 创建pixelMap_
    let pm \= await imageSource.createPixelMap(decodingOptions);

    _// 获取原始图像尺寸_
    let originalWidth \= this.imageWidth;
    let originalHeight \= this.imageHeight;

    _//_ _第一步：先旋转整个图像（注意：rotateSync 会修改图像尺寸）_
    if (this.rotation !== 0) {
      pm.rotateSync(this.rotation);

 _     // 更新图像尺寸（90度和270度旋转会交换宽高）_
      if (this.rotation \=== 90 || this.rotation \=== 270) {
        let tmp\_wh \= originalWidth;
        originalWidth \= originalHeight;
        originalHeight \= tmp\_wh;
      }
    }

 _   // 第二步：计算缩放比例与裁剪区域_
    _// 图片适配控件时的缩放比例_
    let widthScale \= this.componentWidth / this.imageWidth;
    let heightScale \= this.componentHeight / this.imageHeight;
    let adaptScale \= Math.min(widthScale, heightScale);

  _  // 总缩放比例 = 适配缩放 × 手势缩放_
    let totalScale \= adaptScale \* this.scale;

   _ // 旋转 90° 或 270° 时，图像宽高交换，缩放比例应基于新方向_
    let totalRotateScale \= totalScale;

 _   // 仅在 90°/270° 时，使用 totalRotateScale 作为裁剪坐标换算比例_
    if (this.rotation \=== 90 || this.rotation \=== 270) {
      totalRotateScale \= totalScale;
    }

   _ // 计算图片在组件中的实际显示大小_
    let showWidth \= originalWidth \* totalScale;
    let showHeight \= originalHeight \* totalScale;

    _// 图片左上角坐标（基于组件坐标系）_
    let imageX \= (this.componentWidth \- showWidth) / 2;
    let imageY \= (this.componentHeight \- showHeight) / 2;

 _   // 取景框左上角坐标（基于组件坐标系）_
    let frameX \= (this.componentWidth \- this.frameWidth) / 2;
    let frameY \= (this.componentHeight \- this.getFrameHeight()) / 2;

    _// 图片在组件中实际显示的位置（考虑旋转后的偏移）_
    let showX \= 0, showY \= 0;

    if (this.rotation \=== 0) {
      showX \= imageX \+ this.offsetX \* this.scale;
      showY \= imageY \+ this.offsetY \* this.scale;
    } else if (this.rotation \=== 90) {
      showX \= imageX \- this.offsetY \* this.scale;
      showY \= imageY \+ this.offsetX \* this.scale;
    } else if (this.rotation \=== 180) {
      showX \= imageX \- this.offsetX \* this.scale;
      showY \= imageY \- this.offsetY \* this.scale;
    } else if (this.rotation \=== 270) {
      showX \= imageX \+ this.offsetY \* this.scale;
      showY \= imageY \- this.offsetX \* this.scale;
    }

  _  // 计算裁剪区域坐标（基于旋转后图像的像素坐标）_
    let x \= 0, y \= 0, cropWidth \= 0, cropHeight \= 0;
    if (this.rotation \=== 0 || this.rotation \=== 180) {
     _ // 0° 和 180°：宽高不变，使用 totalScale_
      x \= (frameX \- showX) / totalScale;
      y \= (frameY \- showY) / totalScale;
      cropWidth \= this.frameWidth / totalScale;
      cropHeight \= this.getFrameHeight() / totalScale;
    } else if (this.rotation \=== 90 || this.rotation \=== 270) {
      _// 90°_ _和 270°：宽高交换，使用 totalRotateScale（即 totalScale）_
      x \= (frameX \- showX) / totalRotateScale;
      y \= (frameY \- showY) / totalRotateScale;
      cropWidth \= this.frameWidth / totalRotateScale;
      cropHeight \= this.getFrameHeight() / totalRotateScale;
    }

  _  // 确保裁剪区域不超出图像边界_
    x \= Math.max(0, Math.min(x, originalWidth \- cropWidth));
    y \= Math.max(0, Math.min(y, originalHeight \- cropHeight));
    cropWidth \= Math.min(cropWidth, originalWidth);
    cropHeight \= Math.min(cropHeight, originalHeight);

   _ // 裁剪图像（基于旋转后图像的像素坐标）_
    let region: image.Region \= {
      x: x,
      y: y,
      size: {
        width: cropWidth,
        height: cropHeight
      }
    };
    pm.cropSync(region);
    return pm;
  };
}

import { CropModel } from '../models/CropModel';
import Matrix4 from '@ohos.matrix4';
import { hilog } from '@kit.PerformanceAnalysisKit';
import photoAccessHelper from '@ohos.file.photoAccessHelper';

@Entry
@Component
export struct Index {
  @State pm: PixelMap | undefined \= undefined;
  @State model: CropModel \= new CropModel();
  private settings: RenderingContextSettings \= new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D \= new CanvasRenderingContext2D(this.settings);
  @State private matrix: object \= Matrix4.identity()
    .translate({ x: 0, y: 0 })
    .rotate({ x: 0, y: 0, z: 1, angle: 0 })
    .scale({ x: this.model.scale, y: this.model.scale });
 _ /\*\* 临时变量，无需手动赋值 \*\*/_
  private tempScale \= 1;
  private tempRotate \= 0;
 _ /\*\* 临时变量，无需手动赋值 \*\*/_
  private startOffsetX: number \= 0;
 _ /\*\* 临时变量，无需手动赋值 \*\*/_
  private startOffsetY: number \= 0;

  build() {
    Column() {
      Stack() {
        Image(this.model.src)
          .width('100%')
          .height('100%')
          .alt(this.model.previewSource)
          .objectFit(ImageFit.Contain)
          .transform(this.matrix)
          .onComplete((msg) \=\> {
            if (msg) { _// 图片加载成功_
              this.model.imageWidth \= msg.width;
              this.model.imageHeight \= msg.height;
              this.model.componentWidth \= msg.componentWidth;
              this.model.componentHeight \= msg.componentHeight;
              this.checkImageAdapt();
              if (this.model.imageLoadEventListener !== null && msg.loadingStatus \=== 1) {
                this.model.imageLoadEventListener.onImageLoaded(msg);
              }
            }
          })
          .onError((error) \=\> {
            if (this.model.imageLoadEventListener != null) {
              this.model.imageLoadEventListener.onImageLoadError(error);
            }
          });
        Canvas(this.context)
          .width('100%')
          .height('100%')
          .backgroundColor(Color.Transparent)
          .onReady(() \=\> {
            if (this.context \== null) {
              return;
            }
            let height \= this.context.height;
            let width \= this.context.width;
            this.context.fillStyle \= this.model.maskColor;
            this.context.fillRect(0, 0, width, height);
         _   // 把中间的取景框透出来_
            this.context.globalCompositeOperation \= 'destination-out';
            this.context.fillStyle \= 'white';
            let frameWidthInVp \= this.getUIContext().px2vp(this.model.frameWidth);
            let frameHeightInVp \= this.getUIContext().px2vp(this.model.getFrameHeight());
            let x \= (width \- this.getUIContext().px2vp(this.model.frameWidth)) / 2;
            let y \= (height \- this.getUIContext().px2vp(this.model.getFrameHeight())) / 2;
            this.context.fillRect(x, y, frameWidthInVp, frameHeightInVp);
         _   // 画出取景框边线，使边界明显一点_
            this.context.globalCompositeOperation \= 'source-over';
            this.context.strokeStyle \= this.model.strokeColor;
            this.context.strokeRect(x, y, frameWidthInVp, frameHeightInVp);
          })
          .enabled(false);
      }
      .clip(true)
      .width('100%')
      .layoutWeight(1)
      .backgroundColor('#00000080')
      .priorityGesture(
        TapGesture({ count: 2, fingers: 1 })
          .onAction((event: GestureEvent) \=\> {
            if (!event) {
              return;
            }
            if (this.model.zoomEnabled) {
              if (this.model.scale !== 1) {
                this.model.scale \= 1;
                this.model.reset();
                this.updateMatrix();
              } else {
                this.zoomTo(2, 0);
              }
              if (this.model.rotation !== 0) {
                this.model.rotation \= 0;
                this.model.reset();
                this.updateMatrix();
              }
            }
            this.checkImageAdapt();
          })
      )
      .gesture(
        GestureGroup(GestureMode.Parallel,
        _  // 拖动手势_
          PanGesture({})
            .onActionStart(() \=\> {
              this.startOffsetX \= this.model.offsetX;
              this.startOffsetY \= this.model.offsetY;
            })
            .onActionUpdate((event: GestureEvent) \=\> {
              if (event) {
                if (this.model.panEnabled) {
                  let distanceX: number \= this.startOffsetX;
                  let distanceY: number \= this.startOffsetY;
                 _ // 根据旋转角度调整移动方向_
                  const rotation \= this.model.rotation;
                  if (rotation \=== 0) {
                   _ // 0度旋转：正常移动_
                    distanceX += this.getUIContext().vp2px(event.offsetX) / this.model.scale;
                    distanceY += this.getUIContext().vp2px(event.offsetY) / this.model.scale;
                    this.model.offsetX \= distanceX;
                    this.model.offsetY \= distanceY;
                  } else if (rotation \=== 90) {
                   _ // 90度旋转：X和Y方向交换，Y方向取反_
                    distanceX += this.getUIContext().vp2px(event.offsetY) / this.model.scale;
                    distanceY \-= this.getUIContext().vp2px(event.offsetX) / this.model.scale;
                    this.model.offsetX \= distanceX;
                    this.model.offsetY \= distanceY;
                  } else if (rotation \=== 180) {
                   _ // 180度旋转：X和Y方向都取反_
                    distanceX \-= this.getUIContext().vp2px(event.offsetX) / this.model.scale;
                    distanceY \-= this.getUIContext().vp2px(event.offsetY) / this.model.scale;
                    this.model.offsetX \= distanceX;
                    this.model.offsetY \= distanceY;
                  } else if (rotation \=== 270) {
                   _ // 270度旋转：X和Y方向交换，X方向取反_
                    distanceX \-= this.getUIContext().vp2px(event.offsetY) / this.model.scale;
                    distanceY += this.getUIContext().vp2px(event.offsetX) / this.model.scale;
                    this.model.offsetX \= distanceX;
                    this.model.offsetY \= distanceY;
                  }
                  this.model.offsetX \= distanceX;
                  this.model.offsetY \= distanceY;
                  this.updateMatrix();
                }
              }
            })
            .onActionEnd(() \=\> {
              this.checkImageAdapt();
            }),
        _  // 缩放手势处理_
          PinchGesture({ fingers: 2 })
            .onActionStart(() \=\> {
              this.tempScale \= this.model.scale;
            })
            .onActionUpdate((event) \=\> {
              if (event) {
                if (!this.model.zoomEnabled) {
                  return;
                }
                this.zoomTo(this.tempScale \* event.scale, this.model.rotation);
              }
            })
            .onActionEnd(() \=\> {
              this.checkImageAdapt();
            }),
          RotationGesture({ fingers: 3 })
            .onActionStart(() \=\> {
              this.tempRotate \= this.model.rotation;
            })
            .onActionUpdate((event) \=\> {
              if (event) {
                if (!this.model.zoomEnabled) {
                  return;
                }
                this.zoomTo(1, this.tempRotate \+ event.angle);
              }
            })
            .onActionEnd(() \=\> {
              this.checkImageAdapt();
            })
        )
      );
      Column() {
        Row({ space: 20 }) {
          Button('选择相册图片').onClick(async () \=\> {
            let photoSelectOptions \= new photoAccessHelper.PhotoSelectOptions();
            photoSelectOptions.MIMEType \= photoAccessHelper.PhotoViewMIMETypes.IMAGE\_TYPE;
            photoSelectOptions.maxSelectNumber \= 1;
            let photoPicker \= new photoAccessHelper.PhotoViewPicker();
            let photoSelectResult \= await photoPicker.select(photoSelectOptions);
            let selectedUri \= photoSelectResult.photoUris\[0\];
            this.model.setImage(selectedUri)
              .setFrameWidth(1000)
              .setFrameRatio(1);
          })
          Button('旋转')
            .onClick(() \=\> {
              this.rotateImage(90);
            })
          Button('裁剪预览').onClick(async () \=\> {
            this.pm \= await this.model.crop();
          })
        }
        .width('100%')
        .height(50)

        Image(this.pm)
          .width('300lpx')
          .height('300lpx')

      }
      .width('100%')
      .height('30%')
    }
    .height('100%')
    .width('100%')
  }

 _ /\*\*_
   _\* 检查手势操作后，图片是否填满取景框，没填满则进行调整_
   _\*/_
  private checkImageAdapt() {
    let offsetX \= this.model.offsetX;
    let offsetY \= this.model.offsetY;
    let scale \= this.model.scale;
    hilog.info(0, 'CropPage', \`offsetX: ${offsetX}, offsetY: ${offsetY}, scale: ${scale}\`);

   _ // 图片适配控件的时候也进行了缩放，计算出这个缩放比例_
    let widthScale \= this.model.componentWidth / this.model.imageWidth;
    let heightScale \= this.model.componentHeight / this.model.imageHeight;
    let adaptScale \= Math.min(widthScale, heightScale);
    hilog.info(0, 'CropPage',
      \`Image scale ${adaptScale} while attaching the component\[${this.model.componentWidth}, ${this.model.componentHeight}\]\`);

  _  // 经过两次缩放(适配控件、手势)后，图片的实际显示大小_
    let showWidth \= this.model.imageWidth \* adaptScale \* this.model.scale;
    let showHeight \= this.model.imageHeight \* adaptScale \* this.model.scale;

    if (this.model.rotation \=== 0) {
      showWidth \= this.model.imageWidth \* adaptScale \* this.model.scale;
      showHeight \= this.model.imageHeight \* adaptScale \* this.model.scale;
    } else if (this.model.rotation \=== 90) {
      showWidth \= this.model.imageHeight \* adaptScale \* this.model.scale;
      showHeight \= this.model.imageWidth \* adaptScale \* this.model.scale;
    } else if (this.model.rotation \=== 180) {
      showWidth \= this.model.imageWidth \* adaptScale \* this.model.scale;
      showHeight \= this.model.imageHeight \* adaptScale \* this.model.scale;
    } else if (this.model.rotation \=== 270) {
      showWidth \= this.model.imageHeight \* adaptScale \* this.model.scale;
      showHeight \= this.model.imageWidth \* adaptScale \* this.model.scale;
    }
    let imageX \= (this.model.componentWidth \- showWidth) / 2;
    let imageY \= (this.model.componentHeight \- showHeight) / 2;
    hilog.info(0, 'CropPage', \`Image left top is (${imageX}, ${imageY})\`);

    _// 取景框的左上角坐标_
    let frameX \= (this.model.componentWidth \- this.model.frameWidth) / 2;
    let frameY \= (this.model.componentHeight \- this.model.getFrameHeight()) / 2;
    hilog.info(0, 'CropPage', \`Frame left top is (${frameX}, ${frameY})\`);

  _  // 图片左上角坐标_
    let showX \= imageX \+ offsetX \* scale;
    let showY \= imageY \+ offsetY \* scale;
    hilog.info(0, 'CropPage', \`Image show at (${showX}, ${showY})\`);

    if (this.model.rotation \=== 0) {
      showX \= imageX \+ offsetX \* scale;
      showY \= imageY \+ offsetY \* scale;
    } else if (this.model.rotation \=== 90) {
      showX \= imageX \- offsetY \* scale;
      showY \= imageY \+ offsetX \* scale;
    } else if (this.model.rotation \=== 180) {
      showX \= imageX \- offsetX \* scale;
      showY \= imageY \- offsetY \* scale;
    } else if (this.model.rotation \=== 270) {
      showX \= imageX \+ offsetY \* scale;
      showY \= imageY \- offsetX \* scale;
    }

   _ // 图片缩放后，大小不足以填满取景框_
    if (this.model.frameWidth \> showWidth || this.model.getFrameHeight() \> showHeight) {
      let xScale \= this.model.frameWidth / showWidth;
      let yScale \= this.model.getFrameHeight() / showHeight;
      let newScale \= Math.max(xScale, yScale);
      this.model.scale \= this.model.scale \* newScale;
      showX \*= newScale;
      showY \*= newScale;
    }

   _ // 调整x轴方向位置，使图像填满取景框_
    if (showX \> frameX) {
      showX \= frameX;
    } else if (showX \+ showWidth < frameX \+ this.model.frameWidth) {
      showX \= frameX \+ this.model.frameWidth \- showWidth;
    }
 _   // 调整y轴方向位置，使图像填满取景框_
    if (showY \> frameY) {
      showY \= frameY;
    } else if (showY \+ showHeight < frameY \+ this.model.getFrameHeight()) {
      showY \= frameY \+ this.model.getFrameHeight() \- showHeight;
    }
    this.model.offsetX \= (showX \- imageX) / scale;
    this.model.offsetY \= (showY \- imageY) / scale;

    if (this.model.rotation \=== 0) {
      this.model.offsetX \= (showX \- imageX) / scale;
      this.model.offsetY \= (showY \- imageY) / scale;
    } else if (this.model.rotation \=== 90) {
      this.model.offsetX \= (showY \- imageY) / scale;
      this.model.offsetY \= -(showX \- imageX) / scale;
    } else if (this.model.rotation \=== 180) {
      this.model.offsetX \= -(showX \- imageX) / scale;
      this.model.offsetY \= -(showY \- imageY) / scale;
    } else if (this.model.rotation \=== 270) {
      this.model.offsetX \= -(showY \- imageY) / scale;
      this.model.offsetY \= (showX \- imageX) / scale;
    }
    this.updateMatrix();
  }

  public updateMatrix(): void {
    this.matrix \= Matrix4.identity()
      .translate({ x: this.model.offsetX, y: this.model.offsetY })
      .rotate({ z: 1, angle: this.model.rotation })
      .scale({ x: this.model.scale, y: this.model.scale });
  }

  public zoomTo(scale: number, rotate: number): void {
    this.model.scale \= scale;
    this.model.rotation \= rotate;
    this.updateMatrix();
  }

  //旋转图片方法
  public rotateImage(degrees: number): void {
    this.model.rotation += degrees;
    if (this.model.rotation \>\= 360) {
      this.model.rotation \= 0;
    }
    this.updateMatrix();
    this.checkImageAdapt();
  }
}

#### 总结

将裁剪组件裁剪后的图片转换为PixelMap对象并存储在一个局部变量中，用户可自定义使用该PixelMap图片数据对象。