PDFlibPas 把內嵌檔案掛到某一頁、而不是整份文件,做法是在頁面字典裡寫入 /AF 陣列,酬載本身則照舊登記在文件的 EmbeddedFiles 名稱樹。ISO 32000-2 §14.13 描述的正是這道切分,也正是它讓檢視器答得出文件層附件答不了的問題:這份資料屬於哪一頁
它的使用情境比一般附件更特定。勘察報告的每一頁都帶著圖表背後的原始量測數列;掃描批次裡每一頁都留著產出其文字層的 OCR 結果;圖集裡每一張圖都帶著它算繪自的那份 CAD 擷取。這些情況若改用文件層附件清單,就是一堆把頁碼編進檔名的檔案,那是一種約定俗成,不是結構
一份酬載,兩處參照它的地方
結構上最重要的點是:頁面層關聯不會替任何東西造出第二份副本。檔案只內嵌一次,並且與文件層附件完全一樣地登記在 EmbeddedFiles 名稱樹裡,用的是同一套檔案規格機制。不同的只在參照與其關係鍵寫在哪裡:寫進頁面字典,而不是文件目錄(catalog)
由此有兩個後果。第一,只認得文件層附件的閱讀器仍然找得到酬載,因為它就在那種閱讀器會去找的名稱樹裡。第二,清掉頁面關聯解除的是繫結,不是檔案。ClearPageAssociatedFiles 把頁面與其關聯檔案分開,酬載仍可循名稱樹觸及,這是保守的行為:一個名義上是清除關聯的操作,不該無聲無息地銷毀文件其他部分可能參照的資料
這個函式有一個刻意收得很窄的成功條件,值得知道。只有當頁面真的帶過 /AF 鍵,它才回報成功。從未有過關聯的頁面回傳的是失敗,而不是一聲愉快的確認,呼叫端因此不會把一次什麼都沒做的呼叫誤當成清理完成
var
Lib: TPDFlib;
Idx, I: Integer;
begin
Lib := TPDFlib.Create(nil);
try
Lib.LoadFromFile('survey-report.pdf');
// 掛上第 3 頁圖表背後的那組量測數列
Idx := Lib.AddPageAssociatedFileFromFile(3,
'series-03.csv', // 磁碟上的檔案
'measurements.csv', // PDF 內的顯示名稱
'text/csv', // MIME 類型
'Raw measurement series for figure 3',
'Data'); // AFRelationship,ISO 32000-2 14.13
if Idx < 0 then
raise Exception.Create('page association refused');
for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
Writeln('page 3 associated file, embedded index ',
Lib.GetPageAssociatedFileEmbeddedIndex(3, I));
Lib.SaveToFile('survey-report-with-data.pdf');
finally
Lib.Free;
end;
end;
關係字串在實務上不是自由填寫的文字。ISO 32000-2 定義了一份詞彙表:Source、Data、Alternative、Supplement、EncryptedPayload、FormData、Schema 與 Unspecified,消費端會按它行事。Data 給圖表背後的數字,Source 給頁面由之產生的文件,Alternative 給等價的另一種呈現。就算您的管線裡目前沒有任何東西在讀它,也請從這份詞彙表裡挑,因為鏈上的下一個工具可能會
為什麼同一種查找需要 FollowRef 的兩個方向?
因為追蹤參照回答的是兩個不同的問題,程式碼必須知道自己問的是哪一個。會追蹤間接參照的鍵查找,回傳參照所指的那個物件;不追蹤的查找,回傳的是參照本身。兩者都正確,用錯那一個得到的是無聲的錯誤行為,不是錯誤訊息
讀取關聯檔案示範了第一個方向。要取得檔案規格 /EF 與 /F 鍵背後那個內嵌串流的物件編號,查找就不能追蹤,因為一追蹤,參照就被解析成串流物件本身,物件編號就沒了。這條規則可以推廣:任何需要的是物件身分、而不是物件內容的程式路徑,都必須拿原始參照
選用內容展示的是相反方向,而這一個花更多功夫才找出來。選用內容性質字典是以間接物件寫進目錄的,不追蹤就讀回來的程式碼拿到的是參照,不是字典。對那個值做的型別檢查於是失敗,順理成章的 fallback 分支(沒有設定就建一個)接著執行,把已經存在的設定整個蓋掉。什麼例外都沒丟。選用內容群組與圖層描述的那些圖層,就這樣默默失去它們的預設可見狀態
這一課可以推廣到這兩個案例之外。當一個查找可能回傳參照、也可能回傳物件,光禿禿的型別檢查不是錯誤處理:它是一個遲早會因為錯的理由被走到的分支。明確決定每個呼叫點需要的是什麼,能直接回答問題的公開 API(例如選用內容數量的性質)優先於伸手進 protected 存取子去撈目錄字典
// 文件層附件與頁面層關聯可以共存。同一個內嵌檔案
// 也可以在文件層被標記為關聯
if Lib.IsEmbeddedFileAssociated(0) = 0 then
Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');
Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files : ',
Lib.GetPageAssociatedFileCount(3));
// 清除解除的是頁面繫結,酬載仍留在名稱樹裡
if Lib.ClearPageAssociatedFiles(3) > 0 then
Writeln('page 3 associations removed, payloads still reachable');
符合性模式對附件做了什麼
封存類的設定檔會限制什麼東西可以內嵌,而且這道限制在進入點就執行,不是留到存檔的時候。PDF/A-1 完全禁止內嵌檔案;PDF/A-2 只允許內嵌 PDF/A 文件;PDF/A-3 則是那個對任意檔案類型打開內嵌大門的設定檔,混合式發票格式正是看上這一點才建在它上面
當前的符合性模式不允許時,PDFlibPas 當場在呼叫點拒絕這個附件,不是幾百個操作之後在輸出階段才拒。這是刻意選擇在錯誤最便宜的位置處理:呼叫點的拒絕會指名您正在加的那個檔案;存檔時的拒絕只會指名一份文件,留給您自己去算到底是四十個附件裡的哪一個惹的禍
這也是關聯檔案在電子發票領域如此常見的原因。混合式發票是一份給人讀的 PDF,上面掛著機器可讀的 XML 酬載、並以正確的關係標記,而容器設定檔與關係鍵都是規格的一部分,不是約定俗成。這套構法記錄在建立 Factur-X 與 ZUGFeRD 混合式發票,詮釋資料那一側則在PDF/A-3 XMP 延伸綱要
什麼時候該用每頁關聯、而不是每份文件?
當消費端需要知道資料屬於哪一頁的時候,而且只有在那個時候。文件層附件更簡單、受到更多檢視器支援,只要酬載描述的是整份文件(發票 XML、簽章清單、原始碼封存檔)就夠用。等到酬載真的以頁為範圍、頁的身分本身就是其意義的一部分時,再伸手拿頁面層關聯
支援度是實務上的限制。頁面層關聯檔案是 PDF 2.0 的構造,檢視器支援比文件層附件單薄。因為酬載無論如何都放在名稱樹裡,忽略頁面上 /AF 的檢視器照樣會在附件清單裡顯示該檔案,所以退化是優雅的。但如果頁面繫結對您的消費端是不可或缺、不只是好用的詮釋資料,請驗證您實際鎖定的閱讀器,不要用猜的
頁面層關聯檔案、文件層附件,以及治理兩者的封存設定檔關卡,都隨 PDFlibPas Delphi PDF library 出貨。如果您在檔案進來的路上還要順手修老檔,轉換 PDF/A 並修復詮釋資料裡的詮釋資料與符合性工程,正是決定這些附件路線一開始哪些用得上的那件事