为 PDF 添加水印——在页面上叠加半透明文字——听起来像是只有桌面软件才能完成的事情。但借助 pdf-lib 和一些画布计算,你可以打造一个完全基于浏览器的水印工具。
本文将逐步讲解实现细节,包括文字渲染、旋转和多页支持。
为什么选择客户端?
传统水印工具需要上传文件、在服务器端处理,然后返回结果。对于可能包含机密或敏感信息的文档,这会引入不必要的隐私风险。基于浏览器的方法:
- 全部在本地处理
- 文件始终保留在用户设备上
- 加载后即可离线使用
- 避免服务器端带宽开销
技术栈
- Vue 3 + Composition API
- pdf-lib 用于 PDF 操作和添加水印
-
PDF.js (
pdfjs-dist) 用于预览渲染 - Vite 用于打包
添加文字水印
pdf-lib 提供了内置的 PDFDocument.embedFont() 方法用于嵌入自定义字体,以及 page.drawText() 方法用于放置文字。下面介绍如何在所有页面上添加旋转、半透明的水印:
<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'
const file = ref<File | null>(null)
const watermarkText = ref('DRAFT')
const opacity = ref(0.3)
const fontSize = ref(72)
const rotationDeg = ref(-45)
const applying = ref(false)
async function handleFileUpload(selected: File) {
file.value = selected
}
async function applyWatermark() {
if (!file.value) return
applying.value = true
try {
const arrayBuffer = await file.value.arrayBuffer()
const pdfDoc = await PDFDocument.load(arrayBuffer)
// Embed the Helvetica font — required for correct rendering
const helveticaFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
const pages = pdfDoc.getPages()
pages.forEach((page) => {
const { width, height } = page.getSize()
page.drawText(watermarkText.value, {
x: (width - helveticaFont.widthOfTextAtSize(watermarkText.value, fontSize.value)) / 2,
y: height / 2,
size: fontSize.value,
font: helveticaFont,
color: rgb(128, 128, 128), // mid-gray
rotate: {
type: 'rad',
angle: (rotationDeg.value * Math.PI) / 180,
},
})
})
const watermarkedBytes = await pdfDoc.save()
const blob = new Blob([watermarkedBytes], { type: 'application/pdf' })
downloadBlob(blob, `watermarked_${file.value.name}`)
} finally {
applying.value = false
}
}
function downloadBlob(blob: Blob, name: string) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = name
a.click()
URL.revokeObjectURL(url)
}
</script>
Enter fullscreen mode Exit fullscreen mode
旋转的挑战
棘手的部分在于让文字绕自身中心旋转。pdf-lib 是相对于原点 (x, y)(字符的左下角,而非字符串中心)来旋转文字的。这意味着以 -45° 旋转时,看起来不会居中。
要解决这个问题,我先计算文字的包围盒,然后偏移位置,使视觉中心与页面中心对齐:
function getCenteredPosition(page: Page, text: string, fontSize: number, font: PDFFont) {
const { width, height } = page.getSize()
const textWidth = font.widthOfTextAtSize(text, fontSize)
const textHeight = fontSize
// Account for rotation — the bounding box changes when text is rotated
const radians = (-45 * Math.PI) / 180
const cos = Math.abs(Math.cos(radians))
const sin = Math.abs(Math.sin(radians))
// Rotated bounding box dimensions
const rotatedWidth = textWidth * cos + textHeight * sin
const rotatedHeight = textWidth * sin + textHeight * cos
return {
x: (width - rotatedWidth) / 2,
y: (height - rotatedHeight) / 2,
}
}
Enter fullscreen mode Exit fullscreen mode
让功能可配置
对于生产工具,仅靠一刀切的做法是不够的:
| 设置 | 范围 | 默认值 | 用途 |
|---|---|---|---|
| 文字 | 任意 Unicode | "DRAFT" | 显示内容 |
| 字号 | 12–200pt | 72 | 可见度级别 |
| 不透明度 | 0.05–0.8 | 0.3 | 透明度控制 |
| 旋转角度 | 0–360° | -45° | 角度偏好 |
| 颜色 | Hex/RGB | 灰色 | 品牌匹配 |
| 位置 | 居中/自定义 | 居中 | 放置位置 |
| 页面 | 全部/范围/选中 | 全部 | 作用范围控制 |
处理大型 PDF
对于 100 页以上的 PDF,添加水印可能会耗时明显。关键优化措施:
- 批量处理:将页面操作集中在一起,而不是每处理一页就保存一次
- 渐进渲染:在处理全部页面前,仅预览前几页
- 取消选项:允许用户在中途停止操作
更进一步
对于简单的加水印需求,pdf-lib 内置的 drawText() 已足够。如果你需要基于图像的水印(带透明度的 PNG 标志),则需要将图片嵌入为表单并将其绘制为叠加层。这是一个值得在另一篇文章中探讨的独立主题。
想要查看完整源码?github.com/sunshey/pdf-tool。
如果你需要高级水印功能——批量操作、标志叠加或计划处理——请查看 Wondershare PDFelement。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.