Skip to content

About

本地 PDF 编辑工具:内容流级真涂黑、中文字体子集嵌入、页面管理、文字与图形对象编辑。零第三方依赖,纯 Python 标准库 + 原生 JS。

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

PDF 真涂黑 · True Redact

在 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 工具不敢做、或做不到的事:

  1. 真涂黑 —— 把文字从内容流里删掉,而不是盖一个黑块
  2. 写中文 —— 运行时子集化字体并嵌进文件,写进去的字能选中、能复制,但别人改不动

顺手也做成了一个 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。

两处硬骨头

如果你想判断这项目有没有技术含量,看这两个文件就够了。

pdfredact.py(1478 行)—— 内容流级删除

里面最难的是一个内容流解释器:维护完整的图形状态栈
(q / Q / cm / Tf / Td / Tm / Tc / Tw / Tz / TL / Ts),
算出每个字形在页面上的精确矩形(TRM × CTM、字形空间 1/1000 em、字体宽度三级回退、CID 的 /W)。

pdffont.py(609 行)—— 从零写的 TrueType 子集化

用标准库 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。


About

本地 PDF 编辑工具:内容流级真涂黑、中文字体子集嵌入、页面管理、文字与图形对象编辑。零第三方依赖,纯 Python 标准库 + 原生 JS。

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages