PDF 批注怎么彻底删掉?隐藏≠删除,附验证命令
很多人遇到过这个情况:在 PDF 上写了批注,删掉、保存、发出去,结果对方还是看到了。
先说结论:你在阅读器里"删除批注",很多时候只是让它不再渲染,对象还在文件里。 这篇讲清楚批注到底存在哪、为什么删不掉、怎么验证。
一、批注在文件里的位置
PDF 是对象树结构。批注不是画在页面上的像素,而是独立对象,通过引用挂在页面字典的 /Annots 数组上:
4 0 obj
<< /Type /Page
/Parent 2 0 R
/Contents 5 0 R
/Annots [10 0 R 11 0 R] % 批注引用
>>
endobj
10 0 obj
<< /Type /Annot
/Subtype /Text
/Rect [100 700 120 720]
/Contents (这里的价格需要重新确认)
/T (zhangsan@company.com)
/M (D:20261024153000+08'00')
/Popup 13 0 R
>>
endobj
三个字段要注意:
/T(作者):你用什么账号登录编辑器,这里就留什么,通常是邮箱。/M(修改时间):精确到秒。/Popup:弹出框是独立的引用对象,不是批注的内嵌字段。
最后一条就是"删不干净"的第一个原因:删了主批注对象,Popup 还挂在文件里。
二、为什么"删掉"了对方还能看见
有三个层次的原因,从浅到深:
层次 1:隐藏 ≠ 删除
阅读器里的"隐藏批注"只是不渲染,对象仍在 /Annots 数组里。换个工具扫描批注层,一条不少。
层次 2:增量更新保留了旧字节
PDF 支持增量更新:修改时不重写全文,而是在文件尾部追加一段新的 xref,用 /Prev 指向旧的。
结果是:你"删除"的对象,字节完整地留在文件里。
pdftotext 抽不到,是因为它按最终状态解析。但字节还在,动手就能拿到:
strings 文件.pdf | grep -i "你批注过的关键词"
层次 3:批注层根本不在正文流里
这点最反直觉。批注是页面字典的独立字段,不在 /Contents 指向的内容流中。所以:
- 扫描内容流找批注 → 找不到(本来就不在那);
- 只看
/Annots→ 才能看到全部。
三、正确的删除方式
步骤 1:用库在对象层操作,别手写字节
直接改字节会破坏交叉引用表(xref 记录每个对象的字节偏移),删了字节后面全错位 → 文件打不开。
用 pdf-lib:
const { PDFDocument, PDFName } = require('pdf-lib');
const fs = require('fs');
const bytes = fs.readFileSync('in.pdf');
const pdfDoc = await PDFDocument.load(bytes);
for (const page of pdfDoc.getPages()) {
page.node.set(PDFName.of('Annots'), pdfDoc.context.obj([]));
}
fs.writeFileSync('out.pdf', await pdfDoc.save());
步骤 2:处理增量更新
对象层删除之后,旧版本的字节还在。做一次完整重写:
# mupdf:清理未引用对象 + 解压流
mutool clean -g -s -d in.pdf out.pdf
# qpdf:线性化重写
qpdf --linearize in.pdf out.pdf
mutool clean 的参数:-g 垃圾回收(删未引用对象)、-s 清理未使用资源、-d 解压所有流。
步骤 3:验证(这一步不能省)
# 1. 抽全文,搜批注关键词
pdftotext out.pdf - | grep -i "价格需要重新确认"
# 2. 搜原始字节(查增量更新残留)
strings out.pdf | grep -i "价格需要重新确认"
# 3. 看文档属性
pdfinfo out.pdf
两条 grep 都搜不出来,才算真的清干净。
四、Subtype 不同,处理方式不一样
| Subtype | 含义 | 注意点 |
|---|---|---|
/Text |
便签 | 删对象即可 |
/Highlight |
高亮 | 只删批注,正文不变 |
/FreeText |
自由文本 | 看起来像正文,实为浮层 |
/Widget |
表单域 | 删了表单就不能填,慎删 |
/Watermark |
水印注释 | 只能对象层删,白块盖不住 |
/Stamp |
图章 | 通常带外观流(/AP) |
/FreeText 是最大的坑:用户打字改了数字,屏幕显示新值,但底下原文一个字节没动。对方框选就能复制出原始数字。
五、顺带说一下检测清单
如果你的需求是"文件外发前扫一遍",批注只是其中一层。完整的有八层:
| 检测项 | 技术要点 |
|---|---|
| 文字水印 | 内容流扫描;中文走 Identity-H CID + ToUnicode CMap 还原 |
| 图片水印 | 递归 Form XObject + cm 矩阵复合映射 |
| 注释水印 | /Subtype /Watermark,对象层删 |
| 元数据 | 作者 / 公司 / 路径 / 软件 / 时间 |
| 隐藏文字 | 白字、被遮挡、图层下 |
| 批注与修订 | /Annots + /Popup + 增量更新段 |
| 隐私信息 | 身份证 / 手机 / 银行账号(含图片 OCR) |
| 暗水印 | 频域 / 像素级 |
六、总结
三句话:
- 批注是独立对象,不是页面像素 —— 所以在
/Annots里,不在内容流里。 - "看不见"不等于"不存在" —— 增量更新和 Popup 都会残留。
- 验证只需要两条 grep —— 别信工具的"已清除"。
pdftotext 文件.pdf - | grep -i "关键词"
strings 文件.pdf | grep -i "关键词"
相关工具:PDFnoted(pdfnoted.com),做文档安全工作流(扫描 → 脱敏 → 批注 → 导出),元数据清理、批注清理、文字水印清理免费且免登录。
浙公网安备 33010602011771号