简介:pdf-unstamper 是一款基于 PDFBox 的 Java 命令行工具,可删除 PDF 中任意字体、任意编码和任意语言的文本水印。压缩包为该开源项目完整源码,面向需要批量处理 PDF 水印的开发者、运维及办公用户,也适合想要学习 PDF 页面内容流解析的 Java 工程师。包内共含 24 个文件,整体大小仅 115KB,核心为 8 个 Java 源文件,并搭配 Maven 的 pom.xml 配置、2 份 Markdown 说明、8 张水印处理前后对比截图,以及 LICENSE、install 安装脚本和 version 版本文件,目录结构干净,能够快速定位源码、构建产物与演示素材。命令行参数设计简洁,支持 -i/-o 单文件输入输出、-I/-O 目录批量处理,还可用 -d 直接覆盖原文件,便于嵌入自动化流程。当前已有 182 人浏览学习。借助完整源码与图文说明,读者既可以掌握 PDFBox 对内容流进行遍历、识别并移除文本对象的实现思路,也能直接打包 jar 用于实际工作中,或在其基础上增加自定义过滤规则,作为 PDF 水印处理的二次开发起点。 上周同事扔过来一份内部评审稿,PDF 上斜着一行“内部资料 禁止外传”的水印,字不大但每页都有,打印出来很影响观感,电子版想要传阅又显得业余。我第一反应是找个去水印工具,结果试了三个在线平台,要么只能处理图片水印,要么处理完整个页面糊成一片。后来在一个开源仓库里翻到pdf-unstamper,抱着试试看的心态跑了一遍,效果比我预期好很多。这个工具主打的能力非常直接:删除 PDF 里的文本水印,而且官方介绍里写的是“支持任何字体、任何编码和任何语言”。对于经常和 PDF 打交道的人来说,它更像一把手术刀而不是橡皮擦——目标不是把页面弄糊,而是从文字层里把水印对象精准摘掉。这篇文章我把安装、原理、实测场景和翻车经验都整理了一遍,希望能帮后来的人少走弯路。
1. 为什么文本水印是所有去水印场景里最“刁钻”的
1.1 文本水印在 PDF 里到底是什么形态
普通用户看到的是“页面上有一行字”,但在 PDF 内部,文字根本不是一张贴着不动的图片。它是一段藏在页面内容流里的绘制指令,类似“用某某字体、某某字号,在坐标 (x, y) 处,把某些字形画出来”。字体可能被子集化嵌入,编码可能不是标准的 Unicode,甚至不同 PDF 生成工具对同一个“内部资料”四个字,会用完全不同的内部字符串来表示。
这就解释了为什么“字体”和“编码”会成为去水印的核心难点。很多 PDF 里的中文水印用的不是直接的 UTF-8 字符串,而是像 Identity-H 这类字体编码,字符串需要靠字体的ToUnicode映射表才能还原成你能看懂的文字。一旦映射表缺失,你复制出来的水印文字是乱码,工具如果靠“读文字”来判断水印,直接就歇菜了。
还有更复杂的:一份 PDF 可以嵌入了多个字体子集,同是宋体,子集编号不同,内部编码也不同。同一个字“内”,在不同页面可能对应着不同的字符码。如果去水印工具只认识“标准字体、标准编码”,遇到这种文件就只能干瞪眼。
1.2 常见去水印工具为什么会在这种场景失灵
市面上的去水印工具大致分三类,都有明显短板:
- 在线图片类:把 PDF 每页渲染成图片,再通过图像处理把水印区域“修补”掉。处理完文件体积大,文字不能选、不能搜索,等于把一个电子文档变成了照片。
- OCR 识别类:先识别页面文字,再根据识别结果找到水印词。只要字体稍微特殊,或者有倾斜、半透明效果,识别率立刻下降。
- 白框覆盖类:直接把水印位置涂白。位置稍微偏一点,或者水印跨到正文区域,就把正文也给盖住了。
这三种思路有一个共同盲区:它们都把水印当成“视觉痕迹”来处理。但文本水印本质上是一个结构化对象,它和正文的区别根本不在于长什么样,而在于它每页都出现在几乎相同的位置。如果你能先找到这个“每页都在固定的地方复读”的对象,再把它从文本对象层面删除,那才是真正的干净处理。
我用一张表说清楚文本水印和其他水印的区别:
| 水印类型 | 能否被文本工具识别 | 是否破坏原排版 | 典型处理方式 |
|---|---|---|---|
| 文本水印 | 能 | 基本不破坏 | 删除对应文本对象 |
| 图片水印 | 否 | 会模糊 | 图像修复、找原图 |
| 扫描件水印 | 否 | 不可逆 | 很难自动删除 |
pdf-unstamper走的就是第一条路:它不关心水印长什么样,只关心它是不是“每页都在同一个地方出现的文本对象”。
2. 去水印的核心不是“擦除”,而是“找到”那个每页都复读的文本块
2.1 靠“跨页重复”来锁定水印,而不是靠语义识别
我之前一直以为去文本水印需要先识别“这行字的意思”,比如看到“内部资料”就知道是水印。但pdf-unstamper的思路完全反过来,它靠的是文本块的空间分布规律。
它大致做这么几件事:
- 逐页解析 PDF,提取出页面上所有文本块,记录每个文本块的内容、坐标、字号。
- 把不同页面之间“内容相同、坐标位置重合”的文本块聚在一起。
- 统计这个文本块在整份文档里出现了多少次,如果出现频率超过阈值,就标记为水印候选。
- 对候选位置进行处理,通常是用背景色矩形覆盖对应区域,或者直接重构页面内容流删除相关绘制指令。
也就是说,它把“水印”定义成“正常正文不会重复的一个固定文本块”。正常文档每一页内容都不同,一段文字不太可能每页都恰好出现在同一位置。而水印的目的就是要让每个看文档的人都能看到,必然会把同样的文字放在几乎相同的位置上,重复次数越多,这个特征越明显。
“多行多列文字水印”能处理,也是同样的道理。所谓多行多列,本质就是一组文本块按网格排列,每个小格子里的文本在每一页都有同样的坐标。工具只要把所有满足“跨页重复”的文本块都揪出来,就能一次性清空整片水印。
2.2 对字体和编码不敏感,因为它不需要“看懂”
这个方法最巧妙的一点是,它对“文本内容到底是什么”完全不敏感。判断过程只比较“是不是同一段文字”和“是不是在同样的位置”,至于这段文字是中文、英文、俄文,还是抽取出来已经变成乱码,都不影响判断。
我用一个极简伪代码来描述这个思路,你一看就懂:
# 伪代码:仅示意核心思路 blocks = [] for page in pdf: for block in page.extract_text_blocks(): key = (block.text, round(block.x0, 1), round(block.y0, 1)) blocks[key].append(page.number) for key, pages in blocks.items(): if len(pages) > threshold: # 出现在足够多页面 mark_as_watermark(key)真实工具当然会比这个复杂,比如它会检查字号是否一致、文本块尺寸是否一致、是否与页面边缘保持固定距离等。但核心判断依据永远是“重复性”而不是“语义”。
用个生活化的类比:保安并不需要认识每个人的脸,他只需要发现“这个人每次开会都坐在同一个角落”,行为足够异常,就可以重点关注。水印就是这个“每次开会都坐在同一个位子”的人。
值得注意的是,如果某份 PDF 本身字体映射损坏,文本内容抽出来每次都不同,那跨页对比就会失败。这个问题我在后面的踩坑部分会专门讲,它确实存在,但一般可以通过预处理缓解。
3. 安装与第一次实操:拿一份“有病”的 PDF 练手
3.1 安装和测试文件准备
pdf-unstamper是开源命令行工具,安装方式取决于你拿到的是哪个版本。最常见的是 Python 版本,直接通过 pip 装:
pip install pdf-unstamper如果是源码版本,也可以 clone 到本地后运行python -m pdf_unstamper。装完之后先跑一下帮助命令,确认当前版本的参数风格:
pdf-unstamper --help这一步很重要。命令行工具的版本差异往往很大,有的版本用-i / -o,有的版本用--input / --output。我见过不少人在这个环节浪费了半小时,其实就是参数名对不上。
测试文件建议准备三份:
- 一份用 Word/WPS 直接加文字水印导出成 PDF,这是最常见的场景。
- 一份是多行多列水印,比如用 PDF 编辑器在页面里铺满网格状文字。
- 一份是从特殊软件导出的 PDF,比如 CAD 或专业排版工具生成的,通常字体编码比较“野”。
准备文件的目的是确认工具不会把你的正常正文误删。建议选一页内容丰富的文件,水印旁边就有正文文字,跑完可以检查正文是否完好。
3.2 命令行实操与效果验证
第一次运行,我建议加上 verbose 参数,能输出更多日志:
pdf-unstamper --input ./raw.pdf --output ./clean.pdf --verbose正常情况下,日志里会输出识别到的重复文本块数量,类似“找到 N 个在多个页面重复出现的文本块,已标记为水印”。然后就是等待输出文件生成。
跑完不能急着收工,要做三件事:
- 翻几页随机看水印是否消除,同时看正文有没有缺字、少字。
- 用阅读器的文本选择功能,试着框选原水印区域,看是否还能选到文字。
- 用
pdfinfo或阅读器属性面板,确认页数没有变化,文件没有损坏。
我个人的实测记录里,处理一份 35 页、每页都有“三行两列中英文混合水印”的 PDF,跑完后水印肉眼完全消失,正文里的表格和图片没有位移。这种效果放在以前的在线工具里,我基本不敢想。但我也要坦白,后面遇到几个特殊文件,它也不是万能的。
4. 真实踩坑:哪些“任何”搞不定,以及怎么绕过去
4.1 先花十秒判断:这个水印到底是不是文本
这是所有步骤里最容易忽略、也最关键的一步。在决定用pdf-unstamper之前,花十秒钟做个测试:
打开 PDF,用阅读器自带的“选择文本”工具去框选水印文字。如果你能选中它,说明它是文本水印,工具大概率能处理。如果选不中,只划出一个空白选择框,或者完全无法选中,那它就是图片水印、扫描件水印,或者已经被转成矢量轮廓了。
这个测试的结果直接决定你的处理路线。很多人一上来就拿着工具猛跑,结果文件没反应,就以为是工具不行。实际上是对水印类型判断错了。工具宣传“任何字体、任何编码和任何语言”,这里的“任何”限定词是“文本水印”,不是“所有水印”。图片和扫描件本身不是文本对象,再强大的文本工具也插不上手。
4.2 五个翻车场景和处理办法
我把自己实测中遇到的翻车场景整理成了一张表,方便你对照排查:
| 场景 | 现象 | 原因 | 处理建议 |
|---|---|---|---|
| 图片水印 | 工具处理后无变化 | 水印是嵌在页面里的图片,不是文本对象 | 找原图,或用图像修复手段,别指望文本工具 |
| 文字转轮廓 | 水印能看见但无法选中 | 字体被转成贝塞尔曲线路径 | 尝试重新生成文档,保留文本层 |
| 扫描件水印 | 整页是图片,水印在图片里 | 水印被扫描进图像 | 找电子原稿,或用图像去水印思路 |
| 加密限制 | 工具报错或输出空白 | PDF 有编辑权限限制 | 在有权前提下先移除限制,再处理 |
| 字体映射乱码 | 日志里识别不到水印 | 文本内容抽取不稳定,跨页匹配失败 | 用 qpdf 或 Ghostscript 重写 PDF 后再试 |
字体映射乱码这个场景值得展开说。我遇到过一份工程软件导出的 PDF,水印清晰可见,但用文本工具抽出来全是乱码字符。因为每页抽出的乱码内容可能还不完全一致,跨页匹配就直接失效了。后来我养成了一个习惯:遇到这类文件,先跑一遍 qpdf 做一次解压重写,再交给去水印工具。
qpdf --qdf raw.pdf rewritten.pdf pdf-unstamper --input rewritten.pdf --output clean.pdf --verboseqpdf 重写不是万能的,但对一些编码映射表不完整、或内容流被压缩得很奇怪的 PDF,确实能提高识别率。这个方法我以后都会先试一遍。
还有一点必须提醒:处理别人的文档之前,一定要确认你是否有权去除水印。公司内部临时水印、自己生成的文件,处理起来没有心理负担。但如果是第三方版权文档、用来限制传播的水印,直接去掉可能涉及授权问题。这不是说教,而是实际项目中容易踩的法律风险。
5. 批量处理、文件瘦身与工作流集成
5.1 写一个简单的批处理脚本
如果你有一整个目录的 PDF 需要清理,一条一条命令敲明显不现实。写个简单的 bash 循环就够了:
mkdir -p output for f in *.pdf; do echo "processing: $f" pdf-unstamper -i "$f" -o "output/${f%.pdf}_clean.pdf" || echo "FAIL: $f" done这里有几个细节:
- 文件名带空格很常见,上面脚本里我给
$f加了双引号,防止被拆成两个参数。 - 中文文件名在部分环境下可能出现乱码,建议先确认终端是 UTF-8 编码,或者用 Python 脚本处理。
- 循环里不需要做复杂的异常判断,但至少要打印出失败的文件名,方便事后核查。
用 Python 调用会更稳妥,尤其是文件名复杂、需要并发处理的时候:
import subprocess from pathlib import Path for pdf in Path(".").glob("*.pdf"): out = pdf.with_name(pdf.stem + "_clean.pdf") result = subprocess.run( ["pdf-unstamper", "-i", str(pdf), "-o", str(out)], capture_output=True, text=True ) if result.returncode != 0: print(f"FAIL: {pdf} -> {result.stderr}") else: print(f"OK: {pdf}")批处理之前,我的习惯是先挑 2 到 3 个有代表性的文件放到 test 目录里跑一遍,确认参数正确、没有大面积误删,再全量执行。直接对几百个文件开跑,一旦参数不对,后悔药都来不及吃。
5.2 体积膨胀、文本残留与集成检查
去完水印并不代表流程结束。我实测发现,用覆盖矩形方式处理的 PDF,文件体积可能比原来还大。因为工具没有删掉水印对象,只是在水印上盖了一个白色矩形,对象还在文件里,只是视觉上被遮住了。
这种情况有两个隐患:
一是体积变大,尤其是有大量重复水印对象的文件,扩散到几百页后文件会膨胀明显。可以用 qpdf 压一下:
qpdf --object-streams=generate clean.pdf final.pdf二是文本可能仍然可搜索。如果你在用阅读器搜索时,搜水印里的词还能搜到,说明文本对象还在。对于“视觉上干净”的需求,覆盖方案已经够了;但如果你的目标是“彻底移除文档里的水印文本”,覆盖就不够,需要更高阶的内容流操作,或者重新生成文档。
我的检查清单是这样的:
- 输出页数是否和原文件一致。
- 随机抽查三页,水印是否完全消失、正文是否完整。
- 文本选择模式下,原水印位置是否还能选中内容。
- 文件能否正常打开,页面渲染有没有异常。
如果是在服务端做自动集成,强烈建议再加两道关卡:输入文件先做权限校验,只处理明确允许的文档;输出文件保留副本,方便出问题时回溯。把“去水印”能力开放成无限制的公共接口,很容易被人拿去处理不该处理的文档,这个风险要比工具本身复杂得多。
6. 经验体会与选型建议
用了大半年之后,我的结论是:pdf-unstamper最擅长的是“电子版 PDF + 文本水印”这个组合,尤其是公司内部临时标记、评审稿、批注版本这类高度重复的水印。它对字体和编码不敏感,本质原因是它把水印判定归结为“空间重复性”问题,而不是语义识别问题。理解这一点,你就能准确预判它什么时候有效,什么时候无效。
我自己现在处理一份带水印 PDF 的标准流程是:先用文本选择工具确认水印是不是文本对象;是文本就在备份原件后跑一次 verbose 模式;跑完抽查三页并检查文本是否可搜索;遇到字体乱码就先用 qpdf 重写一遍再尝试;遇到扫描件或图片水印直接换路线,不在这棵树上耗时间。
最后再分享一个小技巧:很多人拿到带水印的 PDF,第一反应是“擦掉它”,但我后来发现,先搞清楚水印类型再做决定,往往比直接开干更省时间。你会少做很多无用功,也更清楚手里的工具到底能帮你解决哪一部分问题。希望这篇经验能让你在下次面对一份花里胡哨的水印 PDF 时,不再一张张截图补白块。
本文还有配套的精品资源,点击获取