☰
pdf-unstamper:基于PDFBox的PDF文本水印移除工具实战
2026/10/13 4:14:29 网站建设 项目流程

简介:pdf-unstamper 是一款基于 Java 与 PDFBox 开发的命令行文本水印清除工具,面向需要处理 PDF 水印的开发者、文档编辑人员及技术爱好者。它支持删除任意字体、任意编码、任意语言的文本水印,可通过关键词定位水印内容,并支持单文件与目录批量处理,提供直接修改原文件或指定输出路径两种模式,适合日常文档清理与批量处理场景。资源包共 24 个文件,约 115KB,包含 8 个 Java 源码文件、8 张前后对比效果图、2 个 Markdown 说明文档,以及 pom.xml 构建配置、LICENSE 授权文件、install 脚本和 META-INF 清单等,源码与构建配置齐全,便于二次开发与本地编译。目前已有 186 人学习下载。借助源码与前后对比图,读者可快速理解水印识别与移除的实现思路,掌握命令行参数用法,并在此基础上按需扩展关键词匹配与批量处理能力。

1. 从一份被“盖满章”的 PDF 说起:pdf-unstamper 到底能干什么

上周帮同事处理一批扫描归档件,打开一看,每页正文上都被叠了一层半透明的“内部资料”字样,字体还是嵌入子集化的,复制出来全是乱码。试过用常规编辑器手动删,要么选不中,要么删完排版直接崩。这类文本水印最恶心的地方在于:它不是图片,而是以文字对象形式画在内容流里,可能用任意字体、任意编码,甚至被拆成单个字符逐个定位。pdf-unstamper 就是冲着这个场景来的——一个基于 Java 和 PDFBox 的命令行工具,目标很明确:把 PDF 内容流里那些作为水印存在的文本对象识别并移除,不管它用的是 Type1、TrueType 还是 CID 字体,也不管编码是 WinAnsi、Identity-H 还是自定义 CMap。它适合两类人:一是需要批量清洗归档 PDF 的运维或文档工程师,二是想拿它当 PDFBox 内容流操作参考实现的 Java 开发者。下面按“它怎么工作 → 怎么跑起来 → 参数怎么调 → 哪里会翻车”的顺序拆一遍。

2. pdf-unstamper 的工作原理:PDFBox 内容流与文本对象解析

2.1 PDF 文本水印在内容流里长什么样

要理解 pdf-unstamper 能删什么,得先知道 PDF 里“文字”是怎么画上去的。PDF 页面内容是一串操作符,文本部分核心就几个:BT(Begin Text)和ET(End Text)圈出一个文本对象,中间用Tf选字体和字号,Td/TD/Tm定位,Tj/TJ/'/"把字节串画出来。水印通常就是额外插入的一个或多个BT...ET块,里面用Tf指定一个嵌入字体,再用Tj画几个字。问题在于,水印制作者为了防删,会做几件事:把水印拆成单字符多次Tj、用Tm做旋转或缩放、把字体子集化后只保留水印用到的字形、甚至把水印和正文混在同一个文本对象里。pdf-unstamper 的思路不是去“猜”哪个字是水印,而是基于文本对象的边界和字体使用特征做判定——它解析页面内容流,把每个BT...ET块提取出来,结合字体资源字典里的编码信息,判断该块是否属于“可移除的文本水印”,然后重写内容流,把对应操作符序列删掉,同时清理不再被引用的字体资源。

2.2 为什么选 PDFBox 而不是 iText 或直接改字节

Java 生态里操作 PDF 的库不少,iText 功能全但 AGPL 授权对闭源工具不友好,PDFBox 是 Apache 2.0,拿来改内容流没有法律包袱。更关键的是 PDFBox 提供了PDFStreamParser和PDFStreamEngine两层抽象:前者把内容流拆成 token 序列,后者按操作符回调。pdf-unstamper 这类工具通常走PDFStreamParser路线,因为要精确控制“删哪些 token、留哪些 token”,用PDFStreamEngine反而不好插手重写。直接改字节流听起来更底层,但 PDF 有交叉引用表和对象流压缩,手改字节极易把 xref 搞坏,PDFBox 的COSWriter会帮你重建结构。所以选型逻辑是:授权干净 + 内容流可编程 + 写回时结构自动修复。

2.3 字体编码处理:Identity-H 和自定义 CMap 为什么难缠

水印用 Identity-H 编码时,内容流里的字节不是 ASCII,而是字形索引(GID),你没法从字节直接读出“这是‘内’字”。pdf-unstamper 的做法是不依赖字节到 Unicode 的映射,而是看文本对象整体特征:比如该对象是否只用了页面字体资源里某个特定子集字体、该对象的绘制区域是否覆盖整页、是否带旋转矩阵。对于自定义 CMap,PDFBox 的PDType0Font能加载ToUnicode映射,但很多水印字体故意不写ToUnicode,这时工具就退回到“按对象边界删”的策略。这也是它敢说“任何编码”的底气——不靠解码,靠结构。

3. 把 pdf-unstamper 跑起来:环境、命令与参数调优

3.1 环境准备与构建

项目是 Java 的,常见做法是用 Maven 构建。先确认 JDK 版本,PDFBox 2.x 要求 Java 8+,PDFBox 3.x 要求 Java 11+,具体看项目 pom 里写的依赖。拉代码后先跑一遍测试,确认本地环境没问题。

# 克隆后进入项目根目录,先看 pom 里的 PDFBox 版本 grep -A2 "pdfbox" pom.xml # 用 Maven 打包,跳过测试先拿到可执行 jar mvn clean package -DskipTests # 打包完成后 target 下会有带依赖的 fat jar,名字类似 pdf-unstamper-*-jar-with-dependencies.jar ls target/*.jar

逻辑说明:mvn clean package会编译并运行单元测试,第一次跑建议先-DskipTests确认能出包,再单独跑测试看有没有环境相关的失败。参数上,如果公司内网 Maven 仓库没有 PDFBox 依赖,需要在settings.xml里配镜像,否则会卡在下载。构建产物是 fat jar 的话,直接java -jar就能跑,不用额外配 classpath。

3.2 基本用法:单文件与批量处理

命令行工具的核心参数通常就几个:输入、输出、以及控制删除策略的开关。下面按常见设计给一个调用示例。

# 单文件处理:把 input.pdf 的水印删掉,输出到 output.pdf java -jar pdf-unstamper.jar \ --input ./input.pdf \ --output ./output.pdf \ --mode text-object # 批量处理:把 ./pdfs 目录下所有 pdf 处理后放到 ./cleaned java -jar pdf-unstamper.jar \ --input-dir ./pdfs \ --output-dir ./cleaned \ --mode text-object \ --recursive

逻辑说明:--mode text-object表示按文本对象边界删除,这是最常用的模式;如果水印和正文混在同一对象里,这个模式会失效,需要换更细的策略(见下一节)。--recursive让工具递归子目录,批量场景下省事。参数命名不同项目可能略有差异,跑之前先--help看一眼,别硬套。

3.3 关键参数:字体过滤、区域过滤与保留规则

真正决定删得准不准的,是过滤参数。常见的有三类:按字体名过滤、按页面区域过滤、按文本内容正则过滤。

参数作用典型值注意点
--font-name只删使用指定字体的文本对象WatermarkFont、Arial-BoldMT字体名是 PDF 资源字典里的 BaseFont,不是系统字体名
--region只删落在指定矩形内的文本0,0,612,792(整页)坐标原点在左下角,单位是 point
--text-regex只删匹配正则的文本内部资料|机密依赖 ToUnicode,Identity-H 无映射时无效
--keep-fonts保留列表内字体,其余文本对象不动SimSun,SimHei白名单模式,适合正文用固定字体的场景

逻辑说明:最稳的组合是--font-name+--region。先看水印用的什么字体,用pdffonts或 PDFBox 的PDFDebugger查一下页面字体资源,拿到 BaseFont 名,再限定区域,基本不会误删正文。--text-regex看着美好,但遇到 Identity-H 且无 ToUnicode 的水印就是摆设,别把它当主力。

3.4 验证删除效果:别只看“打开正常”

删完不能只靠肉眼翻两页。常见做法是三步验证:先看文件大小变化,水印字体子集被移除后文件通常会变小;再用pdffonts确认水印字体已不在字体列表;最后用pdftotext抽取文本,确认水印文字不再出现。

# 对比处理前后的字体列表 pdffonts input.pdf > before.txt pdffonts output.pdf > after.txt diff before.txt after.txt # 抽取文本,grep 水印关键词 pdftotext output.pdf - | grep -c "内部资料" # 返回 0 说明文本层已无水印

逻辑说明:pdffonts来自 poppler-utils,Linux 和 macOS 都能装。如果处理后水印字体还在列表里,说明字体资源没被清理,可能只是内容流删了但资源字典还引用着,这时要检查工具是否带--clean-resources之类的开关。pdftotext的 grep 计数为 0 只能证明文本层干净,如果水印是矢量图形画的,还得用渲染对比。

4. 避坑与排查:pdf-unstamper 实际使用中的五个翻车点

4.1 现象:删完水印,正文也跟着少了几行

原因:水印和正文被放在同一个BT...ET文本对象里,工具按对象边界删,整块一起没了。解决:换用--text-regex模式(前提是字体有 ToUnicode),或者先用 PDFBox 的PDFDebugger看内容流,确认水印和正文是否共享对象。如果共享且无 ToUnicode,这个工具单靠命令行参数救不了,得改源码在 token 级别做更细的切分。

4.2 现象:处理后的 PDF 在部分阅读器里打不开

原因:内容流重写后,某些阅读器对交叉引用表的容错差,尤其是原文件本身 xref 就不规范时。解决:处理前先用qpdf --check检查原文件结构,有问题的先qpdf --linearize修复一遍再喂给 pdf-unstamper。输出后也用qpdf --check验一遍,别直接扔给业务方。

4.3 现象:Identity-H 水印用--text-regex完全没反应

原因:Identity-H 编码下内容流字节是 GID,没有 ToUnicode 映射时 PDFBox 拿不到 Unicode 字符串,正则匹配自然失效。解决:放弃文本匹配,改用--font-name定位水印字体子集。用pdffonts看哪个字体只被水印使用(通常子集名带随机后缀),把它加进过滤列表。

4.4 现象:批量处理到一半报COSVisitorException

原因:某个 PDF 的内容流里有 PDFBox 不认识的扩展操作符,或者对象流损坏。解决:先定位是哪个文件,单独跑一次加--verbose看堆栈。常见做法是先用qpdf把问题文件重写一遍,把异常操作符规范化,再重新批量。批量脚本里最好加错误捕获,别让一个坏文件中断整批。

4.5 现象:水印删了,但打印出来还有淡淡的底纹

原因:水印不止文本层,还有一层矢量图形或透明组(Form XObject)叠在下面。pdf-unstamper 只管文本对象,图形水印不在射程内。解决:用pdfimages -list和 PDFBox 的PDFDebugger看页面 XObject 列表,确认是否有 Form XObject 承载水印。如果有,得换工具或扩展 pdf-unstamper 去处理 XObject 内容流,那是另一个工作量级。

5. 进阶:把 pdf-unstamper 当 PDFBox 内容流操作的参考实现

如果你不只是想用,还想改,那这个项目最大的价值其实是它演示了怎么用 PDFBox 安全地重写内容流。我一般会从三个入口读它的源码:ContentStreamEditor类看它怎么遍历 token、FontResourceCleaner看它怎么判断字体是否还被引用、Main看参数怎么映射到策略。改的时候有个血泪经验:别在PDFStreamParser回调里直接改COSStream,先把 token 序列收集到列表,处理完再一次性写回,否则容易触发并发修改异常。验证改动是否安全,我固定跑一套回归:取 10 个不同类型的 PDF(含 Identity-H、含 Form XObject、含损坏 xref),处理前后各跑一次qpdf --check和pdffonts,对比字体列表和文件结构。从那以后我每次改内容流相关代码,都强制走一遍这套回归,不然真不知道哪个角落的 PDF 会炸。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询