如何实现为图片添加马赛克的效果
===============

#### 问题现象

从图库中选择手机相册图片，加载成功后展示在首页。为了保护隐私，用户可以在图片上触摸添加马赛克，并且绘制完成后可以实现撤销。如何实现该效果？

#### 效果预览

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/a5/v3/d_nqh0pUQHO9kgChBG1YVA/zh-cn_image_0000002628395464.gif?HW-CC-KV=V1&HW-CC-Date=20260709T134040Z&HW-CC-Expire=86400&HW-CC-Sign=B93C649569576F1F58E57C79063DA0ABD6265162A7E66834BBD49EA73109F84B "点击放大")

#### 背景知识

*   [相册管理模块](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-photoaccesshelper)提供了相册管理能力，包括创建相册、访问和修改相册中的媒体数据，可通过[PhotoViewPicker](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-photoaccesshelper-photoviewpicker)图库选择器对象来选择图片。
*   [Canvas](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-components-canvas-canvas)提供画布组件，用于自定义绘制图形。该组件可通过[CanvasRenderingContext2D](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-canvasrenderingcontext2d)在Canvas组件上进行绘制，绘制对象可以是矩形、文本、图片等。
*   [getImageData](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-components-canvas-canvasrenderingcontext2d#getimagedata)：以当前Canvas指定区域内的像素创建ImageData对象。

#### 解决方案

1.  通过PhotoViewPicker打开图库选择图片。
    
    _//_ _选择图片_
    private async selectImage() {
     _ // picker拉起图库，选择图片_
      const picker \= new photoAccessHelper.PhotoViewPicker();
      const result \= await picker.select({
        maxSelectNumber: 1,
        MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE\_TYPE
      });
      if (!result.photoUris.length) {
        return;
      }
      this.processImage(result.photoUris\[0\]);
    }
    
2.  使用Canvas组件绘制图片。
    
    _//_ _绘制图片_
    private clearAndDraw() {
      this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
      if (this.maskUri) {
        this.myPen.drawImage(new ImageBitmap(this.maskUri), 0, 0, this.canvasWidth, this.canvasHeight);
      }
    }
    
3.  为Canvas组件补充onTouch触摸事件监听，通过getImageData方法获取触摸点处的像素颜色数据。然后分别提取红、绿、蓝三个通道的值，使用rgb函数将提取的颜色值组合成一个颜色字符串，用于后续的填充操作。
    
    _//_ _触摸事件_
    .onTouch((event) \=\> {
      const x \= event.touches\[0\].x, y \= event.touches\[0\].y;
      switch (event.type) {
        case TouchType.Down:
          const path \= new DrawingPath();
          path.points.push({ x: x, y: y });
          this.paths.push(path);
          break;
        case TouchType.Move:
          this.paths\[this.paths.length\-1\].points.push({ x: x, y: y });
          this.draw(x, y);
          break;
      }
    })
    
4.  利用数组记录绘制路径，撤销时移除最上层路径然后重新绘制即可实现“撤销”操作。
    
    Button('撤销').onClick(() \=\> {
      if (this.paths.length \>\= 1) {
        this.paths.pop()!!;
      }
      console.debug(\`----------撤销\---\`);
      if (this.maskUri) {
        console.debug(\`----------撤销   ${this.paths.length}\---\`);
        this.clearAndDraw();
        this.paths.forEach((path) \=\> {
          for (let i \= 0; i < path.points.length; i++) {
            this.draw(path.points\[i\].x, path.points\[i\].y);
          }
        });
      }
    });
    

完整示例参考如下：

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { display, window } from '@kit.ArkUI';
import image from '@ohos.multimedia.image';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';

interface Point {
  x: number;
  y: number;
}

export class DrawingPath {
  points: Point\[\] \= \[\];
}

@Entry
@Component
struct ImageAddMaskCase {
  @State @Watch('clearAndDraw')
  maskUri: string \= '';
 _ // 画布尺寸_
  @State private canvasWidth: number \= 0;
  @State private canvasHeight: number \= 0;
 _ // 展示区域的尺寸_
  private screenWidth: number \= 0;
  private screenHeight: number \= 0;
  _// 缩放比例_
  @State private scaleFactor: number \= 1;
 _ // 马赛克像素大小_
  private maskRadius: number \= 30;
  private paths: DrawingPath\[\] \= \[\];
  private myPen: CanvasRenderingContext2D \= new CanvasRenderingContext2D(new RenderingContextSettings(true));

 _ // 选择图片_
  private async selectImage() {
  _  // picker拉起图库，选择图片_
    const picker \= new photoAccessHelper.PhotoViewPicker();
    const result \= await picker.select({
      maxSelectNumber: 1,
      MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE\_TYPE
    });
    if (!result.photoUris.length) {
      return;
    }
    this.processImage(result.photoUris\[0\]);
  }

 _ // 图片处理_
  private async processImage(uri: string) {
    try {
      const file \= fileIo.openSync(uri, fileIo.OpenMode.READ\_ONLY);
      const imageSource \= image.createImageSource(file.fd);
      const decodingOptions: image.DecodingOptions \= {
        editable: true,
        desiredPixelFormat: 3,
      };
      imageSource.createPixelMap(decodingOptions, (err: BusinessError, pixelMap: image.PixelMap) \=\> {
        if (err) {
          console.error(\`Failed to create pixelMap. Code: ${err.code}, Message: ${err.message}\`);
          return;
        }
        pixelMap.getImageInfo().then(info \=\> {
         _ // 更新画布尺寸_
          this.updateCanvasSize(info.size);
          this.maskUri \= uri;
        _  // 计算缩放比例_
          this.calculateScaleFactor();
        });
      });
    } catch (error) {
      console.error('Error processing image:', error);
    }
  }

  private updateCanvasSize(size: image.Size) {
    this.canvasWidth \= this.getUIContext().px2vp(size.width);
    this.canvasHeight \= this.getUIContext().px2vp(size.height);
  }

  _// 根据图片的宽度、高度以及屏幕的宽度、高度计算缩放比例_
  private calculateScaleFactor() {
    const widthRatio \= this.screenWidth / this.canvasWidth;
    const heightRatio \= this.screenHeight / this.canvasHeight;
    this.scaleFactor \= Math.min(widthRatio, heightRatio);
    if (this.scaleFactor \> 1) {
      this.scaleFactor \= 1;
    }
  }

  _// 绘制图片_
  private clearAndDraw() {
    this.myPen.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
    if (this.maskUri) {
      this.myPen.drawImage(new ImageBitmap(this.maskUri), 0, 0, this.canvasWidth, this.canvasHeight);
    }
  }

  _// 计算可展示区域的屏幕宽度和高度_
  private getScreenDimensions() {
    window.getLastWindow(this.getUIContext().getHostContext()).then(win \=\> {
      const topSafeHeight \=
        this.getUIContext().px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE\_SYSTEM).topRect.height);
      const bottomSafeHeight \= this.getUIContext()
        .px2vp(win.getWindowAvoidArea(window.AvoidAreaType.TYPE\_NAVIGATION\_INDICATOR).bottomRect.height);
      const displayInfo \= display.getDefaultDisplaySync();
      this.screenWidth \= this.getUIContext().px2vp(displayInfo.width);
      this.screenHeight \= this.getUIContext().px2vp(displayInfo.height) \- topSafeHeight \- bottomSafeHeight \- 50;
    });
  }

 _ // 获取_
  aboutToAppear() {
    this.getScreenDimensions();
  }

  build() {
    Column() {
      Row({ space: 20 }) {
    _    // 加载图片_
        Button('选择图片').onClick(() \=\> this.selectImage());
        Button('撤销').onClick(() \=\> {
          if (this.paths.length \>\= 1) {
            this.paths.pop()!!;
          }
          console.debug(\`----------撤销\---\`);
          if (this.maskUri) {
            console.debug(\`----------撤销   ${this.paths.length}\---\`);
            this.clearAndDraw();
            this.paths.forEach((path) \=\> {
              for (let i \= 0; i < path.points.length; i++) {
                this.draw(path.points\[i\].x, path.points\[i\].y);
              }
            });
          }
        });
        Button('清空').onClick(() \=\> {
          if (this.maskUri) {
            this.clearAndDraw();
          }
        });
      }
      .width('100%')
      .height(50)
      .justifyContent(FlexAlign.Center);

      Canvas(this.myPen)
        .onReady(() \=\> this.clearAndDraw())
        .width(this.canvasWidth)
        .height(this.canvasHeight)
        .scale({
          x: this.scaleFactor,
          y: this.scaleFactor,
          centerX: 0,
          centerY: 0
        })
    _    // 触摸事件_
        .onTouch((event) \=\> {
          const x \= event.touches\[0\].x, y \= event.touches\[0\].y;
          switch (event.type) {
            case TouchType.Down:
              const path \= new DrawingPath();
              path.points.push({ x: x, y: y });
              this.paths.push(path);
              break;
            case TouchType.Move:
              this.paths\[this.paths.length\-1\].points.push({ x: x, y: y });
              this.draw(x, y);
              break;
          }
        })
        .backgroundColor(Color.Pink);
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Start);
  }

  private draw(x: number, y: number) {
    const data \= this.myPen.getImageData(x, y, 1, 1).data;
    const color \= \`rgb(${data\[0\]}, ${data\[1\]}, ${data\[2\]})\`;
    this.myPen.fillStyle \= color;
  _  // 矩阵填充_
    this.myPen.fillRect(x, y, this.maskRadius, this.maskRadius);
  }
}