在 Delphi 中進行真實的 PDF 徹底遮蔽意味著刪除底層的位元組,而不是在上面塗抹。losLab PDF Library 透過兩個 API 來界定這條線:RedactRegion 編輯頁面內容資料流,使移除的文字、向量和影像在物理上完全消失;而 SanitizeDocument 則清除中介資料、指令碼和動作,因為被遮蔽的資訊也可能隱藏在這些地方。如果這兩個環節中任何一個出錯,您發送的檔案雖然在螢幕上看起來被審查遮蔽了,但只要有人複製文字或開啟「文件屬性」,機密資訊就會暴露無遺
這種失敗模式在公開記錄中屢見不鮮。報社、法院和政府機構都曾發布過在姓名或數字上覆蓋黑色矩形的 PDF,然而讀者只需選取下方的文字,複製並貼上,就能讓原本應該被遮蔽的字串完全顯現。該矩形只是個繪圖物件,而文字依然存在,它完好無損且完全可選取地保留在塗層下方一層的內容資料流中。losLab PDF Library 作為透過 TPDFlib 類別公開的 Delphi 和 C++Builder PDF 引擎,將這種區別視為徹底遮蔽的核心意義,而非事後補救
為什麼在 PDF 文字上繪製黑盒子不是真正的徹底遮蔽?
losLab PDF Library 為您真正需要遮蓋的情況(例如浮水印、視覺黑屏、防偽印章)提供了 MaskRect。MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) 會將一個矩形附加到頁面內容資料流中,並使用 re 路徑建構運算子以及隨後的 f 填滿運算子(ISO 32000-1 §8.5)進行繪製。頁面上原有的每個位元組——您覆蓋的姓名外觀面(glyphs)、向量筆劃、內嵌影像——都精確地保留在原處,也就是在同一個資料流中前一條指令的位置。符合規範的檢視器最後才會繪製黑盒子,因此它在視覺上勝出。這種覆蓋沒有改變下方的任何內容
任何人都可以透過三種方式擷取原始內容:直接穿過不透明的盒子選取並複製文字、在頁面上執行文字擷取程式,或者從內容資料流中刪除覆蓋的矩形。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 點盒子內的所有內容。
// 座標為左上角,以當前原點和單位表示。
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 中介資料流通常攜帶 Author、Subject、Keywords、Creator 或 Producer 字串,這些字串命名了您剛從本文中移除的個人或案例。文件級的 JavaScript 和目錄 OpenAction 可以在開啟文件時執行,並存取您認為已經消失的資料。每個註解都帶有選用的 /A 動作,且註解本身(便利貼、過期的表單欄位、連結)可以直接放在被遮蔽的區域上方,並包含其專屬的文字複本。這些都不會受到加密的影響;一個帶有純文字 XMP 封包的加密檔案仍會向任何索引器洩漏其標題,這個漏洞本身就值得進行稽核加密與權限。防禦性的淨化必須訪問每一個隱藏之處,而不僅僅是您看到的頁面
使用 SanitizeDocument 清除文件
losLab PDF Library 透過 SanitizeDocument(Flags) 來應對這些洩漏,這是一個協同呼叫,它執行一組由位元遮罩選取的獨立清除動作,並傳回其套用的清除次數。這些旗標可以組合使用。SANITIZE_JAVASCRIPT 移除每個文件級的 JavaScript 套件和任何 OpenAction 指令碼。SANITIZE_ACTIONS 透過新的 TPDFAnnots.RemoveAnnotAction 從每個註解中清除 /A 動作字典,並清除目錄 OpenAction。SANITIZE_METADATA 清空文件資訊字典(ISO 32000-1 §14.3.3)和 XMP 中介資料流(§14.3.2)中的 Author、Subject、Keywords、Creator 和 Producer。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,並附有完整的內容模型和淨化參考手冊