简介面向Vue.js开发者这份资源聚焦于在项目中实现PDF文件的上传、盖章、签名与下载适合需要处理合同签署、文档审批等业务场景的前端工程师。压缩包整体为zip格式共19个文件包含5个vue组件、5个js脚本、3个json配置并有页面入口html、项目图标、说明文档等辅助文件整体大小仅216KB结构紧凑核心代码与配置分离便于跟踪学习。目前已有1457人学习下载印证了其在PDF交互场景中的实用价值。资源内提供完整的Vue项目示例覆盖基于pdfjs-dist的PDF渲染、vue-pdf-signature插件添加签名与印章、axios上传文件到后端以及通过隐藏链接触发下载的完整链路。读者可从中提取组件化封装思路、事件处理与前后端协作要点并能直接参考现成代码快速搭建可运行的PDF盖章签名功能。1. 先想清楚上传 PDF 盖章签名到底在解决什么做业务系统的前端迟早会碰到这个需求用户上传一份 PDF 合同或审批单需要在线盖一个公司印章、签一个手写名字然后下载一份已经生效的 PDF 回去打印或归档。这个场景在办公审批、人事流程、招投标文件里非常常见很多团队第一次接到需求时都会觉得「不就是上传、画个章、下载吗」真做起来才发现里面全是坑印章位置飘了、文字选了复制不了、文件从几百 KB 变成几十兆、下载下来的 PDF 根本打不开。这类需求的核心不是「显示印章」而是「在保持原始 PDF 内容和版式不变的前提下把签章作为可保留的图层写入文件」。所以方案选型的判断标准就三条原始文字层不能丢、文件体积不能失控、下载后的 PDF 必须能在主流阅读器里正常打开。这篇文章我会把从技术选型到落地实现的全过程拆开讲包括坐标换算、参数调优、常见翻车点照着做基本能跑通。2. 技术选型三条路线里为什么最后留了 pdf-lib2.1 先排除最省事的方案整页转图片再导出市面上有很多「在网页上直接编辑 PDF」的现成组件但大多数走的是同一个路子把 PDF 每一页渲染成 Canvas 图片然后在图片上叠印章、叠签名最后把整张图片导出为新的 PDF。这个方案实现起来最快界面也好做但代价是致命的——原来 PDF 里的所有文字都被拍扁成图片了文件体积直接膨胀好几倍文字不能选中、不能复制打印出来清晰度也随缩放比例打折。我接过一个需求原始合同 PDF 只有 800 KB整页渲染导出之后变成了 23 MB而且里面每一页的文字都成了图像。用户拿到手之后第一件事就是来找茬为什么我 PDF 里的文字都不能搜索了这属于「能用但没法交差」的方案只适合做预览不适合做成品下载。2.2 主流的可靠组合pdf.js 做预览pdf-lib 做写入正确的做法是拆成两段预览交互用pdfjs-dist渲染页面签章的最终写入用pdf-lib在原始 PDF 上叠加图片图层。这样原始 PDF 的每个字节都保留着印章和签名只是作为一个额外的图像对象嵌入到页面内容流里PDF 里的文字照样能复制、能搜索文件体积增加量也可以控制在几十到几百 KB。pdfjs-dist是 Mozilla 的 PDF 渲染引擎负责把 PDF 画到 Canvas 上同时我们从中拿到 PDF 页面的原始尺寸数据pdf-lib是一个纯 JS 的 PDF 操作库不依赖浏览器原生插件支持读取已有 PDF、嵌入图片、在指定坐标绘制图像。这两者配合前端就能独立完成一条完整的签署链路。2.3 签名板组件signature_pad 是绕不开的方案手写签名的部分是另一个独立问题。常见做法是用signature_pad这个 Canvas 手写板组件它输出的是透明背景的 PNG 图片正好适合作为图层叠到 PDF 上。选它而不是自己写的原因很简单它内部处理了 Canvas 的坐标缩放问题在高 DPI 屏幕上画出来的签名不会模糊而且 API 很薄只有 start、end、clear、toDataURL 几个方法不引入额外的心智负担。下面这张表是我做方案对比时整理的核心差异你可以直接用方案原始文字保留文件体积增幅实现成本推荐度整页转图片 导出新 PDF丢失5~20 倍低不推荐pdf.js 渲染 jsPDF 重新编排丢失2~5 倍中不推荐pdf.js 渲染 pdf-lib 写入图层保留10~200 KB中高推荐注意上面第二行jsPDF虽然也能做但它擅长的是「从零生成 PDF」往已有 PDF 里加内容时需要先把原 PDF 导入处理起来远不如pdf-lib顺手而且多页文档的坐标对齐很容易出幺蛾子。所以我的结论很直接预览归 pdf.js写入归 pdf-lib签名归 signature_pad各干各的互不干扰。3. 完整实现从上传到下载的七个步骤3.1 安装依赖与初始化项目这一步很简单直接装三个包就行。你的项目如果是 Vue 3 Vite 或者 Vue CLI命令都一样npm install pdfjs-dist pdf-lib signature_pad安装完成后pdfjs-dist需要设置一下 worker 的路径否则会在控制台报警告并且渲染不出页面。Vue 项目里通常在入口文件或者工具模块里做一次全局配置// lib/pdfSetup.js import * as pdfjsLib from pdfjs-dist; import pdfWorker from pdfjs-dist/build/pdf.worker.min.mjs?url; pdfjsLib.GlobalWorkerOptions.workerSrc pdfWorker; export default pdfjsLib;这里的?url后缀是 Vite 的静态资源处理语法直接帮你把 worker 文件变成可访问的 URL。如果你用的是 Vue CLI需要改成import pdfWorker from pdfjs-dist/build/pdf.worker.min.mjs然后把 workerSrc 指向这个模块的 URL或者走 public 目录放文件的老办法。3.2 上传 PDF 并渲染第一页上传组件直接用 el-upload 或者你项目里现成的上传组件都可以关键在拿到 File 对象之后怎么喂给 pdf.js。File 对象本质上是一个 BlobgetDocument接受 ArrayBuffer、URL、TypedArray 等多种输入这里用file.arrayBuffer()是最稳的不会遇到跨域和 URL 失效的问题。async function loadPdf(file) { const arrayBuffer await file.arrayBuffer(); const pdf await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const page await pdf.getPage(1); const viewport page.getViewport({ scale: 1.5 }); const canvas document.getElementById(pdfCanvas); const ctx canvas.getContext(2d); canvas.width viewport.width; canvas.height viewport.height; await page.render({ canvasContext: ctx, viewport }).promise; // 保存必要数据后续坐标换算要用 pdfDocRef.value pdf; currentPageRef.value page; viewportRef.value viewport; pdfSizeRef.value { width: viewport.width, height: viewport.height }; }这段代码的逻辑顺序很关键先解析 PDF 拿到总页数再取第一页的 viewportscale: 1.5是渲染分辨率倍率数字越大 Canvas 越清晰但内存占用越高一般 1.5~2 就够用。渲染完成后要把 pdf 对象、page 对象、viewport 都存到变量里后面盖章定位全靠它们。3.3 印章拖拽坐标采集与图层渲染印章本质是一张 PNG 图片拖拽过程只需要监听 Canvas 上的鼠标事件记录落点位置。这里有一个最容易错的点鼠标在 Canvas 上的像素坐标不等于 PDF 页面上的坐标两者之间隔着一个devicePixelRatio和渲染缩放比。canvas.addEventListener(mousedown, (e) { dragging.value true; const rect canvas.getBoundingClientRect(); const scaleX canvas.width / rect.width; const scaleY canvas.height / rect.height; startX (e.clientX - rect.left) * scaleX; startY (e.clientY - rect.top) * scaleY; }); canvas.addEventListener(mousemove, (e) { if (!dragging.value) return; const rect canvas.getBoundingClientRect(); const scaleX canvas.width / rect.width; const scaleY canvas.height / rect.height; const x (e.clientX - rect.left) * scaleX; const y (e.clientY - rect.top) * scaleY; // 清除后重绘整个页面 印章图层 renderPageWithStamp(x, y); });为什么要做canvas.width / rect.width这个换算因为 CSS 里 Canvas 的显示尺寸和它的位图尺寸可以不一致鼠标事件返回的clientX是基于 CSS 像素的而 Canvas 绘制用的是位图像素。如果你不做这一步在高 DPI 屏幕上印章位置就会明显偏移这是很多人第一次做拖拽盖章时遇到的第一个玄学问题。拖拽结束后记录下来的坐标还需要再从 Canvas 位图像素换算回 PDF 坐标系的点数Point这一步放在 3.5 节和 pdf-lib 写入时一起讲。3.4 手写签名signature_pad 集成与透明背景导出签名画布单独放在一个弹窗或者右侧面板里核心配置就两个penColor设黑色backgroundColor设为透明这样导出的签名图就是透明的 PNG叠到 PDF 上不遮住底下的表格线。import SignaturePad from signature_pad; const canvas document.getElementById(signatureCanvas); const signaturePad new SignaturePad(canvas, { penColor: #1a1a1a, backgroundColor: rgba(0,0,0,0), minWidth: 0.5, maxWidth: 2.5 }); // 确认签名 function confirmSignature() { if (signaturePad.isEmpty()) { alert(请先签名); return; } const dataUrl signaturePad.toDataURL(image/png); signatureImgRef.value dataUrl; }minWidth和maxWidth控制笔迹的粗细范围单位是像素。数值建议保持小一点0.5~2.5 这个区间写出来的字接近真实笔迹。注意签名 Canvas 本身的尺寸也要处理一下如果直接按 300x150 的 CSS 尺寸画在高分屏上导出会模糊需要在初始化时把 Canvas 的位图尺寸乘以window.devicePixelRatio。3.5 核心写入用 pdf-lib 把印章和签名嵌入原始 PDF这是整个方案最关键的一段代码。思路是读取原始 PDF 文件 → 嵌入印章 PNG 和签名 PNG → 获取 PDF 页面尺寸 → 把 Canvas 上的落点坐标换算成 PDF 坐标 → 在对应位置绘制图片 → 把最终字节流保存为 Blob 触发下载。import { PDFDocument } from pdf-lib; async function exportSignedPdf() { // 1. 读取原始 PDF const originalBytes await fileRef.value.arrayBuffer(); const pdfDoc await PDFDocument.load(originalBytes); // 2. 嵌入图片资源 const stampImage await pdfDoc.embedPng(stampImgRef.value); const signImage await pdfDoc.embedPng(signatureImgRef.value); // 3. 获取目标页面默认第一页 const pages pdfDoc.getPages(); const targetPage pages[0]; const { width: pageWidth, height: pageHeight } targetPage.getSize(); // 4. 坐标换算Canvas 像素坐标 - PDF 坐标 const canvasWidth viewportRef.value.width; const canvasHeight viewportRef.value.height; const pdfX (stampPosRef.value.x / canvasWidth) * pageWidth; const pdfY pageHeight - (stampPosRef.value.y / canvasHeight) * pageHeight; // 5. 指定印章尺寸并绘制 const stampWidth 120; // 单位是 PDF 点大约 42 毫米 const stampHeight (stampImage.height / stampImage.width) * stampWidth; targetPage.drawImage(stampImage, { x: pdfX, y: pdfY - stampHeight, width: stampWidth, height: stampHeight }); // 6. 签名绘制到稍靠下的位置 const signWidth 100; const signHeight (signImage.height / signImage.width) * signWidth; targetPage.drawImage(signImage, { x: pdfX 20, y: pdfY - stampHeight - signHeight - 10, width: signWidth, height: signHeight }); // 7. 导出并下载 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(); }这段代码里最需要注意的是一条公式pdfY pageHeight - (canvasY / canvasHeight) * pageHeight。原因是 PDF 的坐标系原点在页面左下角Y 轴向上生长而 Canvas 坐标系原点在左上角Y 轴向下生长。如果你直接把 Canvas 的 Y 坐标当 PDF 的 Y 坐标用印章会跑到页面顶端外面去这也是「盖完章下载下来章不见了」的最常见原因。还有一个细节drawImage里的y参数是图片左下角的位置不是左上角。所以代码里做了y: pdfY - stampHeight来把图片向上推一个自身高度保证用户在 Canvas 上看到的落地点就是印章在 PDF 里的视觉位置。4. 三个必调参数与坐标换算的完整公式4.1 渲染缩放比 scale清晰度与性能的权衡pdf.js 渲染时getViewport的 scale 参数直接决定 Canvas 位图尺寸。scale 越大Canvas 像素越多页面渲染越清晰但内存占用和渲染时间也线性增长。我一般取 1.5作为普通 A4 文字页的默认值如果是包含大量小字的合同取 2 能保证放大后依然锐利。超过 2 就没有实际意义了因为导出时的图像质量只取决于嵌入 PDF 的图片分辨率和预览 Canvas 的 scale 无关。4.2 印章图片的目标宽度PDF 点的直觉换算PDF 的坐标单位是点Point1 英寸 72 点A4 纸宽度约 595 点。一个直径 40 毫米的圆形公章换算过来大约是 113 点。代码里stampWidth 120就是按这个比例取的。你不需要精确到毫米只需要知道一个规律100 点大约等于 35 毫米。这样下次要盖一个 30 毫米的章直接把宽度设为 86 左右即可。4.3 PDF 坐标换算的标准公式把整个换算过程串起来实际上就是三段式第一步鼠标 CSS 坐标 - Canvas 位图像素坐标 canvasX (clientX - rect.left) * (canvas.width / rect.width) canvasY (clientY - rect.top) * (canvas.height / rect.height) 第二步Canvas 位图像素坐标 - PDF 比例坐标 ratioX canvasX / canvas.width ratioY canvasY / canvas.height 第三步比例坐标 - PDF 点坐标 pdfX ratioX * pageWidth pdfY (1 - ratioY) * pageHeight这个公式适用于任何页面尺寸不需要关心 PDF 是 A4 还是自定义大小。你只需要保证 viewport 的宽高和 Canvas 的位图宽高一致——也就是用viewport.width赋值给canvas.width而不是手动写死——整个链路就闭环了。我踩过一次的坑是为了适配不同尺寸的屏幕给 Canvas 设置了 CSS 的width: 100%导致 Canvas 显示尺寸和位图尺寸不一致结果鼠标坐标换算全乱。解决办法就是坚持用位图尺寸做内部计算CSS 尺寸只负责显示不管内部逻辑。5. 避坑手册覆盖五个最容易翻车的环节5.1 印章位置偏移尤其是下载后和预览不一致现象在页面上拖拽印章时位置看着是对的下载 PDF 打开后印章整体偏上或者偏下几十个像素。原因最常见的是坐标换算时忘了 Y 轴反转直接用了 Canvas 的 Y 值其次是 viewport 的 scale 和 Canvas 宽高不匹配导致比例计算错误。解决严格按 4.3 节的公式走三层换算。另外在写入 pdf-lib 之前建议先打印一行日志确认canvasY / canvasHeight应该等于1 - pdfY / pageHeight看到这个等式成立再导出。5.2 下载的 PDF 文件巨大从 1 MB 变成 30 MB现象原始 PDF 1.2 MB盖章下载后 31 MB打开还特别卡。原因印章 PNG 没有压缩或者用的是高分辨率大图一张 1000x1000 的印章 PNG 嵌入 PDF 后会占用大量空间。部分组件在导出时甚至会嵌入整张 Canvas 位图那体积更失控。解决在嵌入之前对印章和签名图片做尺寸控制。签名图控制在 300x150 像素左右印章图控制在 200x200 以内。如果必须用大图先用 Canvas 做一次降采样再转 dataURL。这一步对最终文件体积的影响是数量级的。5.3 老版本 PDF 在 pdf-lib 里加载报错现象PDFDocument.load(bytes)直接抛异常控制台提示cannot parse或者invalid PDF。原因有些历史扫描件或打印驱动生成的 PDF 带有损坏的交叉引用表或异常字体编码pdf-lib 严格模式下无法解析。解决PDFDocument.load支持传入{ ignoreEncryption: true }和{ parseSpeed: 0 }这类容错参数。我一般会先捕获异常如果失败就用{ ignoreEncryption: true }重试一次大多数老文件能救回来。实在不行的做一个前置提示「该 PDF 格式异常请重新导出后再上传」比让用户卡在加载中体验好得多。5.4 签名导出的 PNG 背景是白色方块现象签名盖上去了但底下的文字被一块白色矩形遮住了。原因signature_pad 初始化时backgroundColor没有设为透明默认是白色。或者toDataURL时显式指定了image/jpeg而 JPEG 不支持透明通道。解决初始化时设置backgroundColor: rgba(0,0,0,0)导出时固定用toDataURL(image/png)png 才支持透明。如果你用了clear之外的清空方式检查一下是不是重绘导致了背景重置。5.5 多页 PDF 只盖了第一页其他页签不了现象用户上传一份 10 页的合同只能操作第一页。原因预览时只渲染了当前页盖章数据只绑定到了一个page对象上没有做「当前页码」的状态管理。解决维护一个currentPage变量切换页面时重新渲染 Canvas同时把该页的坐标数据和该页的PDFPage对象关联存储。导出时遍历所有页面把每页自己的印章和签名写在对应页上。注意多页场景下坐标换算公式不变但每个页面的pageWidth和pageHeight可能不同不能共用第一页的尺寸。6. 进阶技巧批量盖章、章迹防伪与下载文件的自检习惯如果你已经跑通了上面的基础链路可以再往前推一步批量盖章。很多业务场景不是一份文件盖一个章而是一批合同每份都要盖同一个章。做法也不复杂先让用户上传一份印章图片再触发一个循环任务逐份读取 PDF、嵌入章图、写入到固定坐标位比如文件的右下角最后打包下载。用JSZip把多个输出文件打包即可单份处理逻辑和上面完全一致只是套了一层循环。章迹防伪是一个容易被忽视但很实用的点。真实公章盖出来不是绝对规整的外圈带一点微小的旋转偏移。你可以给印章图片设一个rotate角度范围取 0.5~2 度之间的随机值每次生成的文件角度都略有不同肉眼几乎看不出来但增加了被复制的难度。pdf-lib 的drawImage支持rotate参数单位是弧度写起来就一行代码的事。时间戳落款也可以顺手做掉用pdf-lib的drawText在印章下方写入签署日期和操作人标识。drawText需要先embedFont通常用StandardFonts.Helvetica不需要加载外部字体文件。这一层加上之后下载的 PDF 才真正有「线上签署」的效力雏形。最后说一个我自己的验证习惯每次写完导出逻辑不要只看能不能下载而是下载后用 PDF 阅读器做三个检查。第一CtrlF 搜索原始 PDF 里的一行文字确认还在第二看文件大小增幅如果超过原始文件 1 MB 就要回头检查嵌入的图片尺寸第三把页面放大到 200%检查印章边缘是否发虚。这三个检查都通过了这套方案才算真正落地。做这个功能的过程中我最大的教训就是把「预览」和「写入」混成一套逻辑来做结果一到坐标换算就翻车。后来坚持预览归预览、写入归写入中间只靠一个比例坐标传递数据整个方案的复杂度一下子就降下来了。这个习惯后来被我带到了所有涉及坐标系转换的需求里屡试不爽。希望这篇笔记能帮你少走一段弯路让签章功能一次交付。希望帮到你。本文还有配套的精品资源点击获取