PDF/A 與 PDF/UA 回答的是兩個彼此毫不相干的問題,而把它們當成同一個「無障礙兼封存」的勾選方塊,正是壞掉的檔案得以掛著合規標籤走進封存庫的原因。PDF/A 問的是這份檔案二十年後是否仍會忠實算繪。PDF/UA 問的是輔助科技今天讀不讀得懂它。一份文件可以徹底通過其中一項而敗在另一項,所以唯一誠實的裁決來自兩者都跑一遍,而且是在檔案被寫出去之前跑,不是等下游系統去信任它中繼資料裡烤進去的那個一致性識別碼之後才跑。那個識別碼是一項自我宣告。格式裡沒有任何東西要求它為真,而一個未經標準驗證就把「PDF/A-1b」寫進 XMP 的應用程式,產出的檔案在每一個只讀標籤的消費端眼裡都看似合規。losLab PDF Library(PDF Library for Delphi)為 Delphi 與 C++Builder 補上這道缺口,做法是把兩套驗證器都建進函式庫裡,於是檢查在行程內執行,不必架設任何外部服務
兩套為相反理由而讓檔案不合格的標準
ISO 19005(PDF/A)是一紙重現契約。一份合規檔案必須在幾十年後,於從未見過產生它那台系統的軟體上算繪得一模一樣,所以規則攻擊的是外部相依:字型全部嵌入、色彩錨定到內嵌的 ICC OutputIntent 或以裝置無關的色彩空間表達、PDF/A-1 中不得加密、不得有 JavaScript、XMP 中繼資料要與文件資訊字典一致。ISO 14289(PDF/UA)則是一紙語意契約。輔助科技必須走訪整份文件並帶走意義,而意義住在一個截然不同的層次:完整的結構樹、圖片上的替代文字、為顯示而設定的文件標題、不跳級的標題層級,以及在頁面離開螢幕後仍存活下來的表格標頭關係
正因為這兩套標準管的是不同層次,會咬人的是那些卡在兩者之間的檔案。一份封存意義上完美的文件,對螢幕閱讀器可能一片寂靜。一份標籤打得漂漂亮亮的文件,可能參照到一套十年後不會存在的桌面字型。公部門出版通常就是這兩項要求同時落地的地方,而那裡的管線不能把它們塌縮成單一道關卡。檢出項要送給不同的人。未嵌入的字型是產生 PDF 那段程式碼的缺陷,而缺少的替代文字則屬於內容範本的所有者,一份把兩者混在一起的報告,只會被轉寄兩次
您瞄準 PDF/A 的哪一部分,跟您有沒有命中一樣要緊。PDF/A-1 凍結在 PDF 1.4,並拒收透明度與 JPEG2000,而現代報表輸出想都不想就會用上這兩樣。PDF/A-2(ISO 19005-2,建立在 ISO 32000-1 之上)兩者都收,是新建封存庫合理的預設值。PDF/A-3 走得更遠,允許任何型別的內嵌檔案,受管制的電子發票格式靠的就是這一點。一支在 2026 年還把 PDF/A-1b 定為標準的團隊,多半是背著某人十五年前寫下的一條要求,而重新協商目標部分,往往比把系統產出的每一張圖表裡的透明度都剝掉來得便宜
進料當下就取得結構化的檢出項
平面 API 的入口是 CheckFileCompliance,測試選擇器 1 代表 PDF/A,2 代表 PDF/UA。它交回一個字串清單控制代碼,其中每一個項目就是一條獨立的檢出項、一行一條,而這正是自動化關卡想走訪的形狀:
function GateArchiveUpload(Pdf: TPDFlib; const FileName: string): Boolean;
var
ListId, I: Integer;
begin
ListId := Pdf.CheckFileCompliance(FileName, '', 1, 0); // 1 = PDF/A
if ListId = 0 then
begin
// 0 代表「沒有檢出項」或「檔案讀不了」-- 放行前先分辨清楚
Result := Pdf.LastErrorCode = 0;
Exit;
end;
for I := 0 to Pdf.GetStringListCount(ListId) - 1 do
LogFinding(FileName, Pdf.GetStringListItem(ListId, I));
Pdf.ReleaseStringList(ListId);
Result := False;
end;
有兩個細節決定這段東西能不能無人看顧地跑。第一個是一個意思相反的傳回值。CheckFileCompliance 在檔案完全合規時傳回 0,在檔案根本開不起來時也傳回 0,因為內部這兩種情況下空的結果清單都塌縮成 0。一個把 0 讀成通過的關卡,會把損壞的上傳檔直接揮進封存庫,所以在您信任那個零之前,先用 LastErrorCode 分辨清楚,就像上面那道關卡所做的。第二個關乎檔案處在它生命週期的哪個位置。檢查器跑在函式庫的串流讀取器上,而不是完整的文件模型上,它以讀取共用直接開啟檔案,從不呼叫 LoadFromFile,這也就是為什麼它能嚼得動數 GB 的輸入卻不必建出物件樹。同樣這種串流開啟,在另一個行程仍為寫入而握著檔案時會失敗,而一次進行中的上傳恰恰就是那個狀態。請在傳輸完成之後才設關卡
串流設計在高負載下再度得到回報。每次檢查都以唯讀方式開啟輸入並共用它供讀取,所以一次語料稽核可以橫向擴展到多個工作執行緒或行程上,每個工作者一份 TPDFlib 實例,彼此之間毫無爭用。真正需要紀律的資源是控制代碼本身。CheckFileCompliance 每一個非零的結果都會一直配置著,直到您呼叫 ReleaseStringList 為止,而一個長期執行卻忘了釋放的關卡不會當掉,它只會緩慢地滲漏記憶體,直到有人動身去查為什麼
給人看的報告,給建置關卡看的差異
一份檢出項清單對關卡來說形狀正好,對一封要寄給範本團隊的信來說形狀就錯了。CreatePreflightReport 把同一份分析算繪成可讀的文字,CreatePreflightReportEx 加上報告格式選擇器,而 SavePreflightReport 把它寫到磁碟,好讓報告能跟著交付的文件包一起走。不少封存契約會把那份報告本身列為交付物,而不只是內部產物
這一家子裡默默掙得自己位置的成員是 ComparePreflightReports。合規跟其他任何一種行為一樣,是一個會迴歸的面。一次範本微調、一套新授權的企業字型,或一次函式庫升級,都可能引進一條上一版還不存在的檢出項,而且沒有一個會自己出聲。把一組具代表性文件的黃金報告納入版本控制,每次變更後重新產生,然後跑 ComparePreflightReports 算出差異。一份空的差異是值得留存的發行產物。一條意料之外的檢出項會讓建置失敗,而那是個比稽核當下便宜太多的發現地點
產生第一次就通過的輸出
預檢在處理從外部送來的檔案時最能掙到它的飯錢。對於您自己程式碼產出的文件,等產生完才找違規再回頭修補,是繞遠路的做法。PDF Library for Delphi 為每一套標準各帶了一個產生端的模式,而且您可以對同一份文件把兩者都打開:
var
Pdf: TPDFlib;
Diag: WideString;
begin
Pdf := TPDFlib.Create;
try
Pdf.NewDocument;
Pdf.SetPDFAMode(1);
Pdf.LoadOutputIntentProfile('sRGB-IEC61966-2.1.icc', 'RGB');
Pdf.SetPDFUAMode('en-US');
Pdf.SetInformation(1, 'Quarterly Statement'); // /Title:PDF/UA 必備
// ... 在這裡繪製加了標籤的內容 ...
Diag := Pdf.GetPDFUADiagnostics;
if Diag <> '' then
Writeln('fix before shipping: ', Diag);
Pdf.SaveToFile('statement.pdf');
// 真正算數的預檢跑在存好的檔案上:
Writeln(Pdf.CreatePreflightReport('statement.pdf', '', 1, 0));
finally
Pdf.Free;
end;
end;
陷阱藏在存檔的時刻。好幾項合規修正是在文件被序列化的當下發生的,而不是在您啟用該模式的時候:強制打開註釋上的列印旗標、為 PDF/A-3 的內嵌檔案寫入預設的 AFRelationship、為 PDF/UA 正規化定位順序與表單欄位描述。躺在記憶體裡的那份文件,跟落到磁碟上的那份並不是位元組相同的,所以唯一有意義的預檢裁決,是從已存檔的檔案算出來的那一個。請驗證 statement.pdf 本身。別從還在記憶體裡的那個物件推論合規性,因為您所評判的位元組並不是您出貨的位元組
在視覺文件旁邊夾帶機器可讀 XML 的發票情境,走的是 ZUGFeRD 與 Factur-X 的模式,而它建立在 PDF/A-3 之上。這些情境應該用 SetPDFA3DefaultAFRelationship 明確設定附件關係,因為 ISO 19005-3 要求每一個內嵌檔案都必須宣告自己相對於文件的角色。放著不設,內嵌的 XML 就只是一團沒有言明用途的資料,而驗證器會注意到這件事
獨立的裁判:veraPDF 與 Acrobat
生產者不該是自己輸出的唯一評判者。PDF Library for Delphi 的檢查器在行程內給您快速且結構化的裁決,這正是您在熱路徑上要的東西,但一批封存輸出的發行關卡,仍然應該讓輸出通過一套不是您團隊寫的驗證器。veraPDF 是 PDF/A 由社群維護的參考實作,也是多數封存庫在驗收準則裡點名的工具,所以它就是要對上的那一個。當 veraPDF 與行程內檢查意見不合時,Acrobat 的預檢設定檔是個好用的打破僵局者。請在每一份存下的報告旁邊記下驗證器名稱與它的版本。一句「這份檔案通過了 veraPDF」,少了讓它通過的那個組建編號就幾乎什麼都沒說,因為這套工具在各版本之間會把規則收緊
驗證器確實會在標準的邊緣地帶意見不合,而當它們不合時,答案不是挑你順眼的那套工具。把檔案縮到一個仍會觸發歧異的最小樣本,再對照標準原文去讀它。這樣花一小時,通常會浮出兩件事之一:一個值得往上游回報的真實工具臭蟲,或是一條您團隊一直讀錯、而且應該寫進合規筆記以免下一個人再吵一次的條文
加密過的輸入有捷徑可走。兩套檢查器都收一個密碼引數,但一份帶有加密字典的 PDF/A-1 檔案本來就已經不合規,因為 ISO 19005-1 直接禁止加密,所以一份加密的送件可以在任何更深入的分析跑起來之前就被退回。至於弄清楚一個加密字典實際上授予了什麼,那是它自己的一項工作,涵蓋於 PDF 加密與權限稽核
PDF/UA 的檢出項幾乎總能追溯回結構樹當初是怎麼撰寫出來的,而背後的標籤化技巧住在 在 Delphi 中建立標籤化 PDF 結構樹。同時要求數位簽章的封存庫,應該把這道關卡與 PAdES 簽署與驗證裡的工作流程搭在一起。完整的預檢 API 參考住在 losLab PDF Library for Delphi 產品頁