在 PDF 里涂黑一段字,然后按 Ctrl+F 搜它 —— 大多数工具还能搜到。
因为它们只是盖了个黑块:原文一个字节都没动,还躺在内容流里。
strings一跑、pdftotext一提取、qpdf --qdf一解压,全都出来。这个工具是真的把字从内容流里删掉:搜不到、复制不出、提取不到。
零第三方依赖 · 纯 Python 标准库 + 原生 JS · GPL-3.0 · 本地运行,文件不上传
Redact PDF text for real — delete the text operators from the content stream, don't just paint a black box. Zero runtime dependencies. Runs locally; your files never leave your machine.
一个跑在本地的 PDF 工具。它做两件大多数 PDF 工具不敢做、或做不到的事:
- 真涂黑 —— 把文字从内容流里删掉,而不是盖一个黑块
- 写中文 —— 运行时子集化字体并嵌进文件,写进去的字能选中、能复制,但别人改不动
顺手也做成了一个 PDF 阅读器 / 标注器 / 图形编辑器。但上面那两件才是它存在的理由。
普通的「涂白」只是往内容流末尾追加一个白色填充矩形:
1 1 1 rg 72 640 300 24 re f
原文的文字指令一个字节都没动。于是:
- Ctrl+A 全选复制,被盖住的字原样出现在剪贴板里
pdftotext和任何文本提取工具照读不误- 搜索引擎能索引到
- 白框删掉就原形毕露,或者
qpdf --qdf解压内容流直接看
这就是"PDF 涂黑事故"的成因:发布者以为盖住了,其实没有。
真涂黑的语义完全不同——它要求信息真正消失。所以必须把内容流解释执行一遍,
算出每个字形在页面上的精确矩形,再把它整字删除。
这几条不是"还没做",是明确不做,理由都在代码里写着:
| 不做 | 为什么 |
|---|---|
| 扫描件 / 图片里的文字 | 那需要 OCR,而 OCR 会错。涂黑这件事不能有 1% 的漏网率——那 1% 就是泄漏。所以扫描件请先用别的工具转成文本 PDF 再用它。宁可明确拒绝,也不给你一个看起来安全的文件。 |
| 移动原 PDF 里已有的文字(回流重排) | PDF 不记录段落结构,重排必须反推、必然有损 |
| 压缩输出 | 输出不压缩,便于逐字节核对 |
| CFF / OTF 字体子集化 | CFF 要重建 INDEX / charstring / subrs 结构,是另一个量级 |
| 表单、数字签名、印刷色彩管理 | 不在本工具的范围内 |
★ 关于"零第三方依赖"的准确说法
运行期零第三方依赖:整个工具只用 Python 标准库。
唯一的例外是makefont.py——它在开发期用一次 fontTools,
把可变字体实例化到常规字重,产出一个静态 TTF 作为资产。
它只在隔离环境里跑一次,跑完的产物已提交到assets/fonts/,
使用者不需要它。运行时真正做子集化的是本项目自己的pdffont.py。
如果你想判断这项目有没有技术含量,看这两个文件就够了。
里面最难的是一个内容流解释器:维护完整的图形状态栈
(q / Q / cm / Tf / Td / Tm / Tc / Tw / Tz / TL / Ts),
算出每个字形在页面上的精确矩形(TRM × CTM、字形空间 1/1000 em、字体宽度三级回退、CID 的 /W)。
用标准库 struct 从头实现,没引 fontTools。这里面至少六个规范级陷阱:
loca短格式存的是offset / 2(本实现强制改长格式,避开奇偶对齐问题)- 复合字形的四种 scale 标志位推进(
ARG_1_AND_2_ARE_WORDS/WE_HAVE_A_SCALE/X_AND_Y_SCALE/TWO_BY_TWO) - 汉字大量由部件合成,不递归展开组件就会出现"某些字缺笔画"
hmtx的numberOfHMetrics语义- sfnt 表目录必须按 tag 排序(二进制搜索依赖它)、每表 4 字节对齐
head.checkSumAdjustment:先把该字段置 0 算整个文件的校验和,再0xB1B0AFBA - cks- 如果还有疑问,请使用fontcheck.py做体检!
| 验证项 | 方法 | 结果 |
|---|---|---|
| 涂黑后确实读不出来 | 涂黑后跑 strings / pdftotext 扫原文关键词 |
命中 0 次 |
| 写进去的中文可选中 | 用 /ToUnicode 把 GID 还原,与原文比对 |
一致 |
| 子集化的体积收益 | 全量 Noto Sans SC 17MB vs 实际嵌入 | __ KB |
python server.py --dir "你的PDF目录"浏览器会打开 localhost。如果秒级超时打不开,说明端口被占,换一个:
python server.py --port 8080其它可选:
python run_tests.py # 跑全部测试
python check.py # 前端静态自检
python fontcheck.py <字体.ttf> # 换字体前先体检字体锁在 assets/fonts/NotoSansSC-Regular.ttf(思源黑体 / Noto Sans SC,OFL 授权)。
换字体必须满足三条:OFL 授权 + glyf 轮廓(CFF 做不了子集化)+ 默认实例是常规字重。
GPL-3.0。