资讯动态

Vue前端用pdf-lib实现PDF盖章签名下载:坐标系避坑与完整指南

发布时间:2026/10/20 10:04:48 来源:尧图企业网站定制
简介这是一份基于Vue.js实现PDF上传、盖章与签名功能的完整项目源码包面向需要为合同、审批等文档流程引入在线签章能力的开发者适合具备一定Vue基础、希望快速落地的前端学习者。压缩包共19个文件含5个Vue组件、5个JavaScript脚本、3个JSON配置文件及2张效果预览图等整体仅216KB结构清晰便于直接导入工程运行。资源演示了借助pdfjs-dist加载与渲染PDF通过vue-pdf-signature交互添加签名和印章并配合axios完成文件上传、生成链接触发下载的完整链路覆盖从文件展示到导出的主要实现思路。已有1457人学习下载可作为理解相关插件用法、排查常见集成问题的参考样本。1. 给 PDF 盖章签名再下载Vue 前端能独立做完但坑在细节里后台管理系统里经常出现这样的需求用户上传一份 PDF 合同审核通过后需要在指定位置盖一个红章、签一个名字然后下载回去当正式文件。很多人第一反应是找后端帮忙但其实 Vue 前端有一套成熟的做法可以独立完成“读取 PDF → 盖章 → 签名 → 下载”这条完整链路。核心思路是用 pdf-lib 这类 PDF 操作库直接读写 PDF 字节流前端不只是能“显示”PDF还能生成合法、可被阅读器正常打开的 PDF 文件。整个流程听起来不复杂但坐标换算、透明通道、字体、内存边界每一个都会让生产环境翻车。这篇文章把这条链路完整拆开让你照着就能跑通也把最容易踩的坑提前标出来。2. 技术选型与坐标体系为什么是 pdf-lib以及 PDF 坐标系里 Y 轴朝上这件事2.1 pdf-lib、jspdf、canvas 渲染三选一取舍依据给已有 PDF 加内容业内常见的方案有三个jspdf、pdf-lib、pdf.js 配合 canvas 渲染。它们的定位差异很大选错方向后面全是返工。目标jspdfpdf-libpdf.js canvas从零生成新 PDF强中弱加载已有 PDF 并盖章弱需把原文件当底图重新插原生支持不支持写文件保留原 PDF 的文本和样式可能丢失完整保留保留渲染效果但不产出 PDF嵌入图片、字体支持支持不支持合并、拆分、多页操作有限完善不支持我一般这样选凡是“给已有 PDF 加内容、合并页面、拆分文件”这类编辑场景直接上 pdf-lib凡是“从数据库生成报表 PDF、完全由前端排版出文件”的场景才用 jspdf。pdf.js 在这条链路里只承担一个职责——渲染预览和校验不参与最终文件生成。pdf-lib 在浏览器环境里能直接加载 PDF 字节数组读取每一页然后在页面上绘制图片、文字、矩形最后保存为新字节流。它不会像 canvas 渲染方案那样把 PDF 变成位图再导出所以原文件里的文字仍然可选可复制文件体积也不会因为像素化而膨胀。2.2 PDF 页面坐标换算前端 Y 轴朝下PDF Y 轴朝上这是一个非常容易忽略的点。前端页面和 canvas 的坐标系原点在左上角Y 轴向下而 PDF 页面坐标系原点在左下角Y 轴向上单位是 point1/72 英寸。一张 A4 纸在 PDF 里的尺寸是 595.28 × 841.89 point。如果你把前端页面上拿到的鼠标坐标直接传给 pdf-lib 的 drawImage章的位置会完全错乱——看起来像是盖到了镜像位置。换算公式不复杂// frontY 是前端坐标系里的 Y 值pageHeight 是 PDF 页面高度 const pdfX frontX const pdfY pageHeight - frontY如果前端预览区对 PDF 做了缩放还需要先把前端坐标除以缩放比例还原成 PDF 原始坐标。比如预览容器宽度是 800pxPDF 渲染宽度也是 800px那 scale 就是 1如果容器是 400pxPDF 实际渲染比例是 0.5那么坐标也要除以 0.5 才能对上。2.3 盖章素材准备透明 PNG、尺寸与旋转规范电子章和签名图片的素材质量直接决定盖章效果。红章建议用透明背景的 PNG尺寸在 500 到 1000 像素之间形状居中对齐中央文字与边缘保留一定留白。如果是圆形章四周不要裁太紧否则旋转后边缘会被页面裁掉。签名则建议用白色背景画布手写导出导出格式必须是 PNG不能是 JPEG否则透明区域会变成黑色或白色块。素材准备好之后在代码里统一约定印章宽度按 PDF 页面宽度的 15% 到 20% 计算签名宽度按页面宽度的 30% 左右计算。这样在不同尺寸的 PDF 里盖出来比例协调而不是固定像素导致大页面显小、小页面溢出。3. 在 Vue 里跑通盖章签名下载从上传到落盘的最小实现3.1 Vue 组件读取 PDF从 file 对象到 Uint8ArrayVue 3 的组件里上传 PDF 最直接的方式是用 input 元素绑定 change 事件。拿到 file 对象后把它转成 Uint8Arraypdf-lib 才能识别。现代浏览器可以直接用 file.arrayBuffer()兼容性要求高再用 FileReader。// upload-pdf.vue script setup import { ref } from vue import { PDFDocument } from pdf-lib const pdfBytesRef ref(null) async function handleFileChange(event) { const file event.target.files[0] if (!file) return // 读取文件字节流arrayBuffer() 返回 ArrayBuffer转成 Uint8Array const pdfBytes new Uint8Array(await file.arrayBuffer()) pdfBytesRef.value pdfBytes }这里有两个容易忽略的点。第一file.arrayBuffer() 返回的是 Promise必须 await第二pdf-lib 的 load 方法只接受 Uint8Array 或 ArrayBuffer不要直接传 Blob。如果项目需要兼容旧版浏览器可以用 FileReader 的 readAsArrayBuffer 替代回调里同样转成 Uint8Array。加载 PDF 文档时建议把 ignoreEncryption 选项打开。很多业务系统导出的 PDF 带有空密码保护不打开这个选项会直接抛错。如果 PDF 确实有内容加密且需要密码pdf-lib 会要求传入 password这块要单独做交互处理。3.2 第一页盖第一个章pdf-lib 最小盖章代码这是整条链路的核心操作。先把印章图片嵌入 PDF 文档再从文档中取出目标页面最后调用 drawImage 把图片画上去。async function stampFirstPage(stampFile) { const pdfBytes pdfBytesRef.value if (!pdfBytes) return // 加载 PDF 文档ignoreEncryption 避免被空密码拦截 const pdfDoc await PDFDocument.load(pdfBytes, { ignoreEncryption: true }) // 嵌入印章图片必须是 PNG 字节流 const stampBytes new Uint8Array(await stampFile.arrayBuffer()) const stampImage await pdfDoc.embedPng(stampBytes) // 取第一页索引从 0 开始 const page pdfDoc.getPage(0) // 按页面宽度 18% 计算章的大小 const stampWidth page.getWidth() * 0.18 const stampHeight stampWidth // 圆形章宽高一致 // 盖在右上角X 靠右留 40pt 边距Y 靠上留 40pt 边距 page.drawImage(stampImage, { x: page.getWidth() - stampWidth - 40, y: page.getHeight() - stampHeight - 40, width: stampWidth, height: stampHeight, opacity: 0.95, }) // 保存为新的字节流 const savedBytes await pdfDoc.save() return savedBytes }drawImage 的参数里x 和 y 是图片左下角在 PDF 页面上的坐标不是中心点。width 和 height 控制实际显示尺寸单位是 point。opacity 控制透明度红章一般用 0.9 到 0.95太透明会发虚完全不透明会盖住正文太生硬。如果印章图片需要旋转加 rotate 参数单位是角度比如 rotate: 15 表示顺时针旋转 15 度。注意getPage(0) 是第一页不是第零页。pdf-lib 沿用了数组索引习惯第一页的 index 是 0。多页文档里如果要盖第三页用 getPage(2)。3.3 手写签名画布canvas 导出与 PDF 嵌入签名功能通常用一个 canvas 让用户手写然后导出 PNG 字节嵌入 PDF。这块的关键点是导出格式和数据转换。canvas refsignCanvas width400 height200 styleborder:1px solid #ccc;/canvas// 签名画布导出 function exportSignature() { const canvas signCanvas.value // 导出 PNG 格式 dataURL黑色笔迹保留透明背景 const dataUrl canvas.toDataURL(image/png) const pngBytes dataUrlToUint8Array(dataUrl) return pngBytes } // dataURL 转 Uint8Array function dataUrlToUint8Array(dataUrl) { const base64 dataUrl.split(,)[1] const binary atob(base64) const bytes new Uint8Array(binary.length) for (let i 0; i binary.length; i) { bytes[i] binary.charCodeAt(i) } return bytes }签名图片嵌入 PDF 的方式与盖章完全一样用 pdfDoc.embedPng 把 pngBytes 嵌入。签名不像印章要求圆形宽高可以根据笔迹实际内容调整或者按宽 30%、高 15% 的比例设定。建议给画布加鼠标事件监听并在 mousedown/mousemove 中实现画笔轨迹避免手写时出现线条断裂。3.4 多页盖章与多次盖章按页码数组循环铺章实际业务里很少只盖一页。合同可能每页右下角都要盖骑缝章或者只在指定几页盖公章。常见做法是把要盖章的页码放进数组循环处理。async function stampMultiPages(stampFile, pageNumbers) { const pdfDoc await PDFDocument.load(pdfBytesRef.value, { ignoreEncryption: true }) const stampImage await pdfDoc.embedPng(new Uint8Array(await stampFile.arrayBuffer())) const stampWidth pdfDoc.getPage(0).getWidth() * 0.18 const margin 40 // pageNumbers 是页码数组比如 [1, 3, 5]内部转换为索引 for (const pageNum of pageNumbers) { const page pdfDoc.getPage(pageNum - 1) const x page.getWidth() - stampWidth - margin const y margin // 右下角 page.drawImage(stampImage, { x, y, width: stampWidth, height: stampWidth, opacity: 0.9, }) } return pdfDoc.save() }这里把章盖在每一页的右下角x 靠右、y 靠底。多个章叠加时只需对同一页面多次调用 drawImage后画的会覆盖先画的可以用 opacity 做出骑缝章的层次感。要注意的是页面尺寸不一定相同比如 PDF 里同时存在 A4 和 A3 页面此时不能在循环外写死 x 和 y必须在循环内用每页的 getWidth() 和 getHeight() 重新计算。3.5 保存与下载PDF 字节转 Blob中文文件名保留保存下载是整个流程的最后一跳。pdf-lib 的 save() 返回 Uint8Array需要包装成 Blob再通过 URL.createObjectURL 生成下载链接。中文文件名在这个环节经常出问题。async function downloadPdf(pdfBytes, originalName) { const blob new Blob([pdfBytes], { type: application/pdf }) const url URL.createObjectURL(blob) const link document.createElement(a) link.href url // 保留原文件名追加“盖章”后缀注意 .pdf 后缀不能重复 const baseName originalName.replace(/\.pdf$/i, ) link.download ${baseName}_盖章.pdf link.click() // 及时释放对象 URL避免内存泄漏 URL.revokeObjectURL(url) }Blob 类型必须写 application/pdf否则部分 Windows 机器下载后后缀异常。a.download 属性对中文文件名支持良好但如果文件名里带斜杠、冒号等非法字符保存时会失败需要在拼接前过滤。文件较大时建议下载完成后调用 revokeObjectURL 释放内存否则连续操作多个大文件页面会越来越卡。4. 盖章下载的翻车点与排查清单坐标、白底、字体、内存、模糊4.1 章盖到了坐标系“镜像位”现象、定位与换算修复现象预览层里章的位置完全正确但下载后的 PDF 里章跑到页面底部或者上下颠倒的位置。原因很直接预览层用的是前端坐标系原点在左上角Y 向下增长PDF 坐标系原点在左下角Y 向上增长。直接把前端 Y 坐标塞进 drawImage 的 y 参数等于把章放到了对称位置。解决所有传进 drawImage 的 y 都必须经过翻转换算。function toPdfPoint(frontX, frontY, pageHeight, scale) { return { x: frontX / scale, y: pageHeight - frontY / scale, } }scale 是前端预览的缩放比例。如果预览区域做了缩放必须先除以 scale 再翻转。我在项目里习惯封装一个 getStampPosition 函数统一处理缩放、翻转和页面边距业务代码里不再出现裸坐标避免每个开发者各写一套换算逻辑。4.2 印章周围出现白块源图有白底现象红章盖上去之后图片周围多了一个白色矩形块正文被挡住一块。原因印章源图不是透明 PNG而是带白底的 JPG 或者导出时没去掉背景。pdf-lib 的 embedPng 只能识别 PNG 的 alpha 通道白底会原样保留。解决如果手头只有白底图片前端可以做一个简单的去底处理。用 canvas 读取像素把接近白色的像素 alpha 置 0。function removeWhiteBackground(canvas, threshold 240) { const ctx canvas.getContext(2d) const { width, height } canvas const imageData ctx.getImageData(0, 0, width, height) const data imageData.data for (let i 0; i data.length; i 4) { const r data[i] const g data[i 1] const b data[i 2] // RGB 都大于阈值视为白底 if (r threshold g threshold b threshold) { data[i 3] 0 } } ctx.putImageData(imageData, 0, 0) return canvas.toDataURL(image/png) }要注意阈值别设太低否则红章边缘会被削掉一层也别太高否则阴影残留发灰。这个方案只适合纯色白底如果印章里有白色文字或细线会被一起变透明这种素材只能重新制作透明 PNG。4.3 中文文件名乱码与标准字体不支持中文现象下载下来的文件名变成一串随机字符或者用 setFont 添加文字时中文全部变成乱码。文件名乱码的原因通常是直接用了 Blob URL 的默认名称或者 a.download 里没有编码处理中文乱码的深层原因是 pdf-lib 内置标准字体只有 Helvetica、Times 等西方字体不支持中文字符。解决文件名问题按 3.5 的方式用 a.download 显式指定即可注意把文件名过滤一遍非法字符。字体问题要正视如果盖章时需要添加“审核通过”“2025-06-01”这类中文文字不能依赖标准字体常见做法有两种。第一种是在 canvas 上把文字画成透明 PNG再嵌入 PDF简单可靠不引入字体文件第二种是用 fontkit 嵌入中文字体文件效果更专业但字体文件体积大加载慢还要处理 fontkit 的初始化。我的习惯是优先用 canvas 画字转 PNG除非业务要求 PDF 内文字可选中可复制才走字体嵌入路线。4.4 大 PDF 加载占用过多内存上传与图片压缩边界现象用户在系统里上传了一个几百 MB 的 PDF点击盖章后浏览器直接白屏或崩溃。原因很明确pdf-lib 的 load 会把整个 PDF 文件读进内存浏览器 tab 的内存上限有限大文件加上图片嵌入内存瞬间爆掉。解决第一道防线是上传限制设置 30MB 或 50MB 的文件大小校验超出直接提示。第二道防线是图片压缩印章和签名图片在嵌入前先用 canvas 缩放到合理尺寸比如 200px 宽就够用不要直接嵌入 4000px 的扫描件。第三道防线是加载期间的 UI 反馈写入 loading 状态避免用户重复点击触发多个并发任务。如果业务确实需要处理超大 PDF建议拆页处理或者交后端转换前端不适合硬扛。4.5 签名导出文字发虚画布分辨率与格式选择现象手写签名嵌入 PDF 后笔迹边缘发虚放大后有明显锯齿。原因canvas 的 CSS 尺寸和实际像素尺寸不一致默认情况下画布只有 400×200 的物理像素在 PDF 里拉伸后被插值模糊。解决把 canvas 的物理尺寸按设备像素比放大再通过 CSS 控制显示大小。代码里固定设置const canvas signCanvas.value const dpr window.devicePixelRatio || 1 canvas.width cssWidth * dpr canvas.height cssHeight * dpr const ctx canvas.getContext(2d) ctx.scale(dpr, dpr)这样绘制时的坐标还是按 CSS 像素写但物理像素密度提升了一倍导出 PNG 的清晰度明显改善。导出格式一定用 image/pngJPEG 会把透明背景变成黑色而且有损压缩会让笔迹边缘出现色斑。5. 让盖章结果可核对、可存证文件的二次校验与元数据写入5.1 下载前先校验从字节到页面的闭环检查生产环境里经常遇到“下载后打不开”或者“页数变少”的问题。我习惯在保存下载前加一道校验逻辑把生成好的字节流重新 load 一遍先确认文件结构没被破坏。async function verifyPdf(generatedBytes, originalPageCount) { const doc await PDFDocument.load(generatedBytes, { ignoreEncryption: true }) // 页数是否一致 if (doc.getPageCount() ! originalPageCount) { throw new Error(页数不一致原文件 ${originalPageCount}生成文件 ${doc.getPageCount()}) } // 文件体积是否异常小于 100 字节基本说明生成失败 if (generatedBytes.byteLength 1024) { throw new Error(生成文件体积异常请检查源文件) } return true }这只验证了文件层面的完整性。位置是否正确还需要把生成文件渲染成 canvas 目视检查。可以用 pdf.js 把每一页渲染成图片和前端预览层对比盖章区域。这个步骤不用全自动能通过布尔值判断页数和体积是否异常加上人工抽查位置就已经能拦截绝大多数问题。检查项方法页数无增减PDFDocument.load 后 getPageCount 对比原文件文件能正常解析load 不抛错且 byteLength 不为 0盖章位置准确pdf.js 渲染页面到 canvas与预览层人工比对文件体积异常生成文件与原文件体积对比超出合理范围则排查5.2 元数据写入与业务边界不能忽视的一件事pdf-lib 支持写入 PDF 元数据这在一份要归档的盖章合同里很有用。在保存前把标题、作者、创建时间写进文档方便后续检索。pdfDoc.setTitle(合同-${originalFileBaseName}) pdfDoc.setAuthor(业务系统签章模块) pdfDoc.setCreationDate(new Date()) pdfDoc.setModificationDate(new Date())通过这种方式生成的文件自带操作时间和业务来源标识。但要诚实面对一个边界前端盖章是“视觉上的套印”做出来是一张带有印章图像的普通 PDF不等于法律意义上的电子签章。真正的电子签章需要对接 CA 体系、可信时间戳和防篡改机制那是另一套系统不是 pdf-lib 能解决的。做业务开发时我会把这个边界提前和需求方说清楚避免上线后产生合同效力纠纷。我自己常年的一个习惯是“先出预览再写文件”。在模拟项目 X 里第一次做盖章功能时直接按鼠标坐标传给 pdf-lib预览看着是对的下载下来章全跑到了左侧边缘就是因为没按 PDF 坐标系翻转。后来我强制自己每次改动都先跑一遍校验函数确认页数和文件结构没问题再点击下载。前端做 PDF 编辑不复杂但每一步都要能验证希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑