# 如何通过Canvas绘制图形并添加到PDF上

#### 问题现象

应用需要通过在Canvas上绘制的图形、文字，添加到PDF上。

#### 背景知识

*   应用有自定义场景需要使用Canvas绘制图形。
*   绘制后需要将绘制的图形通过addWatermark添加水印到PDF页面上。

#### 解决方案

1.  使用Canvas组件和onTouch方法实现手绘功能。
    
    ```
    Stack() {
      Canvas(this.context)
        .width('100%')
        .height('100%')
        .backgroundColor(Color.White)
        .onTouch((event) => {
          if (event.type === TouchType.Down) {
            this.showThickness = false;
            this.eventType = 'Down';
            this.isDrawing = true;
            this.x = event.touches[0].x;
            this.y = event.touches[0].y;
            this.context.beginPath();
            this.tempPath = new Path2D();
            this.tempPath.moveTo(this.x, this.y);
            this.context.lineCap = 'round';
          }
          if (event.type === TouchType.Up) {
            this.eventType = 'Up';
            this.isDrawing = false;
            this.context.closePath();
          }
          if (event.type === TouchType.Move) {
            if (!this.isDrawing) {
              return;
            }
            this.eventType = 'Move';
            this.isEmpty = false;
            // 绘画路径
            this.x = event.touches[0].x;
            this.y = event.touches[0].y;
            this.context.strokeStyle = '#000000';
            this.context.lineWidth = 3;
            this.tempPath.lineTo(this.x, this.y);
            this.context.stroke(this.tempPath);
          }
        });
    
      Column() {
        //resources/media下替换对应图片
        Image(this.isEmpty ? $r('app.media.startIcon') : $r('app.media.startIcon'))
          .width(30)
          .onClick(() => {
            this.context.clearRect(0, 0, 1080, 1922);
            this.isEmpty = true;
          });
      }
      .position({
        x: '85%',
        y: '2%'
      });
    }
    .width('100%')
    .height('70%')
    .backgroundColor('#F1F3F5')
    .margin({
      top: 36,
      bottom: 5
    })
    .visibility(this.isShow ? Visibility.Visible : Visibility.None);
    ```
    
2.  将绘制的Canvas内容以png的形式保存到沙箱路径。
    
    ```
    // 将rawfile中的pdf文件转存至沙箱路径供预览使用
    savePdfToCache() {
      this.UIContext.getHostContext()?.resourceManager.getRawFd('test.pdf', (err, data) => {
        let filePath = this.UIContext.getHostContext()?.tempDir + '/test.pdf';
        let dest = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
        let bufsize = 4096;
        let buf = new ArrayBuffer(bufsize);
        let off = 0, len = 0, readedLen = 0;
        // 通过buffer将rawfile文件内容copy到沙箱路径
        while ((len = fs.readSync(data.fd, buf, { offset: data.offset + off, length: bufsize })) > 0) {
          readedLen += len;
          fs.writeSync(dest.fd, buf, { offset: off, length: len });
          off = off + len;
          if ((data.length - readedLen) < bufsize) {
            bufsize = data.length - readedLen;
          }
        }
        fs.close(dest.fd);
      });
    }
    ```
    
3.  使用addWatermark方法将图片作为水印添加到PDF上。
    
    ```
    // 将沙箱中的canvas图片附加到pdf上
    mergeCanvasToPdf() {
      let filePath = this.UIContext.getHostContext()?.tempDir + '/test.pdf';
      let res = this.pdfDocument.loadDocument(filePath);
      if (res === pdfService.ParseResult.PARSE_SUCCESS) {
        let wminfo: pdfService.ImageWatermarkInfo = new pdfService.ImageWatermarkInfo();
        wminfo.watermarkType = pdfService.WatermarkType.WATERMARK_IMAGE;
        wminfo.imagePath = this.path;
        wminfo.opacity = 1;
        wminfo.isOnTop = true;
        wminfo.rotation = 0;
        wminfo.scale = 0.5;
        wminfo.verticalAlignment = pdfService.WatermarkAlignment.WATERMARK_ALIGNMENT_TOP;
        wminfo.horizontalAlignment = pdfService.WatermarkAlignment.WATERMARK_ALIGNMENT_LEFT;
        wminfo.horizontalSpace = 0;
        wminfo.verticalSpace = 0;
        this.pdfDocument.addWatermark(wminfo, 0, 1, true, true);
        let outPdfPath = this.UIContext.getHostContext()?.filesDir + '/testImageWatermark.pdf';
        let result = this.pdfDocument.saveDocument(outPdfPath);
        if (result) {
          this.showToast('合并成功');
        }
        hilog.info(0x0000, 'PdfPage', 'addImageWatermark %{public}s!', result ? 'success' : 'fail');
      }
      this.pdfDocument.releaseDocument();
    }
    ```
    

完整代码如下：

```
import { pdfService, PdfView, pdfViewManager } from '@kit.PDFKit';
import { image } from '@kit.ImageKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct PaintPage {
  pathStack: NavPathStack = new NavPathStack();
  @StorageProp('topRectHeight') topRectHeight: number = 0;
  @StorageProp('bottomRectHeight') bottomRectHeight: number = 0;
  @State eventType: string = ''; //手指触碰事件类型
  @Provide isEraserMode: boolean = false; // 橡皮擦模式
  @State isDrawing: boolean = false; // 绘画笔
  @State showThickness: boolean = false;
  @State isEmpty: boolean = true;
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private x: number = 0; // 触摸点x坐标
  private y: number = 0; // 触摸点y坐标
  private tempPath: Path2D = new Path2D(); // 临时绘画路径
  @State pixelMap: image.PixelMap | undefined = undefined;
  @State UIContext: UIContext = this.getUIContext();
  private pdfDocument: pdfService.PdfDocument = new pdfService.PdfDocument();
  @State path: string = '';
  private controller: pdfViewManager.PdfController = new pdfViewManager.PdfController();
  @State isShow: boolean = true;
  private loadResult: pdfService.ParseResult = pdfService.ParseResult.PARSE_ERROR_FORMAT;

  // 将canvas图片以png形式保存至沙箱
  async saveCacheImg(pixelMap: image.PixelMap, path: string): Promise<string> {
    // 通过packing生成buffer数据写入文件 设置图片格式，并返回buffer
    const pixelMapArrayBuffer: ArrayBuffer = await image.createImagePacker().packToData(pixelMap, {
      format: 'image/png',
      quality: 98
    });
    let file = fs.openSync(path, fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE);
    await fs.write(file.fd, pixelMapArrayBuffer);
    fs.closeSync(file);
    hilog.info(0x0000, 'PdfPage', 'Save Canvas %{public}s!', file ? 'success' : 'fail');
    this.showToast('canvas保存成功');
    return path;
  }

  // 将rawfile中的pdf文件转存至沙箱路径供预览使用
  savePdfToCache() {
    this.UIContext.getHostContext()?.resourceManager.getRawFd('test.pdf', (err, data) => {
      let filePath = this.UIContext.getHostContext()?.tempDir + '/test.pdf';
      let dest = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
      let bufsize = 4096;
      let buf = new ArrayBuffer(bufsize);
      let off = 0, len = 0, readedLen = 0;
      // 通过buffer将rawfile文件内容copy到沙箱路径
      while ((len = fs.readSync(data.fd, buf, { offset: data.offset + off, length: bufsize })) > 0) {
        readedLen += len;
        fs.writeSync(dest.fd, buf, { offset: off, length: len });
        off = off + len;
        if ((data.length - readedLen) < bufsize) {
          bufsize = data.length - readedLen;
        }
      }
      fs.close(dest.fd);
    });
  }

  // 将沙箱中的canvas图片附加到pdf上
  mergeCanvasToPdf() {
    let filePath = this.UIContext.getHostContext()?.tempDir + '/test.pdf';
    let res = this.pdfDocument.loadDocument(filePath);
    if (res === pdfService.ParseResult.PARSE_SUCCESS) {
      let wminfo: pdfService.ImageWatermarkInfo = new pdfService.ImageWatermarkInfo();
      wminfo.watermarkType = pdfService.WatermarkType.WATERMARK_IMAGE;
      wminfo.imagePath = this.path;
      wminfo.opacity = 1;
      wminfo.isOnTop = true;
      wminfo.rotation = 0;
      wminfo.scale = 0.5;
      wminfo.verticalAlignment = pdfService.WatermarkAlignment.WATERMARK_ALIGNMENT_TOP;
      wminfo.horizontalAlignment = pdfService.WatermarkAlignment.WATERMARK_ALIGNMENT_LEFT;
      wminfo.horizontalSpace = 0;
      wminfo.verticalSpace = 0;
      this.pdfDocument.addWatermark(wminfo, 0, 1, true, true);
      let outPdfPath = this.UIContext.getHostContext()?.filesDir + '/testImageWatermark.pdf';
      let result = this.pdfDocument.saveDocument(outPdfPath);
      if (result) {
        this.showToast('合并成功');
      }
      hilog.info(0x0000, 'PdfPage', 'addImageWatermark %{public}s!', result ? 'success' : 'fail');
    }
    this.pdfDocument.releaseDocument();
  }

  // 加载pdf
  async loadPdf(path: string) {
    // 先释放再加载
    this.controller.releaseDocument();
    this.loadResult = await this.controller.loadDocument(path);
    if (this.loadResult == 0) {
      this.showToast('pdf加载成功');
    }
    hilog.info(0x0000, 'PdfPage', 'loadMergePdf %{public}s!', this.loadResult == 0 ? 'success' : 'fail');
  }

  // 提示
  showToast(str: string) {
    this.getUIContext().getPromptAction().showToast({
      message: str,
      duration: 500,
      showMode: promptAction.ToastShowMode.DEFAULT,
      bottom: 80
    });
  }

  aboutToAppear(): void {
    this.savePdfToCache();
  }

  build() {
    Column() {
      Column() {
        Stack() {
          Canvas(this.context)
            .width('100%')
            .height('100%')
            .backgroundColor(Color.White)
            .onTouch((event) => {
              if (event.type === TouchType.Down) {
                this.showThickness = false;
                this.eventType = 'Down';
                this.isDrawing = true;
                this.x = event.touches[0].x;
                this.y = event.touches[0].y;
                this.context.beginPath();
                this.tempPath = new Path2D();
                this.tempPath.moveTo(this.x, this.y);
                this.context.lineCap = 'round';
              }
              if (event.type === TouchType.Up) {
                this.eventType = 'Up';
                this.isDrawing = false;
                this.context.closePath();
              }
              if (event.type === TouchType.Move) {
                if (!this.isDrawing) {
                  return;
                }
                this.eventType = 'Move';
                this.isEmpty = false;
                // 绘画路径
                this.x = event.touches[0].x;
                this.y = event.touches[0].y;
                this.context.strokeStyle = '#000000';
                this.context.lineWidth = 3;
                this.tempPath.lineTo(this.x, this.y);
                this.context.stroke(this.tempPath);
              }
            });

          Column() {
            //resources/media下替换对应图片
            Image(this.isEmpty ? $r('app.media.startIcon') : $r('app.media.startIcon'))
              .width(30)
              .onClick(() => {
                this.context.clearRect(0, 0, 1080, 1922);
                this.isEmpty = true;
              });
          }
          .position({
            x: '85%',
            y: '2%'
          });
        }
        .width('100%')
        .height('70%')
        .backgroundColor('#F1F3F5')
        .margin({
          top: 36,
          bottom: 5
        })
        .visibility(this.isShow ? Visibility.Visible : Visibility.None);

        Column() {
          PdfView({
            controller: this.controller,
            pageFit: pdfService.PageFit.FIT_WIDTH,
            showScroll: false,
          })
            .id('pdfview_app_view')
            .layoutWeight(1);
        }
        .width('100%')
        .height('70%')
        .backgroundColor('#F1F3F5')
        .margin({
          top: 36,
          bottom: 5
        })
        .visibility(this.isShow ? Visibility.None : Visibility.Visible);

        Row() {
          Button('保存绘画')
            .width('120')
            .height('40')
            .margin({ right: 10 })
            .onClick(() => {
              this.pixelMap = this.context.getPixelMap(0, 0, 300, 300);
              this.path = this.UIContext.getHostContext()?.tempDir + '/canvas.png';
              this.saveCacheImg(this.pixelMap, this.path);
            });
          Button('合并至pdf')
            .width('120')
            .height('40')
            .onClick(() => {
              this.mergeCanvasToPdf();
            });
        }
        .margin({
          top: 10,
          bottom: 5
        })

        Row() {
          Button('加载原pdf')
            .width('120')
            .height('40')
            .margin({ right: 10 })
            .onClick(() => {
              this.loadPdf(this.UIContext.getHostContext()?.tempDir + '/test.pdf');
            });
          Button('加载新pdf')
            .width('120')
            .height('40')
            .onClick(() => {
              this.loadPdf(this.UIContext.getHostContext()?.filesDir + '/testImageWatermark.pdf');
            });
        }
        .margin({
          top: 5,
          bottom: 5
        });

        Button('切换展示')
          .width('120')
          .height('40')
          .onClick(() => {
            this.isShow = !this.isShow;
          })
          .margin({
            top: 5,
            bottom: 10
          });
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#F1F3F5')
      .padding({
        top: this.topRectHeight,
        left: 16,
        right: 16
      });
    };
  }
}
```