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
相关产品推荐
相关产品推荐

