沒畫 /IC 色彩的遮蔽標記,走完套用步驟後可能變成一塊透明空隙或一個空心外框,而審閱者對它下的註解還留在檔案裡。PDFlibPas 以 ApplyRedactions 套用 Redact 註解:被覆蓋的內容先移除,覆蓋物取自 /RO,或取自 /IC 並以黑色兜底,標記自己的字典則清空
標記可逆,套用不可逆
ISO 32000-1 §12.5.6.23 把遮蔽拆成內容識別與內容移除。識別階段的 Redact 註解只是個提案,使用者還能看見、移動、重新定義,審閱輪因此才可能。移除則交給應用程式,要求很硬:註解指認的東西全部消失,註解本身消失,任何影像被覆蓋的部分資料必須銷毀——蓋在像素上的剪裁路徑或影像遮罩都不算數。各鍵列在 ISO 32000-1 的 Table 192(ISO 32000-2 為 Table 195),其中多半描述的是移除之後頁面該長什麼樣,而不是該移除什麼
| 鍵 | 規範中的角色 | ApplyRedactions 對它做什麼 |
|---|---|---|
QuadPoints | 以 n 個四邊形指定的移除區域;存在時取代 Rect 成為區域 | 分別擦除每個四邊形的邊界框;長度不是 8 的倍數的陣列退回 Rect |
Rect | QuadPoints 缺席時的移除區域 | 使用時先把反轉的角點正規化 |
IC | 移除後的 DeviceRGB 填色;缺席代表透明;RO 存在時被忽略 | 存在時保留,缺席時補上黑色 |
RO | Form XObject 覆蓋物;優先於 IC、OverlayText、DA 與 Q | 安裝為正常外觀並展平到頁面上 |
OverlayText、Repeat | 畫在區域上的標籤,可平鋪 | 畫進產生的覆蓋物,Repeat 為 true 時平鋪 |
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,而只靠 Rect 的標記,GetAnnotQuadCount 會回傳 0。掛在標記上的文字走另一套規則。審閱工具把備註放在 /Contents、作者放在 /T,而遮蔽標記上的備註往往正在描述被移除的那件事(「client SSN, confirm with legal」)。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 條目就贏,Table 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 打開它,或你傳給 SaveToFileOptions 的 TPDFlibSaveOptions 記錄設了 GarbageCollect 欄位。中繼資料是最後的缺口。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),所以發布這一步用的是 PDFlibEnterprise 單元裡的 TPDFlibEnterprise
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;
Expected:ApplyRedactions 回傳它在所有頁面展平的 Redact 標記數——MarkForReview 寫出的審閱副本是 2——GarbageCollectObjects 回傳清掃丟掉了多少無人參照的物件
不論標記是哪個工具畫的,它的 /RO 與 /IC 條目決定收件人在頁面上看到什麼,而 PDFlibPas Enterprise for Delphi 的套用步驟決定底下少了什麼