背景 链接到标题
一本 A4 尺寸的扫描版 PDF,A4 尺寸的扫描版 PDF。每页底部固定出现水印图样,并包含一个可点击的矩形区域,点击后跳转到外部站点。原始 PDF 使用 Adobe Acrobat Pro DC 17 Paper Capture 生成,具备完整的 OCR 文字层。
目标:彻底移除水印,保留清晰的纯阅读版 PDF。
诊断:水印的四层结构 链接到标题
解剖 PDF 内部结构后发现,水印并非单一图层,而是同时嵌入了四个层面:
多页"] --> L1["Layer 1: Resources/XObject
图片对象引用 /Im1"] P --> L2["Layer 2: 内容流
渲染命令 /Im1 Do"] P --> L3["Layer 3: 文本层
OCR 可选的水印文字"] P --> L4["Layer 4: /Annots
Link 注释 /URI 外部链接"] L1 --> R1["绝大多数页含此引用"] L2 --> R2["绝大多数页含此命令"] L3 --> R3["约 绝大多数页含此文本"] L4 --> R4["绝大多数页含此注释"] style L1 fill:#e3f2fd style L2 fill:#e3f2fd style L3 fill:#e3f2fd style L4 fill:#e3f2fd
- Layer 1: 每页的
Resources字典引用了名为/Im1的 XObject 图片(xreference → 文件尾部存储的 PNG 图像) - Layer 2: 页面内容流中插入了
q cm /Im1 Do Q指令序列,负责将图片渲染到页面上 - Layer 3: OCR 引擎将水印图片上的文字识别为可选文本,嵌入到内容流中
- Layer 4: 页面的
/Annots数组中定义了一个 Link 注释,框选范围覆盖水印区域,URI 指向外部站点
只有四层全部清除,才算真正移除水印。
走过的弯路 链接到标题
弯路 1:单一工具只能处理单一层面 链接到标题
首先尝试了 pdf-redactor 的文本替换功能,它能够高效处理 Layer 3(文本层),但对 Layer 1、2、4 完全无效。效果是:肉眼仍能看见水印图片,点击仍能跳转,只是在文字搜索时不再出现水印文字。
弯路 2:视觉覆盖留下矩形边框 链接到标题
尝试 page.draw_rect() 用白色矩形覆盖水印区域。问题有二:(1)部分页面背景不是纯白色,白块显得突兀;(2)部分版本留下细不可见的矩形边框,像素检测仍能检出。
弯路 3:delete_image API 行为不一致 链接到标题
PyMuPDF 的 page.delete_images() 在某些页面未能生效,且没有一致的错误提示,排查困难。
弯路 4:正则适配不够全面 链接到标题
绝大多数页中有 绝大多数的 /Im1 Do 命令格式一致,但有 2 页多了一行 /GS1 gs 命令,导致初始写死的单行正则漏匹配。此外,Layer 4 的 /Annots 有内联数组 [11 0 R] 和间接引用 22 0 R 两种表示法,需分别处理。
最终方案:逐层定位与清理 链接到标题
从最内层的引用关系开始,由底层向表层逐步处理。
处理顺序 链接到标题
Layer 1 (Resources) + Layer 2 (内容流)
↓
Layer 3 (文本层)
↓
Layer 4 (注释)
↓
garbage 回收
Layer 1 + 2:移除图片引用与渲染命令 链接到标题
通过解析原始 PDF 对象,用正则表达式匹配并删除内容流中的 /Im1 Do 渲染序列,同时从页对象的 Resources 字典中删除 /Im1 的引用。
关键操作为两处正则替换:
- 内容流:匹配
q … /Im1 Do Q(兼容 2~3 行长度差异) - Resources:匹配
/Im1 \d+ 0 R并移除
Layer 3:清理 OCR 文本层 链接到标题
使用 pdf-redactor 的正则内容过滤器扫描每页内容流,将水印文字替换为空字符串。此步骤仅影响文本搜索与复制,不影响已有图片。
Layer 4:移除 Link 注释 链接到标题
解析每页的 /Annots 对象,删除类型为 /Link 且包含 /URI 的注释条目。同时兼容两种语法格式:
- 内联数组:
/Annots [11 0 R] - 间接引用:
/Annots 22 0 R
收尾:垃圾回收 链接到标题
全部清理后在 save() 中启用 garbage=4,让 PyMuPDF 自动回收所有不再被引用的 xref 对象,最终文件不带冗余数据。
验证方法 链接到标题
采用四种互补手段确认清理结果:
- 像素采样:随机抽取 10 页,读取水印区域的 RGB 像素值,全部为
(255,255,255),且无任何杂色边框 - xref 扫描:调用
page.get_images()确认/Im1图片引用已从所有页面消失 - 文本搜索:用
pdftotext+grep确认水印文字在整个文档中不再出现 - 注释枚举:遍历
page.annots()确认无 Link 注释残留
总结 链接到标题
本问题的核心难点不在于某一层的移除,而在于全面认识水印的散布范围。四个层面中任何一个遗漏,都会被视为"没清干净"。
| 层面 | 清除手段 | 覆盖页数 |
|---|---|---|
| Resources/XObject | 正则移除对象引用 | 全部 |
| 内容流渲染命令 | 正则删除 /Im1 Do |
全部 |
| OCR 文本层 | pdf-redactor 正则替换 | 全部 |
| Link 注释 | 正则删除 /Annots 条目 |
全部 |
关键教训:
- 先诊断、后治疗:解剖 PDF 内部结构,搞清楚"敌人"长什么样、分布在哪,比反复试错更高效
- 格式差异是最大的坑:PDF 作为一种表现层格式,同样的信息可以有完全不同的语法表达,需覆盖主要变体
- 验证必须多维度:肉眼看不到 ≠ 已清除,像素、对象、文本、注释各维度独立验证