PDF 去页码看上去是个简单需求,但多数工具的实现是”视觉遮挡”而不是”内容删除”——原页码仍然完整保留在文本流里,搜索、复制、提取文字时全都浮出来。
这篇讲清楚两种实现的差异、怎么验证、什么场景必须真擦除。
“删除” 和 “覆盖” 的本质区别
原 PDF 的 content stream(每页一段):
BT
/F1 12 Tf
100 50 Td
(正文段落...) Tj
300 50 Td
(12) Tj ← 这就是页码"12"的绘制指令
ET
【白块覆盖】操作后:
BT
/F1 12 Tf
100 50 Td
(正文段落...) Tj
300 50 Td
(12) Tj ← 页码原指令仍然存在
ET
q ← 加了新的图形状态
1 1 1 rg ← 设置白色填充
280 40 60 20 re ← 在页码位置画白色矩形
f ← 填充
Q
视觉上:白色矩形盖住了页码 ✓
文本提取:仍能拿到 "12" ✗
搜索:仍能搜到 ✗
复制:选中能选到 ✗
【真擦除】操作后:
BT
/F1 12 Tf
100 50 Td
(正文段落...) Tj
← 页码绘制指令被物理删除
ET
视觉:原位置无内容 ✓
文本提取:拿不到 "12" ✓
搜索:找不到 ✓
复制:选不中 ✓
两者视觉效果几乎一致,但内部结构完全不同。
三种验证 PDF 页码是否真删的方式
| 方法 | 操作 | 含义 |
|---|---|---|
| 选中复制 | 鼠标在原页码位置拖选 | 能选到 → 被覆盖;选不到 → 真删 |
| 全文搜索 | Ctrl+F 搜页码数字 | 能搜到 → 被覆盖;搜不到 → 真删 |
| 反色查看 | 阅读器开”反色 / 高对比度”模式 | 白色矩形变黑色 → 是覆盖 |
| 文本提取 | pdftotext input.pdf - | 输出含页码 → 被覆盖 |
| content stream 检查 | qpdf --qdf input.pdf - | 看到 (12) Tj 等绘制指令 → 没真删 |
最严谨的是 qpdf --qdf ——直接看绘制指令源码,无所遁形。
PDF 里的”页码”形态
不同 PDF 里的页码可能有完全不同的存储方式:
形态 1:每页独立的文本对象(最常见)
Word / LaTeX 导出的 PDF
↓
每一页的 content stream 里都有独立的页码绘制指令:
Page 1: ... (1) Tj ...
Page 2: ... (2) Tj ...
Page 3: ... (3) Tj ...
处理:一页一页定位 + 真擦除。
形态 2:页眉/页脚作为 Form XObject 复用
专业排版(InDesign / FrameMaker)做的 PDF
↓
所有页面共享一个"页眉模板"对象(Form XObject):
Page 1 引用 → /HeaderTemplate
Page 2 引用 → /HeaderTemplate
...
HeaderTemplate 内部用 PDF 表达式绘制"第 N 页"
其中 N 由每页的特定状态决定
处理:改一次 XObject 影响所有页(高效),或者改每页对 XObject 的引用。
形态 3:AcroForm 字段(Adobe LiveCycle 等动态 PDF)
PDF 内有 /AcroForm 对象,含"页码字段"
渲染时引擎填入页码值
处理:删除 AcroForm 里的字段定义。
形态 4:图像(特殊场景)
扫描的 PDF + 上面有数字水印
或 设计师把页码做成 PNG 贴上去
处理:图像处理——OCR 定位 + 图像擦除(inpainting)+ 重新嵌入。
形态 5:PDF 注释(annotation)
PDF 创建后用注释工具加了"自由文本"作为页码
处理:删除注释对象——这一类是最容易处理的,因为注释是独立对象,不在 content stream 里。
真擦除的实现
正确的真擦除需要走 PDF 引擎而不是字节流操作。
示例:用 mupdf-wasm(浏览器内):
import * as mupdf from 'mupdf';
const doc = mupdf.Document.openDocument(buffer, 'application/pdf');
const page = doc.loadPage(0);
// 1. 用 OCR / 文本提取定位页码 bbox
const textPage = page.toStructuredText();
const bbox = findPageNumberBBox(textPage); // 用户实现
// 2. 创建 redaction annotation
const redact = page.createAnnotation('Redact');
redact.setRect(bbox);
// 3. 应用 redaction —— mupdf 会真正修改 content stream
page.applyRedactions();
// 4. 保存
const outBuffer = doc.saveToBuffer('garbage=2,clean=yes,deflate=yes');
示例:用 Python 的 PyMuPDF:
import fitz # PyMuPDF
doc = fitz.open("input.pdf")
for page in doc:
# 找页码的文本块
blocks = page.get_text("blocks")
for b in blocks:
x0, y0, x1, y1, text, *_ = b
if is_page_number(text): # 用户实现
page.add_redact_annot(fitz.Rect(x0, y0, x1, y1))
page.apply_redactions() # 关键!应用 redaction 才是真删
doc.save("output.pdf", garbage=4, clean=True, deflate=True)
关键点:
redact_annot+apply_redactions()是 PDF 标准里的”真擦除”机制(PDF redaction),符合 ISO 32000 规范- 普通的删除字符不一定是真擦除,必须走 redaction 流程
- 保存时加
garbage=4(清除未引用对象)和clean=yes确保旧数据彻底丢弃
三种擦除方式对比
| 方式 | 视觉效果 | 文本提取 | 搜索 | 增量保存历史 | 法务安全 |
|---|---|---|---|---|---|
| 白块覆盖 | ✓ | ✗(原文还在) | ✗ | 完整保留 | ✗ |
| 删除文本对象 | ✓ | ✓ | ✓ | 旧版本仍在 | △ |
| 真 Redaction + 清理 | ✓ | ✓ | ✓ | 历史已清 | ✓ |
| 重新打印为 PDF | ✓ | ✓ | ✓ | 全新文件 | ✓✓ |
**法务级别(极敏感场景)**推荐 “Redaction + 重新打印 PDF” 双重处理。
决策树
我要去掉 PDF 页码
│
├─ 这个 PDF 之后给谁看?
│ ├─ 自己看 / 学习材料 → 白块覆盖就够(视觉为主)
│ ├─ 发给同事 / 客户 → 用真擦除工具
│ └─ 法务 / 投标 / 公开发布 → Redaction + 清理增量保存
│
├─ 页码是哪种形态?
│ ├─ 文字(能选中复制)→ Redaction 处理
│ ├─ 图像(不能选中)→ 图像处理(OCR 定位 + inpainting)
│ └─ AcroForm 字段 → 删字段
│
└─ 是否要重新加新页码?
├─ 是 → 必须先彻底删旧页码(白块覆盖会叠加)
└─ 否 → 看上面分支
工具选型
| 类型 | 推荐 | 备注 |
|---|---|---|
| 浏览器内 / 隐私 | PDF 去页码 | wasm 真擦除,本地运行 |
| 桌面 GUI | Adobe Acrobat → 编辑 PDF | 真删,但要进”编辑文字”模式 |
| 桌面 GUI | Foxit PhantomPDF | 与 Acrobat 类似 |
| 命令行(Python) | PyMuPDF | redact_annot + apply_redactions |
| 命令行(CLI) | mutool(mupdf) | mutool clean -gggg 清理 + redaction 工具 |
| 配套加新页码 | PDF 加页码 | 真擦除老页码后再加新的 |
| 不推荐 | 大多数在线 “PDF 编辑器” | 多数实现是覆盖,不是真删 |
实务清单
✅ 必做:
- 去页码后先验证:选中复制 + 全文搜索 +
pdftotext三件套 - 重要场景(合同 / 投标 / 公开发布)走 Redaction + 清理
- 重新加页码前一定先彻底删旧页码
- 检查 PDF 是否做过增量保存(
qpdf --check看 xref 数量) - 法务级别再走一次”打印为 PDF” 重建文档
❌ 避免:
- 用”白色矩形涂改”工具当真删(最常见错误)
- 处理完不验证就发出(搜索复制能找出问题,2 分钟核对)
- 直接修改字节流删字符(不解析 PDF 结构会损坏文件)
- 加密 PDF 不解密直接删页码(content stream 改了散列对不上)
- 在线工具传敏感 PDF(公开渠道处理时控制范围)
- 以为页码删了就匿名了——元数据 / 修订历史 / 目录页交叉引用都可能泄露
PDF 去页码的真相是:“看不见 ≠ 不存在”。视觉效果和实际结构是两回事,验证只需要选中复制 + 搜索两步——多数情况下你以为删了的页码其实还完整地躺在文件里。