news 2026/10/30 3:46:11

Vue3/React结合pdfjs实现PDF电子签章与导出功能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3/React结合pdfjs实现PDF电子签章与导出功能

1. 项目概述

在现代Web应用中,PDF处理已成为常见需求。基于Vue3/React框架结合pdfjs实现PDF多页展示、拖拽盖章签名及导出功能,能够满足电子合同、在线审批等业务场景的需求。这个方案的核心在于利用pdfjs-dist库解析PDF文件,通过Canvas渲染页面,再借助Fabric.js实现交互式绘图层。

我曾在一个金融风控系统中实现过类似功能,用户需要在贷款审批流程中对PDF合同进行电子签章。最初尝试了几种方案后,发现pdfjs+Fabric的组合在性能和兼容性上表现最佳。下面将详细介绍这个方案的具体实现。

2. 技术选型与准备

2.1 核心库介绍

pdfjs-dist:Mozilla开源的PDF解析库,能够将PDF文档渲染到Canvas上。最新版本支持:

  • 文本选择和高亮
  • 多页渲染
  • 搜索功能
  • 自定义缩放

Fabric.js:强大的Canvas操作库,提供:

  • 对象模型(支持图像、文本、路径等)
  • 交互式操作(拖拽、缩放、旋转)
  • 序列化/反序列化
  • 事件系统

2.2 环境配置

安装所需依赖:

npm install pdfjs-dist@2.16.105 fabric@5.3.1 lodash-es

对于Vue3项目,还需要安装拖拽组件:

npm install vuedraggable@4.1.0

注意:pdfjs-dist的worker文件需要单独配置,建议使用CDN引入以减小打包体积。

3. 核心实现步骤

3.1 PDF加载与渲染

首先实现PDF文件加载和页面渲染:

// PDF加载函数 const loadPDF = async (url) => { // 设置worker路径(重要!) PDFJS.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/build/pdf.worker.min.js' const loadingTask = PDFJS.getDocument({ url, cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/cmaps/', cMapPacked: true }) try { const pdf = await loadingTask.promise const numPages = pdf.numPages const pageRatios = [] // 预计算所有页面尺寸 for (let i = 1; i <= numPages; i++) { const page = await pdf.getPage(i) const viewport = page.getViewport({ scale: 1 }) pageRatios.push(viewport.width / viewport.height) } return { pdf, numPages, pageRatios } } catch (error) { console.error('PDF加载失败:', error) throw error } }

3.2 多页Canvas渲染

实现PDF多页渲染到Canvas:

// 渲染PDF页面到Canvas const renderPages = async (pdfDoc, containerEl) => { const canvases = [] for (let i = 1; i <= pdfDoc.numPages; i++) { const page = await pdfDoc.getPage(i) const viewport = page.getViewport({ scale: 1.5 }) // 适当放大 const canvas = document.createElement('canvas') canvas.id = `pdf-page-${i}` canvas.width = viewport.width canvas.height = viewport.height containerEl.appendChild(canvas) await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise canvases.push({ canvas, pageNum: i, width: viewport.width, height: viewport.height }) } return canvases }

3.3 Fabric画布叠加

创建交互层覆盖PDF:

// 初始化Fabric画布 const initFabricCanvas = (width, height) => { const fabricCanvas = new fabric.Canvas('fabric-canvas', { width, height, selection: false, backgroundColor: 'transparent' }) // 防止对象超出画布 fabricCanvas.on('object:moving', (e) => { const obj = e.target obj.setCoords() // 边界检查 if (obj.left < 0) obj.left = 0 if (obj.top < 0) obj.top = 0 if (obj.left + obj.width > width) obj.left = width - obj.width if (obj.top + obj.height > height) obj.top = height - obj.height }) return fabricCanvas }

4. 拖拽盖章实现

4.1 印章拖拽交互

实现从侧边栏拖拽印章到PDF上的功能:

<template> <div class="container"> <!-- 印章列表 --> <div class="stamp-palette"> <div v-for="(stamp, index) in stamps" :key="index" class="stamp-item" draggable="true" @dragstart="onDragStart($event, stamp)" > <img :src="stamp.image" :alt="stamp.name"> </div> </div> <!-- PDF查看区域 --> <div class="pdf-viewer" @drop="onDrop" @dragover.prevent> <div v-for="page in pages" :key="page.id" class="pdf-page"> <canvas :id="`pdf-${page.id}`"></canvas> </div> <canvas id="fabric-canvas"></canvas> </div> </div> </template> <script> export default { methods: { onDragStart(e, stamp) { e.dataTransfer.setData('stamp', JSON.stringify(stamp)) }, onDrop(e) { const stamp = JSON.parse(e.dataTransfer.getData('stamp')) const rect = e.currentTarget.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top this.addStampToCanvas(stamp, x, y) } } } </script>

4.2 印章定位计算

精准计算印章在PDF上的位置:

// 添加印章到画布 function addStampToCanvas(stamp, clientX, clientY) { // 计算相对位置 const pdfContainer = document.querySelector('.pdf-viewer') const scrollTop = pdfContainer.scrollTop const scrollLeft = pdfContainer.scrollLeft // 计算落在哪个PDF页面上 let pageIndex = 0 let accumulatedHeight = 0 for (let i = 0; i < this.pages.length; i++) { accumulatedHeight += this.pages[i].height if (clientY + scrollTop <= accumulatedHeight) { pageIndex = i break } } // 计算在该页面内的相对位置 const pageOffset = pageIndex > 0 ? accumulatedHeight - this.pages[pageIndex].height : 0 const relativeY = clientY + scrollTop - pageOffset // 创建Fabric图像对象 fabric.Image.fromURL(stamp.image, (img) => { img.set({ left: clientX + scrollLeft, top: relativeY, scaleX: 0.5, scaleY: 0.5, data: { type: 'stamp', page: pageIndex + 1, stampId: stamp.id } }) this.fabricCanvas.add(img) this.fabricCanvas.renderAll() }) }

5. 导出功能实现

5.1 导出图片

将带有印章的PDF导出为图片:

// 导出为图片 function exportToImage() { // 创建临时Canvas合并所有内容 const tempCanvas = document.createElement('canvas') const tempCtx = tempCanvas.getContext('2d') // 计算总高度 let totalHeight = 0 let maxWidth = 0 this.pages.forEach(page => { totalHeight += page.height maxWidth = Math.max(maxWidth, page.width) }) tempCanvas.width = maxWidth tempCanvas.height = totalHeight // 绘制所有PDF页面 let currentY = 0 this.pages.forEach(page => { const pageCanvas = document.getElementById(`pdf-${page.id}`) tempCtx.drawImage(pageCanvas, 0, currentY) currentY += page.height }) // 绘制印章层 const fabricCanvasEl = this.fabricCanvas.getElement() tempCtx.drawImage(fabricCanvasEl, 0, 0) // 生成图片 const imgData = tempCanvas.toDataURL('image/png') const link = document.createElement('a') link.href = imgData link.download = 'signed-document.png' link.click() }

5.2 导出PDF

将修改后的内容导出为PDF(需要pdf-lib库):

npm install pdf-lib@1.17.1

实现代码:

async function exportToPDF() { const { PDFDocument, rgb } = PDFLib // 创建新PDF文档 const pdfDoc = await PDFDocument.create() // 处理每一页 for (const page of this.pages) { // 添加新页 const newPage = pdfDoc.addPage([page.width, page.height]) // 获取原始PDF页面图像 const canvas = document.getElementById(`pdf-${page.id}`) const pngImage = await pdfDoc.embedPng(canvas.toDataURL('image/png')) // 绘制原始页面 newPage.drawImage(pngImage, { x: 0, y: 0, width: page.width, height: page.height }) // 绘制该页的印章 const stamps = this.fabricCanvas.getObjects().filter(obj => { return obj.data?.page === page.number }) for (const stamp of stamps) { const stampImg = await pdfDoc.embedPng(stamp.toDataURL({ format: 'png', quality: 1 })) newPage.drawImage(stampImg, { x: stamp.left, y: page.height - stamp.top - stamp.height * stamp.scaleY, width: stamp.width * stamp.scaleX, height: stamp.height * stamp.scaleY }) } } // 生成并下载 const pdfBytes = await pdfDoc.save() const blob = new Blob([pdfBytes], { type: 'application/pdf' }) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = 'signed-document.pdf' link.click() }

6. 性能优化与常见问题

6.1 大PDF处理优化

处理大型PDF时的性能建议:

  1. 分页加载:不要一次性渲染所有页面,实现懒加载
  2. Canvas回收:离开视口的页面释放Canvas内存
  3. 分辨率控制:根据设备DPI调整渲染比例
// 视口检测实现懒加载 const lazyLoadPages = () => { const viewer = document.querySelector('.pdf-viewer') const visibleHeight = viewer.clientHeight const scrollTop = viewer.scrollTop this.pages.forEach(async (page) => { const pageEl = document.getElementById(`pdf-page-${page.number}`) if (!pageEl) return const rect = pageEl.getBoundingClientRect() const isVisible = rect.top < visibleHeight && rect.bottom > 0 if (isVisible && !page.rendered) { await renderPage(page.number) page.rendered = true } }) } // 监听滚动事件 viewer.addEventListener('scroll', _.throttle(lazyLoadPages, 200))

6.2 常见问题排查

问题1:印章位置偏移

  • 原因:PDF渲染缩放与Canvas尺寸不匹配
  • 解决:确保Fabric画布与PDF Canvas使用相同的尺寸比例
// 正确的尺寸同步 function syncCanvasSizes() { const pdfCanvas = document.getElementById('pdf-page-1') const { width, height } = pdfCanvas this.fabricCanvas.setDimensions({ width, height }) this.fabricCanvas.setZoom(1) // 重置缩放 this.fabricCanvas.renderAll() }

问题2:移动端触摸支持

  • 原因:原生拖拽事件在移动端不工作
  • 解决:添加触摸事件支持
// 添加触摸支持 function initTouchEvents() { const container = document.querySelector('.pdf-viewer') container.addEventListener('touchstart', (e) => { if (e.target.classList.contains('stamp-item')) { this.draggingStamp = this.stamps.find( s => s.id == e.target.dataset.stampId ) } }) container.addEventListener('touchend', (e) => { if (this.draggingStamp) { const touch = e.changedTouches[0] const { clientX, clientY } = touch this.addStampToCanvas(this.draggingStamp, clientX, clientY) this.draggingStamp = null } }) }

问题3:导出的PDF文字不可选

  • 原因:Canvas渲染的是图像而非原始文本
  • 解决:使用pdf-lib合并原始PDF文本层
async function exportWithTextLayer(originalPdfBytes) { const originalPdf = await PDFDocument.load(originalPdfBytes) const newPdf = await PDFDocument.create() // 复制每一页内容和注释 const pages = await newPdf.copyPages(originalPdf, originalPdf.getPageIndices() ) pages.forEach(page => newPdf.addPage(page)) // 添加印章作为注释 const stamps = this.fabricCanvas.getObjects() for (const stamp of stamps) { const page = newPdf.getPage(stamp.data.page - 1) const stampImg = await newPdf.embedPng(stamp.toDataURL()) page.drawImage(stampImg, { x: stamp.left, y: page.getHeight() - stamp.top - stamp.height * stamp.scaleY, width: stamp.width * stamp.scaleX, height: stamp.height * stamp.scaleY }) } // 导出 const bytes = await newPdf.save() return bytes }

7. 安全与合规考虑

  1. 签名验证:所有印章应包含数字签名防止篡改
  2. 操作审计:记录所有盖章操作的时间戳和用户信息
  3. 水印保护:导出的PDF应添加隐形水印
  4. 权限控制:限制印章使用权限和下载权限

实现示例:

// 添加数字水印 function addWatermark(ctx, text, width, height) { ctx.save() ctx.globalAlpha = 0.1 ctx.font = '20px Arial' ctx.fillStyle = '#000000' ctx.rotate(-0.26) for (let y = 50; y < height; y += 100) { for (let x = 20; x < width; x += 200) { ctx.fillText(text, x, y) } } ctx.restore() } // 在导出时调用 const canvas = document.createElement('canvas') // ...绘制内容... addWatermark(canvas.getContext('2d'), `Confidential ${new Date().toISOString()}`, canvas.width, canvas.height )

8. 项目扩展方向

  1. 多人协作签名:使用WebSocket实现多用户同时签署文档
  2. 手写签名:集成签名板支持手写签名
  3. 模板系统:预设常用盖章位置模板
  4. OCR识别:自动识别PDF中的签名区域
  5. 区块链存证:将签名哈希上链存证

手写签名实现示例:

// 初始化签名板 function initSignaturePad() { const canvas = document.getElementById('signature-pad') const pad = new SignaturePad(canvas, { backgroundColor: 'rgba(255, 255, 255, 0)', penColor: 'rgb(0, 0, 0)' }) // 导出签名为图像 document.getElementById('save-sig').addEventListener('click', () => { if (pad.isEmpty()) { alert('请先提供签名') } else { const dataURL = pad.toDataURL('image/png') this.addStampToCanvas({ id: 'user-sig', image: dataURL }, 100, 100) pad.clear() } }) }

在实际项目中,这种PDF交互方案已经帮助我们将合同签署流程从原来的3-5天缩短到30分钟以内。关键在于处理好细节问题,比如:

  • 高DPI设备的显示适配
  • 不同PDF页面的尺寸统一处理
  • 印章位置的持久化存储
  • 移动端的手势操作支持

通过合理的架构设计和性能优化,即使处理100页以上的PDF文档也能保持流畅的交互体验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 3:44:11

Python+Django构建高效律所案件管理系统实践

1. 项目概述&#xff1a;Python在律所案件管理中的独特价值作为一名在律所信息化领域摸爬滚打多年的技术顾问&#xff0c;我见过太多律所还在用Excel甚至纸质档案管理案件。去年帮某中型律所部署Python案件管理系统后&#xff0c;他们的案件处理效率提升了47%&#xff0c;这让我…

作者头像 李华
网站建设 2026/10/30 3:43:47

SpringBoot超市管理系统:可调试、可部署的完整实战项目

简介&#xff1a;这是一套基于Spring Boot开发的超市信息管理系统完整源码资源&#xff0c;面向Java初学者与Web全栈学习者&#xff0c;解决中小型商超场景下的商品采购、销售、库存盘点及财务分析等核心业务管理需求。资源包大小为7.92MB&#xff0c;为RAR压缩格式&#xff0c…

作者头像 李华
网站建设 2026/10/30 3:43:39

OpenLoong开源机器人社区春节活动与技术贡献指南

1. 活动背景与社区定位OpenLoong作为国内领先的开源机器人技术社区&#xff0c;近期通过GitHub和AtomGit平台展示了其在人形机器人动态控制、强化学习仿真等领域的核心技术积累。社区主要聚焦三大方向&#xff1a;OpenLoong-Dyn-Control&#xff08;全身动力学控制软件包&#…

作者头像 李华
网站建设 2026/10/30 3:43:10

C++移动构造函数原理与性能优化实践

1. Move构造函数的本质与价值在C11之前&#xff0c;对象资源的转移只能通过拷贝构造函数实现&#xff0c;这导致大量不必要的内存分配和释放。移动语义的出现彻底改变了这一局面&#xff0c;它允许我们将资源的所有权从一个对象直接转移到另一个对象&#xff0c;而无需进行深拷…

作者头像 李华
网站建设 2026/10/30 3:42:23

LCD1602接PCF8574:I2C转并口、准双向口与4位模式时序

简介&#xff1a;面向51单片机初学者与电子制作爱好者&#xff0c;这份四针LCD1602 IIC驱动PCF8574T资料聚焦以两根I2C线加电源线控制字符液晶、缓解IO资源紧张的问题&#xff0c;可用于STC89C51等8051内核平台的显示模块实验与小项目移植。压缩包共25个文件、约55KB&#xff0…

作者头像 李华
网站建设 2026/10/30 3:42:23

桌面刻度尺精度实现:屏幕校准、缩放换算与多屏适配

1. 屏幕上量出来的"1厘米"&#xff0c;凭什么信它去年帮一个做硬件结构的朋友改图纸&#xff0c;他截图发过来&#xff0c;说这个卡扣槽宽"大概两厘米"&#xff0c;让我在页面上直接量一下对不对。我当时随手拿屏幕当尺子比了比&#xff0c;觉得差不多&…

作者头像 李华