You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular:如何在HTMLCanvasElement渲染的PDF上绘图并保存修改

解决方案

问题核心原因

你当前复用同一个canvas渲染所有PDF页面,绘图内容直接画在共享canvas上,没有和具体页面绑定。切换页面时仅重新渲染PDF页,之前的绘图未被清除或关联到对应页面,导致绘图内容随canvas“跟随”到其他页面;同时pdfjs仅用于渲染,不支持直接编辑保存PDF,需要额外的PDF编辑库处理。

分步解决

1. 给每页绑定独立的绘图数据

在组件中维护一个映射存储每页的绘图内容,切换页面时先保存当前页绘图,再加载新页并恢复对应页的绘图。

组件内新增变量

// 存储每一页的绘图数据,key为页码,value为canvas的ImageData
pageDrawData: Map<number, ImageData> = new Map();
// 保存原PDF的ArrayBuffer,用于后续编辑导出
originalPdfBuffer: ArrayBuffer | null = null;
// 当前页面的绘图上下文
currentDrawCtx: CanvasRenderingContext2D | null = null;

修改loadPDF方法,保存原PDF buffer

loadPDF() {
  this.commessaService.uploadPDF().subscribe(pdf => {
    this.originalPdfBuffer = pdf; // 保存原PDF的buffer
    new Response(pdf).arrayBuffer().then(arr => this.openPDF(arr));
  });
}

修改handlePages方法,渲染PDF后恢复对应页的绘图

handlePages(page: any) {
  const scale = 1.5;
  const viewport = page.getViewport({ scale });
  const canvas = <HTMLCanvasElement>document.getElementById("canvas");
  canvas.style.display = "block";
  const context = canvas.getContext("2d") as CanvasRenderingContext2D;
  this.currentDrawCtx = context;

  // 设置canvas尺寸
  canvas.width = Math.floor(viewport.width);
  canvas.height = Math.floor(viewport.height);
  canvas.style.width = `${Math.floor(viewport.width)}px`;
  canvas.style.height = `${Math.floor(viewport.height)}px`;

  // 先渲染PDF页面,渲染完成后恢复该页的绘图
  const renderTask = page.render({ canvasContext: context, viewport: viewport });
  renderTask.promise.then(() => {
    const drawData = this.pageDrawData.get(this.currPage);
    if (drawData) {
      context.putImageData(drawData, 0, 0);
    }
  });
}

新增保存当前页绘图的方法

saveCurrentPageDrawing() {
  if (!this.currentDrawCtx || !this.currPage) return;
  const canvas = this.currentDrawCtx.canvas;
  const imageData = this.currentDrawCtx.getImageData(0, 0, canvas.width, canvas.height);
  this.pageDrawData.set(this.currPage, imageData);
}

修改nextPage方法,切换前保存当前页绘图

nextPage() {
  if (this.thePDF != null && (this.currPage + 1) <= this.numPages) {
    this.saveCurrentPageDrawing(); // 保存当前页绘图
    this.currPage++;
    this.thePDF.getPage(this.currPage).then(page => this.handlePages(page));
  }
}

同理,上一页方法(prevPage)也要添加this.saveCurrentPageDrawing()逻辑。

2. 编辑并导出PDF到后端

使用pdf-lib库将原PDF和每页的绘图内容合并,生成新的PDF后发送到后端保存。

安装pdf-lib

npm install @pdf-lib/core

组件内导入pdf-lib

import { PDFDocument } from '@pdf-lib/core';

新增导出并上传的方法

async exportAndUploadEditedPDF() {
  if (!this.originalPdfBuffer) return;

  // 加载原PDF
  const pdfDoc = await PDFDocument.load(this.originalPdfBuffer);
  const pdfPages = pdfDoc.getPages();

  // 遍历每页,添加绘图内容
  for (let pageNum = 1; pageNum <= this.numPages; pageNum++) {
    const drawData = this.pageDrawData.get(pageNum);
    if (!drawData) continue;

    // 将ImageData转换为PNG图片
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = drawData.width;
    tempCanvas.height = drawData.height;
    const tempCtx = tempCanvas.getContext('2d');
    if (!tempCtx) continue;

    tempCtx.putImageData(drawData, 0, 0);
    const imageBlob = await new Promise<Blob>((resolve) => tempCanvas.toBlob(resolve, 'image/png'));
    if (!imageBlob) continue;

    // 将图片嵌入PDF
    const imageBytes = new Uint8Array(await imageBlob.arrayBuffer());
    const pdfImage = await pdfDoc.embedPng(imageBytes);

    // 获取原PDF页面尺寸,计算缩放比例(匹配渲染时的scale=1.5)
    const targetPage = pdfPages[pageNum - 1]; // pdf-lib页面索引从0开始
    const { width, height } = targetPage.getSize();
    const imgWidth = width;
    const imgHeight = height;

    // 将图片绘制到PDF页面的对应位置
    targetPage.drawImage(pdfImage, {
      x: 0,
      y: 0,
      width: imgWidth,
      height: imgHeight,
    });
  }

  // 生成修改后的PDF字节数据
  const modifiedPdfBytes = await pdfDoc.save();

  // 构造FormData发送到后端
  const formData = new FormData();
  formData.append('editedPdf', new Blob([modifiedPdfBytes], { type: 'application/pdf' }), 'edited.pdf');

  // 调用服务上传
  this.commessaService.saveEditedPDF(formData).subscribe({
    next: () => console.log('PDF保存成功'),
    error: (err) => console.error('PDF保存失败', err)
  });
}

服务端添加保存接口(Angular服务示例)

// commessa.service.ts
saveEditedPDF(formData: FormData): Observable<any> {
  return this.http.post('/api/pdf/save', formData);
}

后端示例(Node.js/Express)

使用multer处理文件上传:

const express = require('express');
const multer = require('multer');
const app = express();

// 配置文件存储路径
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, './saved-pdfs'); // 保存到指定目录
  },
  filename: (req, file, cb) => {
    cb(null, `edited-${Date.now()}.pdf`); // 自定义文件名
  }
});
const upload = multer({ storage });

// 保存PDF的接口
app.post('/api/pdf/save', upload.single('editedPdf'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ message: '未上传文件' });
  }
  res.status(200).json({ message: 'PDF保存成功', filePath: req.file.path });
});

app.listen(3000, () => console.log('服务启动在3000端口'));

关键注意事项

  • 绘图操作完成后(比如鼠标松开、画笔结束),需要调用saveCurrentPageDrawing()保存当前页的绘图状态
  • pdf-lib的页面索引从0开始,和你的页码(从1开始)需要做好对应
  • 发送FormData时不需要手动设置Content-Type,浏览器会自动处理

内容的提问来源于stack exchange,提问作者Francesco Notte

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 04:35:28