1. 为什么需要PDF水印功能在Web应用中处理PDF文件时经常需要给文档添加水印来标识文件来源、保护版权或标记敏感信息。比如合同管理系统需要自动添加内部保密水印在线教育平台要给课件打上学员ID防止传播企业OA系统要在公文上加盖电子印章。传统方案是后端生成水印但这样每次修改都要重新请求服务器。前端实现的优势在于实时预览水印样式调整后立即可见减轻服务器压力避免重复生成相同文件保护隐私敏感文件无需上传到服务器交互灵活支持动态调整文字、透明度、旋转角度等参数我在最近一个Vue项目中就遇到这样的需求用户上传PDF后需要动态添加包含用户名和日期的时间戳水印并能即时预览效果。经过技术选型最终采用pdf-libCanvas的方案实测下来既保持了PDF的矢量特性又实现了灵活的水印配置。2. 环境准备与核心依赖2.1 安装必要库首先创建Vue项目假设已安装Vue CLI然后安装三个核心依赖npm install vue-pdf pdf-lib pdf-lib/fontkit --save各库的作用vue-pdfPDF预览组件基于PDF.js封装pdf-lib操作PDF的核心库支持修改现有PDFfontkitpdf-lib的字体插件解决中文乱码问题2.2 字体文件准备由于pdf-lib内置字体不支持中文需要准备中文字体文件如.ttf格式。推荐使用思源黑体等开源字体下载SourceHanSansCN-Normal.ttf放在public/fonts目录下通过相对路径/fonts/SourceHanSansCN-Normal.ttf引用注意测试阶段可能会遇到跨域问题建议将字体文件放在项目静态资源目录而非外部CDN3. 实现PDF水印的核心逻辑3.1 Canvas生成水印图片先用Canvas创建半透明水印图案这是最灵活的方式createWaterMark({ canvas: document.createElement(canvas), fontText: 测试水印, fontSize: 24, fontFamily: Microsoft YaHei, fontcolor: rgba(100,100,100,0.3), rotate: 30 }) { const ctx canvas.getContext(2d) canvas.width 300 canvas.height 200 // 设置绘制样式 ctx.font ${fontSize}px ${fontFamily} ctx.fillStyle fontcolor ctx.textAlign center // 旋转画布 ctx.translate(canvas.width/2, canvas.height/2) ctx.rotate(-rotate * Math.PI / 180) // 绘制文字 ctx.fillText(fontText, 0, 0) return canvas.toDataURL(image/png) }关键参数说明rotate推荐15-45度倾斜更美观fontcolor建议使用rgba设置透明度textAlign控制文字基准位置3.2 给PDF添加水印通过pdf-lib操作PDF字节流async function addWatermarkToPdf(pdfBytes, watermarkText) { // 加载PDF文档 const pdfDoc await PDFDocument.load(pdfBytes) // 注册字体插件 pdfDoc.registerFontkit(fontkit) // 加载中文字体 const fontBytes await fetch(/fonts/SourceHanSansCN-Normal.ttf) .then(res res.arrayBuffer()) const customFont await pdfDoc.embedFont(fontBytes) // 获取所有页面 const pages pdfDoc.getPages() // 为每页添加水印 pages.forEach(page { const { width, height } page.getSize() // 平铺水印 for(let x 0; x width; x 200) { for(let y 0; y height; y 150) { page.drawText(watermarkText, { x, y, size: 24, font: customFont, color: rgb(0.5, 0.5, 0.5), rotate: degrees(30), opacity: 0.3 }) } } }) // 返回修改后的PDF return await pdfDoc.save() }踩坑提醒中文必须使用自定义字体坐标原点在页面左下角水印密度根据内容调整4. 完整实现方案4.1 组件化封装建议将功能封装为可复用的Vue组件template div classpdf-container pdf-viewer :srcpdfUrl loadedonPdfLoaded/ watermark-config v-modelwatermark changeupdateWatermark / button clickdownload下载带水印PDF/button /div /template script export default { data() { return { pdfUrl: /sample.pdf, watermark: { text: 机密文档, size: 24, opacity: 0.3, angle: 30, color: #999999 } } }, methods: { async onPdfLoaded(pdfDoc) { this.pdfDoc pdfDoc this.updatePreview() }, async updatePreview() { const watermarkedPdf await addWatermark( this.pdfDoc, this.watermark ) this.previewUrl URL.createObjectURL( new Blob([watermarkedPdf]) ) }, async download() { const bytes await this.addWatermarkToPdf() saveAs(bytes, watermarked.pdf) } } } /script4.2 性能优化技巧处理大文件时需要注意分页加载使用vue-pdf的page属性逐页渲染Worker线程将PDF处理放到Web Worker中缓存机制存储已处理的水印PDF防抖处理水印参数变化时延迟500ms再更新// Worker示例 const worker new Worker(./pdf.worker.js) worker.postMessage({ type: ADD_WATERMARK, pdfBytes, watermark }) worker.onmessage (e) { const watermarkedPdf e.data // 更新UI... }5. 安全下载方案5.1 前端生成下载链接使用Blob对象创建临时URLfunction saveAs(byte, filename) { const blob new Blob([byte], {type: application/pdf}) const link document.createElement(a) link.href URL.createObjectURL(blob) link.download filename document.body.appendChild(link) link.click() setTimeout(() { URL.revokeObjectURL(link.href) link.remove() }, 100) }5.2 防止未授权下载增加权限控制逻辑检查用户权限添加时效性token记录下载日志async function secureDownload() { if(!this.checkPermission()) { alert(无下载权限) return } const token await getDownloadToken() const bytes await fetchPdfWithToken(token) // 记录下载行为 logDownloadAction() saveAs(bytes, secure-file.pdf) }6. 实际应用案例在最近开发的合同管理系统中我们实现了以下高级功能动态水印将当前用户名和日期作为水印多类型水印支持文字/图片/二维码水印批量处理同时给多个PDF添加水印模板保存存储常用水印样式核心代码片段// 动态生成水印内容 function generateDynamicText() { const user store.state.user return ${user.name} ${dayjs().format(YYYY-MM-DD)} } // 图片水印处理 async function addImageWatermark(pdfDoc, imageFile) { const imageBytes await readFileAsArrayBuffer(imageFile) const image await pdfDoc.embedPng(imageBytes) const pages pdfDoc.getPages() pages.forEach(page { page.drawImage(image, { x: 50, y: 50, width: 100, height: 50, opacity: 0.5 }) }) }遇到的主要挑战是PDF.js和pdf-lib的坐标系差异需要通过计算进行转换。最终效果得到客户高度认可水印无法通过常规手段去除有效防止了合同文件的外泄。