使用 Microsoft Word 或 Excel 的「另存為 PDF」匯出文件後,磁碟上的檔案通常是混合參照檔案。它帶有兩份交叉參照資訊:一份是直到 1.4 版每個 PDF 結尾都使用的傳統固定寬度表格,另一份則是文件大部分內容實際依賴的壓縮交叉參照串流。單一的 trailer 索引鍵 /XRefStm 將這兩種檢視串接起來;工具能否看見完整文件,取決於它是否遵循該索引鍵
本文從取用端探討混合檔案:檔案末端的位元組看起來如何、兩種檢視如何在編輯後逐漸失去同步,以及 Delphi 管線如何偵測並路由混合輸入。載入器如何合併兩種檢視,以及為何順序不可更動,是我們關於載入混合參照檔案的 HotPDF 文章所探討的主題;本文則先說明如何辨識這種版面配置
為何 Office 匯出會寫入兩份索引
PDF 1.5 引入了兩項改變檔案形態的功能:交叉參照串流以壓縮二進位資料取代純文字表格來儲存物件索引,以及物件串流將許多小型物件打包到一個以 Flate 壓縮的容器中。使用這些功能的寫入器可產生較小的檔案,但 PDF 1.4 閱讀器無法開啟結果,因為它所依據的結構,也就是 xref 關鍵字與 trailer 字典,已不復存在
ISO 32000-1 §7.5.8.4 定義了折衷方案。混合參照檔案會同時寫入兩者:傳統交叉參照表定址舊版閱讀器必須取得的物件,其中包括 catalog 與頁面樹;交叉參照串流則為其餘所有物件建立索引。被收納到物件串流中的物件會在傳統表格中標為 free,讓 1.4 閱讀器可略過它們而不會出錯;它們的實際位置只存在於串流中。傳統 trailer 接著會帶有 /XRefStm 索引鍵,用來保存該串流的位元組位移。舊版檢視器從不讀取此索引鍵,而是從表格檢視轉譯檔案。新式檢視器會遵循它,因此能看見完整文件。Word 和 Excel 多年來一直產生這種確切版面配置,因此混合檔案不是罕見的邊緣案例,而是商務管線接收檔案中的很大一部分
混合檔案尾端的樣貌
從位元組最容易理解這種版面配置。以下是小型混合檔案的尾端,位移已縮短;在實際的 Office 匯出檔中,/XRefStm 值通常是接近檔案結尾的一個大型位移。讀取順序是我們的 PDF 檔案結構概觀所述的從尾端開始走訪:尋找 %%EOF、讀取 startxref,再跳至表格
% ... 主體物件,包括 object streams,以及位於位元組 116 的
% 交叉參照串流(帶有 /Type /XRef 的串流物件)...
xref % 傳統區段:startxref 指向的內容
0 4
0000000000 65535 f % slot 0:free list 的開頭,一定存在
0000000017 00000 n % object 1:catalog,所有 reader 都看得到
0000000000 65535 f % object 2:標記為 free——位於 object stream 中
0000000000 65535 f % object 3:相同;只有串流檢視能定位它
trailer
<<
/Size 4
/Root 1 0 R
/XRefStm 116 % 交叉參照串流的位元組偏移
>>
startxref
7164 % 上方 'xref' 關鍵字的位元組偏移
%%EOF
這份傾印中的兩項細節承載了整個機制。第一,startxref 特意指向傳統區段:那是舊版閱讀器必須抵達的位址。交叉參照串流只能透過 trailer 字典內的 /XRefStm 索引鍵取得,因此從不查找該索引鍵的解析器永遠不會知道串流存在。第二,物件 2 和 3 是一種無害的假象。傳統表格將它們宣告為 free,但它們是真正位於壓縮容器內的物件;free 標記可避免 1.4 閱讀器因無法使用的項目而出錯。只信任傳統檢視的取用端會得出這份文件大部分內容不存在的結論
兩種檢視如何逐漸失去同步
剛由 Word 產生的混合檔案在內部是一致的:兩種檢視都在各自宣告的範圍內描述相同文件。問題始於檔案由只理解其中一種檢視的工具編輯時。以一個附加傳統樣式增量更新的蓋章工具為例:新物件、新的 xref 區段、指向前一區段的 /Prev 鏈結,以及新的 trailer。若該 trailer 捨棄 /XRefStm 索引鍵,串流檢視就會失去連結;若它沿用舊值,串流檢視仍描述編輯前的文件。無論哪一種情況,兩個索引現在都會對檔案內容產生歧見
產生的檔案會有明顯的失敗特徵:一種檢視可看見的物件,在另一種檢視中會遺失或過期。透過串流檢視解析的閱讀器會找到已更新物件的編輯前版本,或完全找不到附加物件的項目。使用表格檢視的閱讀器能看見編輯,卻失去只由串流定位之壓縮物件的蹤跡。在實務上,這會表現為表單欄位在一個檢視器中仍存在、在另一個檢視器中消失,蓋章程序似乎刪除了註解,或查找完全落在錯誤的物件上
這些檔案難以除錯的原因是 Adobe Acrobat 通常會毫無抱怨地開啟它們:當索引與位元組不一致時,它會掃描物件標頭並悄悄重建交叉參照資料,因此產生損壞檔案的人看不出任何問題。失敗會在稍後才浮現,當檔案進入嚴格的取用端、preflight 驗證器、簽署服務或封存擷取工作,這些系統信任宣告的結構,並回報遺失物件或交叉參照不符。「它在 Acrobat 中開啟沒問題」幾乎是每張混合檔案失同步問題單的開場白
使用純 Delphi 偵測混合檔案
分類輸入不需要 PDF 函式庫。/XRefStm 索引鍵只能出現在傳統 trailer 字典內,而作用中的 trailer 位於檔案最後幾 KB 之內,因為規範要求 %%EOF 出現在實體結尾附近。讀取受限的尾端視窗並搜尋它,已足以進行分流:
uses
System.SysUtils, System.Classes, System.StrUtils, System.Math;
function IsHybridReferencePdf(const FileName: string): Boolean;
const
TailWindow = 2048;
var
Stream: TFileStream;
Buf: TBytes;
Tail: string;
Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
Result := False;
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if Stream.Size < 48 then
Exit;
Len := Min(TailWindow, Integer(Stream.Size));
SetLength(Buf, Len);
Stream.Position := Stream.Size - Len;
Stream.ReadBuffer(Buf[0], Len);
finally
Stream.Free;
end;
// 涉及的所有關鍵字都是 7 位元 ASCII,因此逐位元組解碼是安全的
Tail := TEncoding.ANSI.GetString(Buf);
// 找到最後一個 'trailer' 關鍵字:如果是增量更新,
// 最新的 trailer 才是掌控檔案的那一個
TrailerPos := 0;
NextPos := Pos('trailer', Tail);
while NextPos > 0 do
begin
TrailerPos := NextPos;
NextPos := PosEx('trailer', Tail, NextPos + 1);
end;
if TrailerPos = 0 then
Exit; // 沒有傳統的 trailer:純 xref-stream 檔案,不是混合式
// 混合式 trailer 在 'trailer' 與 'startxref' 之間攜帶 /XRefStm
KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
StartXrefPos := PosEx('startxref', Tail, TrailerPos);
Result := (KeyPos > 0) and
((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;
三種結果對應三種版面配置。只有傳統表格的檔案具有 trailer,卻沒有 /XRefStm:False。完全採用交叉參照串流的檔案完全沒有 trailer 關鍵字,其 trailer 索引鍵位於串流字典中:同樣正確地得到 False,因為這種檔案是壓縮格式,不是混合格式。只有雙重索引的版面配置會傳回 True
在正式環境中,有兩項強化措施值得多寫幾行程式碼。解析 /XRefStm 後面的整數,定位至該位移,並確認該處確實有帶有 /Type /XRef 的串流物件;截斷的檔案可能帶有此索引鍵,但串流已不存在,這應歸入與健康混合檔不同的類別。另外,將視窗大小視為參數:2 KB 足以涵蓋一般 Office 輸出,但異常龐大的 trailer 字典可能將關鍵字推到範圍外;擴大視窗比誤將檔案宣告為傳統格式更好
在 Delphi 管線中路由混合檔案
偵測可讓您做出路由決策。對於只讀取、轉譯或驗證的檔案,使用可解析兩種檢視的載入器,然後驗證行為而非位元組。PDFium Component 會在載入期間解析 /XRefStm 鏈結,因此您的程式碼所見的物件表格已經合併,而我們關於驗證物件與交叉參照串流的文章所述檢查可原封不動地套用。若失同步的混合檔損壞嚴重到拒絕載入,引擎會透過其錯誤集回報,包含 FPDF_ERR_SUCCESS、FPDF_ERR_UNKNOWN、FPDF_ERR_FILE、FPDF_ERR_FORMAT、FPDF_ERR_PASSWORD、FPDF_ERR_SECURITY 與 FPDF_ERR_PAGE,其中結構性損壞會產生 FPDF_ERR_FORMAT。不過,請勿過度依賴這個訊號:PDFium 的設計本就寬容,會無聲地重建大多數不一致檔案,所以載入成功只能證明檔案可復原,不能證明其兩種檢視一致。有意義的一致性檢查,是將完整物件走訪找到的內容與 trailer 的 /Size 宣告進行比較
對於管線會修改的檔案,最安全的政策是完全避免它們維持混合狀態。透過 HotPDF 載入後再完整儲存,會以單一、自洽形式的一份交叉參照重寫文件:沒有 /XRefStm、沒有第二種可能失去同步的檢視,每個物件都恰好由一個索引項目擁有。這種正規化正是您在封存擷取之前、嚴格的下游 RIP 或簽署服務之前,以及對混合輸入套用任何編輯之後所需要的。它之所以有效,是因為載入器在輸入時正確地合併了兩種檢視,這正是 HotPDF 混合參照文章詳細說明的機制
唯一應保持不動的檔案類別是數位簽署文件。完整重寫會移動每個位元組,因而使任何根據原始範圍計算的簽章失效。對已簽署混合檔的變更,必須以維持兩種檢視的正確增量更新寫入;只需要讀取的檔案應原封不動地傳遞。正規化適用於您擁有的檔案;已簽署檔案則只能附加內容
混合參照 PDF 並非格式不良;它們是格式本身提供的相容性橋梁,只要安裝基礎中仍有 PDF 1.4 閱讀器,Office 應用程式就會持續產生它們。能辨識 /XRefStm 索引鍵、使用 PDFium Component 驗證合併後文件,並使用 HotPDF Delphi Component 重新產生乾淨單一索引輸出的管線,會將它們視為其本質:trailer 中多了一個指示標記的普通輸入