您編寫了一個小型驗證器。它開啟 PDF、尋找結尾、找到 startxref、讀取偏移量,並預期落在關鍵字 xref 上,其下方有一個固定寬度的交叉參照表。它從該表中收集物件偏移量,然後向後掃描關鍵字 trailer 以了解 /Root 和 /Size。它在您產生用於測試的每個檔案上都運作得非常完美。然後,由目前版本的 Word 產生的檔案,或者由目標為 PDF 1.5 的函式庫產生的檔案送達,且驗證器宣告它已損壞。在偏移量指向的地方沒有 xref 關鍵字,任何地方都沒有 trailer 字典,且驗證器建立的物件表幾乎是空的。檔案是有效的。驗證器是透過十五年前的透鏡在讀取它。
這是針對經典佈局編寫的位元組級 PDF 檢查在現代檔案上失敗的最常見原因。它所相依的結構(純文字交叉參照表和 trailer 關鍵字)在 PDF 1.5 中變為選用的,且經常不存在。有兩個功能取代了它:交叉參照資料流與壓縮物件資料流。這兩者都在 ISO 32000-1 中有描述,且不知道它們的驗證器會將健康的檔案視為一堆遺失的物件。
PDF 1.5 對檔案結尾做出了哪些變更
ISO 32000-1 §7.5.8 定義了交叉參照資料流,而 §7.5.7 定義了 /ObjStm 類型的物件資料流。它們一起允許寫入器丟棄經典剖析器作為索引鍵的兩個結構。PDF 1.5 檔案在結尾可能完全沒有 xref 表。在它的位置,startxref 指向的物件是一個普通的資料流物件,其字典攜帶 /Type /XRef,且該資料流以緊湊的二進位形式保存交叉參照資料。也沒有 trailer 關鍵字,因為 trailer 現在是資料流本身的字典。經典剖析器尋找的鍵 /Root、/Size 和 /ID 存在於該字典內部。
第二個變更移動了物件本身。寫入器可以將許多小物件(頁面字典、註解字典、結構樹)打包到單個物件資料流中,並使用 Flate 壓縮整個容器,而不是在各自的位元組偏移量處寫入每個間接物件。個別物件在檔案中不再具有位元組偏移量。它們在壓縮的二進位大型物件 (blob) 中具有一個位置。掃描原始位元組以尋找 1 0 obj 的驗證器絕不可能找到它們,因為該文字僅在充氣(解壓縮)後才存在。對於經典剖析器,檔案的一半單純地消失了。
即使在壓縮檔案中,結尾鍵也是純文字
令人安心的部分是,讀取交叉參照資料流的結尾不需要充氣任何內容。資料流物件被寫入為字典,後面跟著 stream 關鍵字,然後是壓縮的位元組。字典是純文字。因此,當 startxref 指向交叉參照資料流時,緊接在物件編號之後的位元組看起來像是一個普通的字典,且 /Root、/Size 和 /ID 清晰地呈呈現那裡(在 stream 關鍵字和 Flate 資料開始之前)。
這意味著驗證器僅透過剖析資料流字典,就可以了解它最需要的三個事實(目錄在哪裡、檔案聲稱有多少個物件,以及檔案識別碼)。它不必解壓縮交叉參照資料,也不必解譯其中的二進位項目。擊敗天真剖析器的工作不是讀取結尾,而是尋找物件。這是兩個可以分開的問題,且解決第一個問題成本很低。
物件資料流:標頭,然後是 Flate 二進位大型物件
物件資料流是一個容器。其字典攜帶 /Type /ObjStm、給出內部打包物件數量的 /N 項目,以及給出充氣資料內第一個物件主體開始的位元組偏移量的 /First 項目。壓縮的承載資料在充氣後以一個由 /N 個整數對組成的小標頭開始。每個整數對都是一個物件編號以及該物件主體相對於 /First 的偏移量。標頭之後是物件主體本身(已串聯)。
一旦位元組充氣,展開它就是機械性的操作。您讀取字典以取得 /N 和 /First,使用 Flate 解碼器對資料流進行充氣,遍歷前導的 /N 個整數對以了解哪個物件編號位於哪個偏移量處,然後像提取普通的間接物件一樣將每個主體提存出來。唯一真正的相依性是 Flate 解碼器,且您已經擁有一個:Delphi 出貨了 System.ZLib,而 Free Pascal 出貨了 zstream 單元,這兩者都包裝了 zlib 並在沒有任何第三方程式碼的情況下對原始 Flate 資料流進行充氣。將每個提取的物件附加到驗證器物件表中的常式會使驗證器的其餘部分(遍歷 /Root 並檢查頁面樹的部分)的行為與其在經典檔案上的行為完全相同。
您不需要實現的內容
工作很容易被高估。從壓縮檔案中讀取結尾鍵不需要解碼交叉參照資料流的二進位項目。§7.5.8 交叉參照資料流使用三種項目類型,且類型 2 項目(即指出「此物件存在於物件資料流 N 的索引 i 處」的項目)是您解碼以建立完整偏移量地圖所需的內容。您需要該地圖來按編號解析任意物件。您不需要它來讀取 plaintext 字典中的 /Root、/Size 和 /ID,且您不需要它來展開物件資料流,因為每個 /ObjStm 都透過 /N 和 /First 宣告其自身的內容。
您也不必處理交叉參照資料流為了取得結尾鍵而可能透過其 /DecodeParms 套用的 PNG 和 TIFF 預測因子函式。預測因子過濾二進位交叉參照列以使其更好地壓縮;它們與資料流之前的字典無關。因此,使經典驗證器感知現代 PDF 的最小升級很小:當 startxref 落在資料流而不是 xref 關鍵字上時,針對結尾鍵剖析資料流字典,並展開您遇到的任何 /ObjStm 物件,以便其內容進入物件表。解碼類型 2 項目和預測因子是一個單獨、更大的任務,您可以推遲到您真正需要隨機物件解析時再進行。
為什麼合規性檢查必須先展開資料流
在您執行設定檔檢查的瞬間,這就不再是學術問題了。PDF/A 或 PDF/X 驗證器檢查特定的物件:檔案目錄中的 /OutputIntents 陣列、帶有正確識別碼之 XMP 封包的 /Metadata 資料流、每個用於嵌入式字型檔案的字型描述元、結尾的 /ID。在壓縮檔案中,大多數這些物件都在物件資料流內部。尚未展開物件資料流的驗證器無法看到目錄的鍵、找不到中介資料,也無法列舉字型。它會將完全符合規範的檔案報告為遺失其輸出目的、遺失其 XMP,且遺失其一半的結構,因為它所需要的證據仍存在於它從未充氣的 Flate 二進位大型物件中。
順序很重要。展開必須在檢查執行之前發生,而不是與檢查並行,因為每次檢查都假設它可以按編號存取物件。如果您將設定檔檢查直接連接到原始位元組掃描,它會逆量經典剖析器的盲目性,並在精確的現代檔案上產生錯誤的違規(這些檔案最可能是結構良好的,因為它們來自足夠新以致首先寫入交叉參照資料流的工具鏈)。
讓 PDFium 為您進行剖析
PDFium 元件將解析交叉參照資料流和物件資料流作為載入檔案的一部分,這是避免手動充氣和展開步驟的實用方式。當您使用 TPdf 元件載入檔案時,打包到 /ObjStm 容器中的物件已經被解析,且驗證進入點會看到完全展開的檔案。ValidatePdfA 傳回 TPdfAValidationResult 記錄,其 Conformance 欄位是諸如 pac1b 或 pacNone 之類的 TPdfAConformance 值,其 Issues 欄位是找到的特定問題的集合,且其 IsCompliant 方法僅在偵測到相容性層級且問題集合為空時才為 true。因為物件在載入期間已被展開,所以會找到存在於物件資料流內部的 /OutputIntents 陣列或嵌入式字型,而不是報告遺失。
uses
PDFium, FPdfPdfa;
function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
這同樣適用於 ValidatePdfX,它傳回具有相同形狀的 TPdfXValidationResult。透過 PDFium 路由的意義在於,上述結構解壓縮在載入器內部逆量地進行了一次,因此您的驗證程式碼絕不會看到經典檔案與完全壓縮檔案之間的區別。兩者都作為已解析的物件集合到達驗證器。
var
Pdf: TPdf;
R : TPdfXValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Press_Ready.pdf';
Pdf.Active := True;
R := Pdf.ValidatePdfX;
if R.IsCompliant then
Writeln('PDF/X conformance: ', Ord(R.Conformance))
else
Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
finally
Pdf.Free;
end;
end;
如果位元組已經在記憶體中而不是在磁碟上,相同的「載入後驗證」序列可以透過 LoadDocument(const Data: TBytes) 多載來運作,該多載接受原始檔案內容,並以與檔案路徑相同的方式解析其交叉參照和物件資料流。手寫驗證器的要點是結構規則,而非 API:從純文字字典中的資料流字典讀取結尾鍵、在遍歷檔案之前使用 Flate 解碼器展開每個 /ObjStm,並將解碼二進位交叉參照項目視為更大、選用的工作。
一旦結構展開,驗證器就可以在其上驅動工作流程的其餘部分。對於報告整個輸入資料夾相容性的命令列預檢安全帶,請參閱我們關於建立批次預檢報告 CLI 的逐步解說。當驗證是拆分大檔案之前的閘口時,我們關於將 PDF 檔案拆分為多個檔案的指南中的技術自然會與此處顯示的「載入並檢查」模式配對。兩者都建立在適用於 Delphi 和 C++Builder 的 PDFium 元件 的載入和驗證介面之上。