ConvertToPDFA 只靠一次呼叫就把一份普通文件變成封存文件:它移除所選部分禁止的東西、加入該部分要求的東西、聲明文件所屬的部分,然後檢查結果。只有當檢查通過時,這項宣告才會被回報為符合,而 GetPDFAConversionReport 會列出做了什麼,以及還有什麼擋在路上
最後這個性質是值得多談一下的設計決定。一個不加檢查就蓋上宣告的轉換器,比沒有轉換器還糟,因為一份宣稱自己是封存文件卻不是的檔案,會直接穿過那些本來會抓到它的系統。失敗會在幾年後、在一次稽核中、出現在一份沒有人能重新產生的文件上
為什麼一份看起來有效的 PDF 會無法通過 PDF/A 檢查?
最常見的原因,是 PDF 用來聲明作者是誰的兩個地方彼此不一致。驗證器會同時讀取文件資訊字典與 XMP 封包,並拒絕兩者不同的檔案——而在這一點上失敗的檔案,大多數根本從未寫入過 XMP 那一半
RepairDocumentMetadata 會讓兩者趨於一致,並回傳它修復了多少個項目。當只有其中一半帶有值時,另一半會從它填入,所以已經記錄下來的東西不會被丟掉。沒有人需要決定哪一份複本是權威,因為在實務上,永遠有一份複本是空的
同一個呼叫裡有第二道修復,會抓到一個更微妙的情況。一份設定為 PDF/A 模式的文件,若遺失了標準識別就會被還原,而這種情況發生在呼叫端自行提供 XMP 封包時。少了這個識別,驗證器會把檔案當作普通 PDF 讀取,並把所宣告部分的每一條規則都回報為不符——一次看起來驚人的失敗,背後只有一個小小的原因
var
Lib: TPDFlib;
Repaired: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('incoming.pdf', '');
Repaired := Lib.RepairDocumentMetadata;
Log(Format('%d metadata entries brought into agreement', [Repaired]));
Lib.SaveToFile('incoming-fixed.pdf');
finally
Lib.Free;
end;
end;
轉換之前先選定部分
SetPDFAMode 與 ConvertToPDFA 共用同一套模式編號,其中三個值是近期才加入的。模式 9 是 PDF/A-4,這個部分建立在 PDF 2.0 之上。模式 10 是 PDF/A-4e,額外允許 3D 與豐富媒體;模式 11 是 PDF/A-4f,允許嵌入任何格式的檔案
第 4 部分自我識別的方式,與之前的幾個部分不同:用部分編號加上該部分發布的年份,純粹的 PDF/A-4 沒有一致性字母,而兩個擴充則是字母 E 或 F。檢查會辨識第 4 部分、依 PDF 2.0 而非 1.7 評判它的檔案,並對一份未聲明修訂年份的第 4 部分檔案提出回報
第 4 部分文件裡的每一個嵌入檔案,都必須聲明它與文件的關係,這一點第 3 部分與第 4 部分都要求。這正是過去會抓到普通附件的那條規則:關係只寫給第一個之後的附件、從不寫給最後一個,所以一份只有單一附件的文件——常見的情況——一個關係也沒帶,並且正是在這一點上驗證失敗
var
Verdict: Integer;
begin
Lib.LoadFromFile('report.pdf', '');
Verdict := Lib.ConvertToPDFA(9); // 9 = PDF/A-4, 10 = 4e, 11 = 4f
Memo1.Lines.Text := Lib.GetPDFAConversionReport;
if Verdict = 1 then
Lib.SaveToFile('report-pdfa4.pdf')
else
Log('conversion incomplete - see the report for what stands in the way');
end;
轉換報告的用途是什麼
用來決定接下來怎麼做。一次成功的轉換不需要報告;一次不成功的轉換,才是報告存在的全部理由。有些障礙可以被轉換器移除,有些則不行——加密、帶有意義而被禁的內容、一個機器上根本不存在的字型程式。報告會區分已完成的與尚未完成的,把「轉換失敗」變成一項工作項目
把判定結果當作批次管線裡的閘門。轉換、讀取判定、然後為檔案分流:把通過的歸檔,其餘的排入附帶報告的人工處理佇列。你不該做的,是把一次失敗轉換的輸出存進封存裡,只因為它看起來比輸入好看——它現在帶著一項檢查拒絕確認的宣告
讀取檔案已經帶著的標記
在轉換任何東西之前,先了解文件怎麼描述自己。一個無法讀取既有標準標記的 PDF/A 檢查,會把每一份檔案都當作第 1 部分來評判,無論它宣告了什麼,這意味著一份完全有效的 PDF/A-2 或 PDF/A-3 文件,會被回報為未帶標記且版本過高——與事實相反
無論產生器把這個標記寫成 XMP 元素還是屬性,都能被讀取。兩種形式都是普通的 XMP,只接受其中一種,會讓來自其他產生器的檔案看起來像沒帶標記。如果你曾納悶過為什麼一份在別處驗證通過的文件,在你自己的管線裡會失敗,這裡是一個很適合先檢查的地方
封存前先淨化,以及一個值得知道的臭蟲
封存轉換與淨化常常一起執行,因為安全政策想要移除的內容,與 PDF/A 禁止的內容高度重疊。SanitizeDocument 會移除 JavaScript,而移除最後一個指令碼時,也會一併移除它留下的空名稱樹——這棵樹若不移除,會繼續告訴閱讀器這份文件帶有指令碼
後半段是付出代價才學到的:套件清單裡的一個差一錯誤,代表淨化回報移除了指令碼、卻一個也沒移除,於是一份已淨化過的文件,在開啟時仍然執行了它的指令碼。這是支持整篇文章所立足的那條通用原則的好理由——驗證結果而不是信任操作,在你的管線裡如此,在函式庫裡亦然
關於周邊的封存工作,請見PDF/A 與 PDF/UA 預檢、真正的密文標註與內容移除,以及Factur-X 的 PDF/A-3 XMP 延伸綱要等逐步解說;最後這篇涵蓋了當封存文件同時帶有結構化發票資料時,中繼資料那一側的處理
PDFlibPas 是一套原生 Pascal PDF 函式庫,支援 Delphi、C++Builder 與 Lazarus,所以轉換、修復與驗證全部都在你自己的行程內發生,鏈中沒有任何外部工具——支援的 PDF/A 部分與平台請見 PDFlibPas 產品頁