在 Delphi 中进行真实的 PDF 内容擦除意味着删除底层的字节,而不是在上面涂画。losLab PDF Library 用两个 API 划定了这条界线:RedactRegion 编辑页面内容流,从而使被移除的文本、矢量和图像在物理上彻底消失;而 SanitizeDocument 则清除可能隐藏擦除信息的元数据、脚本和操作。如果其中任何一半做错,您发出的文件在屏幕上看起来被审查过,但任何人都可以通过复制文本或打开文档属性将秘密拿回
这种失败模式是众所周知的。报纸、法院和政府机构都曾发布过用黑色矩形覆盖姓名或数字的 PDF,结果读者选中了下方的文本并进行复制,然后将本应被擦除的字符串粘贴到光天化日之下。那个矩形只是一幅画,文本仍然存在,处于画作下方一层的内容流中,完整无损且完全可选。通过 TPDFlib 类公开的 Delphi 和 C++Builder PDF 引擎 losLab PDF Library 将这种区别视为擦除的全部意义,而不是事后的想法
为什么在 PDF 文本上画一个黑盒子不是真正的擦除?
losLab PDF Library 为真正需要覆盖的情况公开了 MaskRect,例如水印、视觉屏蔽、证明印章。MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) 将一个矩形追加到页面内容流中,并使用 re 路径构建操作符和 f 填充操作符 (ISO 32000-1 §8.5) 对其进行绘制。页面上已经存在的每一个字节(例如您覆盖的姓名字形、矢量笔画、嵌入的图像)都原封不动地保留在同一个流中较早的一条指令中。合规的查看器会最后绘制黑盒子,因此它在视觉上占优,但遮盖并未改变下方的任何内容
任何人都可以通过三种方式检索原始内容:直接穿过不透明的盒子选择并复制文本、在页面上运行文本提取器,或者从内容流中删除覆盖的矩形。MaskRect 在其名称中就很诚实——它只是遮罩,并不移除,其效果就像是贴在单词上的贴纸,您仍可以通过撕下贴纸来阅读它
RedactRegion 如何在指令级别删除内容
RedactRegion(Page, Left, Top, Width, Height, Options) 工作在绘制层下方的一层。losLab PDF Library 将页面的每个内容层解析为 TPDFContentProgram,使用 CTM 跟踪分析器(内容模型中的 FindInstructionsInRect)遍历指令列表,并标记绘制区域与您的矩形相交的每个显示文本、绘制路径和图像 Do 操作符。这些指令被 TPDFContentProgram.Delete 移除,编辑后的层被 WritePageContentLayer 写回。这些字节并没有滑到遮盖物后面,而是离开了流,随后运行的文本提取器将一无所获,因为已经没有任何东西可供寻找
有两个实现细节值得带入您自己的思考。RedactRegion 接受当前原点和单位下的左上角矩形,并在内部将其转换为内容流使用的左下角用户空间,因此您可以按照在页面上看到的方式指定区域。当一个矩形覆盖多条指令时,RedactRegion 会按索引降序删除它们,因为在指令 7 之前删除指令 4 会移动每个后续索引并破坏范围。传入 Options = 0,losLab PDF Library 还会把一个实心黑盒子盖在已清除的区域上作为可见标记——但该标记纯粹是装饰性的,是在真实内容已经消失之后才应用的
在实践中,您很少需要硬编码矩形。您在页面中搜索敏感字符串并读回其边界框(这与在 在 Delphi 中定位文本和页面元素 中涵盖的页面搜索和元素枚举相同),然后将该矩形传递给 RedactRegion
var
PDF: TPDFlib;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile('contract.pdf', '') = 1 then
begin
// 删除第 1 页上绘制在 220 x 14 pt 框内的所有内容
// 坐标为左上角,采用当前的原点和单位
PDF.RedactRegion(1, 90, 705, 220, 14, 0); // Options=0 会加盖黑色标记
PDF.SaveToFile('contract-redacted.pdf');
end;
finally
PDF.Free;
end;
end;
被擦除的信息在 PDF 中仍会隐藏在哪里?
仅停留在可见层的擦除会留下痕迹,因为 PDF 在读者从不关注的地方存储信息。losLab PDF Library 将这些泄露通道进行了分类。文档信息字典和 XMP 元数据流通常携带作者、制作程序或关键字字符串,这些字符串可能正是您从正文中删除的人名或案件名。文档级 JavaScript 和编目 OpenAction 可以在打开时执行并访问您认为已消失的数据。每个注释都携带一个可选的 /A操作,并且注释本身(便签、过期的表单字段、链接)可以直接位于被擦除的位置之上,并带有其自己的文本副本。这些都不会受到加密的影响,一个带有明文 XMP 数据包的加密文件仍会向任何索引器泄露其标题,这是一个非常值得进行 审核加密和权限 的漏洞。严密的净化必须访问每一个隐藏地点,而不仅仅是您正在查看的页面
使用 SanitizeDocument 清理文档
losLab PDF Library 通过 SanitizeDocument(Flags) 来应对这种广泛的泄露,这是一个编排调用,它运行由位掩码选择的一组独立清理动作,并返回它应用的清理动作数量。这些标志是可以组合的。SANITIZE_JAVASCRIPT 移除每个文档级 JavaScript 包 and 任何 OpenAction 脚本。SANITIZE_ACTIONS 通过新的 TPDFAnnots.RemoveAnnotAction 从每个注释中剥离 /A 操作字典,并清除编目 OpenAction。SANITIZE_METADATA 同时在文档信息字典 (ISO 32000-1 §14.3.3) 和 XMP 元数据流 (§14.3.2) 中清空作者、主题、关键字、创建者和制作程序。SANITIZE_ANNOTATIONS 在全文档范围内删除每个注释,而 SANITIZE_OPEN_ACTION 则移除编目 /OpenAction 条目。SANITIZE_ALL 是所有这五个标志的按位并集
这些清理动作内部的索引约定正是容易导致手动循环崩溃的原因,这也是为什么 SanitizeDocument 作为单次调用而存在的大部分原因。GlobalJavaScriptPackageName 是基于 0 的,并且 RemoveGlobalJavaScript 会随着进行的进度缩小列表,因此 JavaScript 清理动作始终读取包索引 0 并进行移除,直到计数达到零。相比制造下,DeleteAnnotation 是基于 1 的。AnnotationCount 报告当前选中页面的情况,因此注释清理动作在计数之前会依次选中每个页面。losLab PDF Library 在内部正确处理了这些边界,因此您只需传递一个标志集并读回一个计数,而无需重现五种不同的迭代规则
var
Applied: Integer;
begin
// 剥离元数据、脚本和操作,但保留注释
Applied := PDF.SanitizeDocument(SANITIZE_METADATA or SANITIZE_JAVASCRIPT or
SANITIZE_ACTIONS or SANITIZE_OPEN_ACTION);
// 或者清除每个隐藏通道,包括注释
Applied := PDF.SanitizeDocument(SANITIZE_ALL);
end;
完整的擦除与净化工作流程
将两者结合起来,完整的步骤非常简短。losLab PDF Library 逐页使用 RedactRegion 移除可见内容,然后在使用 SanitizeDocument(SANITIZE_ALL) 写入文件之前清除每个隐藏通道。因为擦除几乎总是运行在那些从您控制之外传来的文档上,所以值得通过防御性路径加载它们(这与在 安全解析不受信任的 PDF 中描述的防御姿态相同),以便畸形的输入能干净地失败,而不是在擦除过程中途崩溃
var
PDF: TPDFlib;
Applied: Integer;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile('incoming.pdf', '') = 1 then
begin
PDF.RedactRegion(1, 90, 705, 220, 14, 0); // 删除敏感行
Applied := PDF.SanitizeDocument(SANITIZE_ALL); // 清除元数据、JS、操作、注释
PDF.SaveToFile('published.pdf');
end;
finally
PDF.Free;
end;
end;
内容流擦除的终点
RedactRegion 操作于内容流,这正是数字源生文本所在的地方,也是扫描文本不在的地方。当页面是扫描图像时,单词是图像 XObject 内部的像素,而不是文本操作符,losLab PDF Library 无法像删除字形那样从光栅中间雕刻出一个名字。它对图像的处理是刻意保守的:任何放置区域与矩形相交的图像都会被整体移除,因此扫描页面上的擦除框会连同整个扫描图一起带走,而不仅仅是它的一小块。在图像内部擦除某个区域意味着在像素级别进行光栅化、绘制和重新嵌入,这与内容流删除是不同的操作。同样的警惕也适用于扫描页面上的 OCR 文本层:RedactRegion 会移除它能看到的文本操作符,但除非其图像 XObject 也与该框相交,否则下方的图像将保持不受影响
有两个微妙之处可以保证结果的真实。相交测试是根据字符数和字体大小来估算每个文本运行的宽度,而不是测量精确的字形指标,因此设置在比例字体中的非常短的单词可能会滑过绘制得很紧凑的矩形——请给框留一点微小的边距。删除也是在指令粒度上进行的:RedactRegion 会移除整个显示文本操作符,因此仅剪切一部分文本运行的矩形可能会带走与其共享同一个操作符的相邻字符。这两种限制都不是怀疑该机制的理由,只是要仔细划定矩形并检查结果的理由
结束每个擦除工作流程的纪律是验证,而不是信任。擦除区域、净化文档、保存到新文件、重新打开它,并尝试拉回您移除的文本和您清除的元数据;当两者都返回为空时,擦除才算成立。这里展示的 RedactRegion 和 SanitizeDocument API 随适用于 Delphi 和 C++Builder 的 losLab PDF Library 一起提供,同时也附带完整的内容模型和净化参考