第4章:文档水印实现——PDF/Word加水印

讲完动态加壳和静态策略后,咱们终于要碰最实际的问题了——怎么往PDF和Word文件里加水印。说实话,这活儿看着简单,做起来坑不少。我早年帮客户做一个外发系统,就因为在Word加水印时没处理好分节符,结果对方拿去打印出来,水印全跑偏了,被骂了个半死。

文档水印这块,核心就两个流派:内嵌式 和 覆层式。搞清楚了它们的区别,你才知道不同场景该选哪个。

4.1 两大技术路线对比

特性 内嵌式(Embedded) 覆层式(Overlay)
原理 水印直接写入文档内容层,作为文字/图片对象 水印绘制在独立的注释层或页面之上,不改变原文
典型实现 Python PyMuPDF 插入文字 PHP FPDI 叠加 PDF 层
优势 兼容性极好,打印/转格式水印不掉 不破坏原文档结构,适合签章场景
劣势 改变文件内部结构,移动或删除水印较难 依赖阅读器渲染,部分浏览器预览会丢失
适用场景 外发防截屏、审计留痕 临时审阅、在线预览

建议:如果你的水印必须“打死也不能丢”,比如司法机关的电子证据外发,果断用内嵌式。如果只是给客户看个预览,覆层式更优雅,不会把源文件搞乱。

4.2 PDF 水印实操:以 PyMuPDF 为例

我个人最常用的库是 fitz(也就是 PyMuPDF)。它不但速度快,而且对中文支持还算友好。但有个坑你得注意——字体路径。

我来拆解几个核心步骤:

  • 第一步:加载文档和页面。记得用上下文管理器,否则容易文件句柄泄漏。
  • 第二步:计算水印位置。千万别死铺,留出阅读区域。一般设在页面中心旋转45°。
  • 第三步:插入文字对象。注意设置透明度,不透明度 0.2~0.3 比较舒适。
  • 第四步:保存输出。增量保存或全新保存,看你是否需要保留原注释。
import fitz

def add_pdf_watermark(input_pdf, output_pdf, watermark_text):
    """
    给 PDF 的每一页添加斜向水印
    """
    doc = fitz.open(input_pdf)
    for page_num in range(len(doc)):
        page = doc[page_num]
        # 获取页面尺寸,计算旋转后的坐标
        rect = page.rect
        center_x = rect.width / 2
        center_y = rect.height / 2
        
        # 插入水印文字
        page.insert_text(
            point=(center_x - 100, center_y),  # 自定偏移
            text=watermark_text,
            fontname="china-s",   # 支持中文的字体别名
            fontsize=48,
            rotate=45,
            color=(0.8, 0.8, 0.8),
            opacity=0.3
        )
    doc.save(output_pdf, incremental=False)
    doc.close()

# 调用
add_pdf_watermark("report.pdf", "report_watermarked.pdf", "内部资料 严禁转发")

重要:上面的 fontname="china-s" 在部分 Linux 环境会报错。我曾经在 Debian 上踩过这个坑——因为系统缺中文字体。解决办法:page.insert_font(fontname="china-s", fontfile="/usr/share/fonts/SourceHanSansCN-Regular.otf"),提前注册一个真实的字体文件。

4.3 Word 水印实操:python-docx 与底层 XML

Word 的水印实现路径和 PDF 完全不一样。Word 文档本质是一个 ZIP 包,里面装了 N 个 XML 文件。水印存在于 header 层的 watermark 标签里。

python-docx 库暴露的 API 算比较友好,但有个致命缺陷——它默认只能加 文字型水印,而且是居中的那种“禁止复制”正文水印。如果你想放一个带公司 logo 的图片水印,或者控制水印在每一节显示,就得动底层 lxml 了。

我整理了一个稳妥的代码模板:

from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from lxml import etree

nsmap = {
    'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
    'pic': 'http://schemas.openxmlformats.org/drawingml/2006/picture',
    'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
    'wp': 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing'
}

def add_text_watermark(doc, text="内部公开"):
    """
    为 document 插入水印(作用于所有节)
    """
    for section in doc.sections:
        header = section.header
        # 如果该节已有水印段落,先清理
        for p in header.paragraphs:
            p.clear()
        # 添加新的水印段落
        p = header.add_paragraph()
        p.alignment = WD_ALIGN_PARAGRAPH.CENTER
        run = p.add_run()
        run.text = text
        run.font.size = 960000   # 48pt
        run.font.color.rgb = RGBColor(0xBF, 0xBF, 0xBF)  # 灰色
        run.font.name = "微软雅黑"
        # 重要:设为半透明需要一个样式覆盖
        rPr = run._element.get_or_add_rPr()
        shading = etree.SubElement(rPr, '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}shd')
        shading.set('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}fill', '000000')
        shading.set('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val', 'clear')
        
doc = Document("source.docx")
add_text_watermark(doc, "机密")
doc.save("watermarked.docx")

小技巧:如果你发现 Word 打开时水印不显示,八成是 页面背景 和 水印 重叠了。在 settings.xml 里设置 <w:displayBackgroundShape w:val="0"/> 即可。

4.4 飞书文档、WPS 的差异点

在实际项目中,我发现很多客户并不用纯 Word/PDF,而是混用飞书、WPS 这类国产办公套件。这些平台对 OOXML 标准的支持略有不同:

  • WPS:对 v:shape 标签的水印支持得很好,但解析 w:watermark 时可能出现字体回退。
  • 飞书文档:完全不走 OOXML 路线,只能用 API 接口加浮窗水印,而且是覆层式的。
  • Google Docs:导出的 PDF 不支持内嵌水印,只能靠服务端渲染后叠加。

说白了,如果你的下游有强管控需求,我建议统一把 Word 转成 PDF 后再加水印,这样最可控。转格式的环节可以用 libreoffice --headless --convert-to pdf 命令行,稳定且无版权问题。

4.5 全量构建一张知识图

好了,上面讲了这么多理论和代码,我们梳理一张 SVG 图,帮你把 文档水印的技术选型与实现路径 串起来。这张图就是你碰到实际需求时,直接拿来对照的决策地图。

文档水印实现技术体系 文档水印引擎 内嵌式 (Embedded) 覆层式 (Overlay) PyMuPDF 文字插入 python-docx Header 底层 XML 注入 PDF 注释层叠加 Web 渲染浮窗 WPS/飞书 API 典型应用场景 防截屏外发 审计合规留痕 在线预览签名

核心结论:内嵌式走“写死”路线,适合最终分发;覆层式走“展示”路线,适合过程审阅。从这张图你可以看到,我特意把 防截屏外发 挂在了内嵌式下面——如果你需要确保水印永远在,那就别偷懒,老老实实嵌进去。

4.6 避坑与实战建议

最后,分享一下我在几个真实项目里摔过的跤:

  • 字体缺失导致水印变方框:这是最常见的。我曾经帮客户处理一批日文 PDF,结果用 SourceHanSansJP 就能显示,用 MS Mincho 就出乱码。方案:容器里统一装 Noto 字体系列,代码里 fallback 到 NotoSansCJK。
  • 图片水印导致文件体积暴增:有次同事拿一张 200KB 的 logo 去铺水印,结果一个 10 页的 PDF 膨胀到 30MB。我们后来改用矢量 SVG 转 PDF 图元,体积控制在 2.5MB 以内。
  • 动态水印刷新时机:如果你用覆层式,水印是在加载时生成的。有客户反馈“我打开文件时没联网,水印没出来”——这是个经典的产品缺陷。我们的补救:让服务端预先渲染好带水印的 PDF,用户下载的是成品。

小建议:做文档水印时,不妨做一次 转格式测试:Word→PDF→图片→打印机。每个环节都检查水印是否还在。很多看起来完美的方案,一到打印环节就露馅了。