sunshey

为 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,添加水印可能会耗时明显。关键优化措施:

  1. 批量处理:将页面操作集中在一起,而不是每处理一页就保存一次
  2. 渐进渲染:在处理全部页面前,仅预览前几页
  3. 取消选项:允许用户在中途停止操作

更进一步

对于简单的加水印需求,pdf-lib 内置的 drawText() 已足够。如果你需要基于图像的水印(带透明度的 PNG 标志),则需要将图片嵌入为表单并将其绘制为叠加层。这是一个值得在另一篇文章中探讨的独立主题。

想要查看完整源码?github.com/sunshey/pdf-tool


如果你需要高级水印功能——批量操作、标志叠加或计划处理——请查看 Wondershare PDFelement