不带 /IC 颜色画的涂黑标记,走出应用步骤时可能成了一块透明空缺或一圈空心轮廓,而审阅者写在它上面的备注还留在文件里。PDFlibPas 用 ApplyRedactions 应用 Redact 注解:先清掉被覆盖的内容,覆盖层取自 /RO 或取 /IC、缺省补黑,最后把标记的字典清空
标记可逆,应用不可逆
ISO 32000-1 §12.5.6.23 把涂黑分成内容识别与内容移除。识别阶段的 Redact 注解只是个提案,用户还能看见、挪动和重新定义它,审阅环节因此才成立。移除交给应用来做,要求很硬:注解识别的一切都得消失,注解本身也得消失,图像被覆盖的部分要销毁数据——拿裁剪路径或图像蒙版盖住像素不算数。键列在 ISO 32000-1 表 192(ISO 32000-2 表 195),其中大多数描述的是移除之后页面该长什么样,而不是要移除什么
| 键 | 规范中的角色 | ApplyRedactions 对它做什么 |
|---|---|---|
QuadPoints | 以 n 个四边形给出的移除区域;存在时取代 Rect 充当区域 | 逐个四边形单独擦除其包围盒;长度不是 8 的倍数的数组退回 Rect |
Rect | QuadPoints 缺席时的移除区域 | 使用时把反转的角归正 |
IC | 移除后的 DeviceRGB 填充;缺席即透明;有 RO 时被忽略 | 在就保留,缺就补黑 |
RO | Form XObject 覆盖层;优先于 IC、OverlayText、DA 和 Q | 安装为常规外观并压平到页面上 |
OverlayText、Repeat | 画在区域上的标签,可选平铺 | 画进生成的覆盖层,Repeat 为真时平铺 |
DA、Q | 标签的字体、字号与颜色;对齐方式 | 字号与颜色读自 DA,对齐读自 Q |
用 AddRedactAnnotation 创建标记
TPDFlib.AddRedactAnnotation(Left, Top, Width, Height, FillR, FillG, FillB, OverlayText, RepeatOverlay) 向所选页面添加一个标记,返回页面新的注解数——这也是该标记从 1 起计的索引——什么都没加时返回 0。坐标遵循当前原点与单位,所以示例切到左上角原点,此时 Top 是上边缘。这次调用写入 /Rect、由填充分量组成的 /IC,以及一条固定的 /DA——/F0 10 Tf 1 1 1 rg,10 磅白字;非空的 OverlayText 外加 /OverlayText、/Repeat 和用于居中标签的 /Q 1。Redact 注解是 PDF 1.7 的特性,更旧的文件头会抬到 1.7。此刻还不写外观流;待定标记长什么样,全看审阅者的应用怎么画。标签固定为白色,所以 OverlayText 要配深色填充,浅黄底上标签就看不见了。每个 PDFlibPas 版本都能创建标记
uses
System.SysUtils, PDFlibrary;
procedure MarkForReview(const SourceFile, ReviewFile: string);
var
Pdf: TPDFlib;
Index: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile(SourceFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [SourceFile]);
Pdf.SetOrigin(1); // 左上角原点:Top 是上边缘
Pdf.SetMeasurementUnits(0); // 点
if Pdf.SelectPage(1) <> 1 then
raise Exception.Create('Page 1 not found');
// 证人姓名:黑色填充,居中白标签
Index := Pdf.AddRedactAnnotation(72, 180, 220, 14, 0, 0, 0, 'REDACTED');
if Index = 0 then
raise Exception.Create('Mark was not added');
// 账户栏:把豁免代码平铺到更高的框里
Pdf.AddRedactAnnotation(72, 410, 300, 40, 0, 0, 0, '(b)(6)', 1);
// 只是标记;原始内容的每个字节都还在
Pdf.SaveToFile(ReviewFile);
finally
Pdf.Free;
end;
end;
QuadPoints 决定移除区域;回复保住自己的文字
标记一旦带上 /QuadPoints,ApplyRedactions 移除什么就由那些四边形决定,Rect 只负责显示时的外框。用文字选择工具做的标记通常每条选中行一个四边形,用 SetAnnotRect 改这种标记的大小,屏幕上的框动了、移除区域原地不动;真正挪区域的是 SetAnnotQuadPoints,而 GetAnnotQuadCount 对只靠 Rect 的标记返回 0。标记上的文字走另一条规则。审阅工具把备注存在 /Contents、作者存在 /T,涂黑标记上的备注往往就在描述要移除的东西(“客户 SSN,找法务确认”)。ApplyRedactions 清空标记自己的字典,但经 /IRT 挂在它下面的回复是独立的注解,只有矩形与涂黑区域重叠时才会被删,所以停在页边空白里的回复保得住文字。下面的清单把每个标记连同其四边形数、以及挂在下面的回复一并打印
procedure ListPendingMarks(Pdf: TPDFlib);
var
P, I, Parent: Integer;
begin
for P := 1 to Pdf.PageCount do
begin
if Pdf.SelectPage(P) <> 1 then
Continue;
for I := 1 to Pdf.AnnotationCount do
if Pdf.GetAnnotType(I) = 'Redact' then
Writeln(Format('p%d #%d by %s, quads=%d: %s',
[P, I, Pdf.GetAnnotTitle(I), Pdf.GetAnnotQuadCount(I),
Pdf.GetAnnotContentsEx(I)]))
else
begin
Parent := Pdf.GetAnnotReplyTo(I); // 无 /IRT 时为 0
if (Parent > 0) and (Pdf.GetAnnotType(Parent) = 'Redact') then
Writeln(Format(' reply #%d to #%d: %s',
[I, Parent, Pdf.GetAnnotContentsEx(I)]));
end;
end;
end;
ApplyRedactions 之后出现的是哪个覆盖层?
/RO 条目只要在就赢,表 192 的优先级规则就是这么要求的。ApplyRedactions 把那个 Form XObject 安装为注解的常规外观(/AP /N),丢掉一切外观状态并压平,于是 /IC、/OverlayText、/DA 和 /Q 对这个标记全部失效。摆放是唯一微妙的地方。规范说覆盖层的原点对齐 Rect 的左下角,而压平把 Form 的 BBox 经其 Matrix 映射到 Rect 上——正是压平施加给表单字段 widget 的那套 BBox 与 Matrix 摆放。BBox 为 [0 0 w h]、尺寸对齐矩形、Matrix 为单位的覆盖层,两种读法下看起来一样,所以自己的工具生成 /RO 标记时就按这个形状来
没有 /RO 时,ApplyRedactions 把字典归一化、自己构建外观。它删掉 /C 和 /BS,把 /Border 设为 [0 0 0]:这些条目描述的是待定标记——通常一圈彩边——烧进成品页会让已完成的涂黑看起来没做完。边框没了,填充盖满整个矩形。标签画在生成的 Form 里,字号与颜色取自 /DA,对齐取自 /Q,平铺取自 /Repeat,字体名 F0 在该 Form 自己的资源里绑定到标准 Helvetica。跨工具传文件时这个绑定就有所谓了:ISO 32000-1 §12.7.3.3 把 DA 字体名定义为交互式表单默认资源(/DR)里的条目,没有表单字段的文档没有它,另一个应用应用同一个标记时可能换上兜底字体。PDFlibPas 跳过这道查找,用 Helvetica 画标签,适合 REDACTED 或豁免代码这类拉丁文字
缺失的 /IC 变成黑盒子
规范对缺席 /IC 的默认是透明内部,PDFlibPas 有意偏离它。移除之后,透明内部露出纸色——名字原来待的地方成了白缺口,而这个缺口与本来就没东西的版面看上去毫无区别。读者分不清涂黑与空白,审阅者不比对提取出的文本就无法确认涂黑发生过。空心框——应用步骤压平只有轮廓的标记的待定外观时的产物——则以另一种方式失败:它读起来像一份没人处理过的提案。我们掂过照字面跟随默认的选项,最终选了黑盒子:可见的“此处隐去”信号本来就是涂黑文档要告诉读者的一部分,而且这个选择只影响作者从未声明填充色的标记。字典里已有的 /IC 无论什么值都照用,要求白色的产出方拿到的就是白色
覆盖层画上去之前,什么已经被清走
ApplyRedactions 逐页工作,先把所有移除做完再压平任何覆盖层,所以覆盖层永远不会被自己的区域逮住。对每页,它按每个四边形收一个矩形、没有四边形的标记取 Rect,然后逐个矩形擦洗每一层内容:
- 文字逐字形切除,度量依据页面字体与完整文本状态,部分被盖的字符串其余部分停在原位
- 画进区域内部的矢量路径、行内图像和着色从内容流里删除
- Form XObject 递归进入;别的页面或位置也绘制的 Form 经私有副本涂黑,那些页面保住原件
- 图像 XObject 只丢被盖的像素,以黑覆盖(CMYK 用满 K 墨量);JPEG、JPEG 2000、JBIG2 和 CCITT 图像解码后以无损 Flate 流存回
- 标签化 PDF 里,被移除的标记内容所附的
/Alt与/ActualText在页面内容和结构树里都清掉 - 矩形碰到区域的其他注解被删除,widget 落在区域里的表单字段失去值
像素这一步就是规范的图像条款落到实处的样子。在扫描护照上画一个黑矩形,眼睛满意了,§12.5.6.23 照样不过——只要有人提取图像,原始样本就解出来了。RedactRegion——直接从你传入的矩形移除内容、与 SanitizeDocument 搭配背后是同一台引擎,不读任何注解。内容清走之后,每个标记被压平,字典清到只剩 /Type 和 /Subtype。压平把注解从页面的 /Annots 数组里摘掉,但字典仍是文档里的一个对象,会把 /Contents、/RC、/T 和其余审阅痕迹一并交给接收方
保存时不能留下未涂黑的修订版
增量保存会把上面的一切推倒。按 §7.5.6(ISO 32000-1 与 ISO 32000-2 同号),增量更新把改动的对象追加到文件末尾、原始字节原封不动,谁在更早的 %%EOF 处把文件截断,就能读到带页面内容、图像和已注记标记的原始修订版;PDFlibPas Small Business 版的 ExtractDocumentRevision 一次调用就能做到。ApplyRedactions 之后,用 SaveToFile 写新文件,绝不用会追加的 SaveChanges。保存之前,GarbageCollectObjects 扫一遍对象表、丢掉不再被引用的对象,比如应用步骤之前标记携带的外观流;完整保存默认跳过这道清扫,除非 SetGarbageCollect 打开了它,或 GarbageCollect 字段已设置——该字段属于那条 TPDFlibSaveOptions 记录,也就是你传给 SaveToFileOptions 的参数。元数据是剩下的缺口。SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET) 清空 Author、Subject、Keywords、Creator 和 Producer 并移除 XMP 流,Title 留给你自己核查。这里避开 SANITIZE_ALL:它包含 SANITIZE_ANNOTATIONS,会删掉文档里剩下的每个注解——你本想保留的备注也在其中
ApplyRedactions 与 RedactRegion 一样,在 Enterprise 版提供。普通 TPDFlib 上它返回 0 并把 LastErrorCode 设为 PDFLIB_ERROR_ENTERPRISE_REQUIRED(900),所以发布环节改用 TPDFlibEnterprise——它来自 PDFlibEnterprise 单元
uses
System.SysUtils, PDFlibrary, PDFlibEnterprise;
procedure ApplyAndRelease(const ReviewFile, ReleaseFile: string);
var
Pdf: TPDFlibEnterprise;
Applied, Orphans: Integer;
begin
Pdf := TPDFlibEnterprise.Create;
try
if Pdf.LoadFromFile(ReviewFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [ReviewFile]);
Applied := Pdf.ApplyRedactions; // 每页的每个 Redact 标记
if Applied = 0 then
raise Exception.CreateFmt('No marks applied (error %d)',
[Pdf.LastErrorCode]);
// Info 和 XMP 还能点名页面不再显示的内容
Pdf.SanitizeDocument(SANITIZE_METADATA or SANITIZE_XMP_PACKET);
Orphans := Pdf.GarbageCollectObjects;
// 完整重写到新文件;涂黑之后绝不用 SaveChanges
if Pdf.SaveToFile(ReleaseFile) <> 1 then
raise Exception.Create('Save failed');
Writeln(Format('%d marks applied, %d unreferenced objects dropped',
[Applied, Orphans]));
finally
Pdf.Free;
end;
end;
预期:ApplyRedactions 返回它在所有页面压平的 Redact 标记数——对 MarkForReview 写出的审阅副本是 2——GarbageCollectObjects 返回清扫丢掉了多少未引用对象
无论哪个工具画的标记,/RO 与 /IC 条目决定接收方在页面上看到什么,而 PDFlibPas Enterprise for Delphi 里的应用步骤决定底下什么被清走了