技術文章

用 HotPDF 在 Delphi 中產生 PDF 2.0 PDF/A-4 與 PDF/UA-2

HotPDF 能從 Delphi 與 C++Builder 撰寫原生 PDF 2.0 文件,包含三種 PDF/A-4 封存設定檔,以及帶有命名空間結構元素的 PDF/UA-2 可存取輸出。選用它們只是兩個屬性的事,但這些屬性背後的標準變動,比版本號字面所暗示的更多:PDF/A-4 拋棄了大家在 PDF/A-2 時期熟習的一致性字母,而 PDF/UA-2 則引進了第 1 部分文件從未有過的結構命名空間

本文涵蓋產生檔案時實際變動了什麼,以及 HotPDF 把哪些失誤變成在 EndDoc 拋出例外,而不是變成一份在客戶那裡才驗證失敗的文件

PDF/A-4 的識別與第 2、第 3 部分有何不同

PDF/A-4 以部分編號加修訂年份來識別自身,基礎部分沒有一致性字母。把 PDFACompliance 設為 '4',HotPDF 會發出 pdfaid:part=4pdfaid:rev=2020,而且完全沒有 pdfaid:conformance 項目。這個字母並沒有遺失——第 4 部分沒有 A/B/U 等級,因為過去用來區分它們的要求,已經被摺進基礎部分裡

有兩個擴充保留了字母。'4E' 選擇工程文件用的 PDF/A-4e,並發出一致性 E,這允許其他設定檔禁止的 3D 與 RichMedia 註解路徑。'4F' 選擇 PDF/A-4f 並發出一致性 F,允許嵌入任何格式的檔案。三者都會強制使用 PDF 2.0 標頭、要求常規的 PDF/A 輸出意向與中繼資料檢查,並禁止加密——一份加密的封存檔案,是標準根本不予考慮的矛盾

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-archive.pdf';
    Pdf.PDFACompliance := '4F';   // PDF/A-4f: associated files of any format
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-0731');
    Pdf.AddPDFAssociatedFile('invoice.xml', 'text/xml',
      'Structured invoice data', 'Data', LoadInvoiceBytes);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

AddPDFAssociatedFile 會嵌入該檔案、用一個 /AFRelationship 建構它的 FileSpec,並同時登錄到 Catalog 的 /AF 陣列與 EmbeddedFiles 名稱樹裡。兩邊的登錄都是必需的;只列在其中一邊的檔案,是混合發票能通過快速目測、卻在真正的驗證器上失敗的單一最常見原因。關係字串接受 SourceDataAlternativeSupplementUnspecified,而作用中的設定檔必須是 PDF/A-3、PDF/A-4e 或 PDF/A-4f——基礎的第 4 部分設定檔並不允許關聯檔案。舊的 AddPDFA3AssociatedFile 名稱為了既有程式碼仍然有效

PDF/UA-2 要求了哪些 PDF/UA-1 沒要求的東西

PDF/UA-2 強制使用 PDF 2.0,並發出 pdfuaid:part=2pdfuaid:rev=2024,同時把命名空間引進結構樹。一份第 1 部分的文件只有一份平坦的標準角色詞彙表。一份第 2 部分的文件可以帶有自訂角色,只要每一個都屬於一個已宣告的命名空間——這正是讓特定領域的標記對輔助科技而言可讀、而非猜測的原因

兩個方法實作了這一點。RegisterStructureNamespace 會建立或重用一個間接的 /Type /Namespace 字典,並把它列在 StructTreeRoot /Namespaces 裡,同時回傳該字典讓你重用。AddStructureElementNS 建立一個結構元素,其 /NS 項目指向那個字典,而這正是讓一個標準集合之外的角色名稱得以合法的原因。對同一個 URI 的重複呼叫會重用同一個字典,而不是堆出一堆重複

var
  Root: THPDFDictionaryObject;
begin
  Pdf.PDFUACompliance := True;
  Pdf.PDFUAPart := 2;             // part 2 forces PDF 2.0
  Pdf.Lang := 'en-US';
  Pdf.BeginDoc;
  Root := Pdf.AddStructureElement('Document', nil);
  Pdf.AddStructureElementNS('WidgetGroup',
    'https://example.com/ns/widgets', Root);
  Pdf.EndDoc;
end;

這裡的 Lang 不是裝飾。一份沒有宣告自然語言的標記文件,會讓螢幕閱讀器只能猜測發音,而 PDF/UA 把這種省略視為缺陷,而不是偏好

EndDoc 會抓出哪些結構錯誤?

四個,而每一個都對應到一份否則會帶著破損送達驗證器的文件。結構根必須恰好包含一個最上層 Document 元素。每一個命名空間字典都必須是間接的、型別為 Namespace,並帶有唯一的非空 URI。每一個結構元素的 /NS 參照都必須解析到一個確實列在根 /Namespaces 陣列裡的字典。而一個未帶命名空間的角色,必須是 PDF 2.0 標準角色,或能透過 RoleMap 解析

這些檢查會在 EndDoc 觸發,因為那是整棵樹仍存在於記憶體中的最後一刻,也是它首度完整的時刻。更早抓它們,意味著拒絕有效的中間狀態;更晚抓,則意味著完全抓不到。對你的程式碼而言,實際後果是:一個結構臭蟲會在產生結束時帶著一條說明問題的訊息浮現,而不是幾週後才以某人從客戶那裡轉來的 veraPDF 報告形式浮現

值得知道的 PDF 2.0 角色

強型別角色列舉新增了 DocumentFragmentAsideTitleFENoteSubEmStrongArtifact。其中三個改變了你標記普通商業文件的方式。Aside 終於讓側邊欄與引述框有了一個不是被誤用之 Sect 的家。FENote 把腳註與尾註標記成它們本來的樣子,讓閱讀器能主動提供它們,而不是把它們與內文交錯朗讀。EmStrong 取代了過去把強調標記成 span 層級格式時所帶來的語意猜測

字串多載額外接受開放式的 Hn 形式,包含 H7 及更深。PDF 1.7 停在 H6,這迫使深度技術文件得壓平大綱或重複使用層級。如果你產生的是標準文件、法規或零件目錄,光是這一點,就可能是把輸出搬到 PDF 2.0 的理由

切換正式輸出前要先檢查什麼

PDF 2.0 是一次帶著長尾巴的標頭變更。較舊的封存匯入工具、某些印刷 RIP,以及為數不少的業務線閱讀器,只接受到 PDF 1.7,而且它們是卡在標頭上,而不是卡在你做錯的任何事情上。切換之前,先確認消費端系統,並且記住:選擇任何一個 PDF/A-4 設定檔,無論你願不願意,都會同時選擇 PDF 2.0

安全的順序是:對外送往不明讀者的文件,繼續使用 PDF/A-3;對你掌控匯入流程的內部封存,使用 PDF/A-4f;只有當可存取性政策明確點名時,才採用 PDF/UA-2。如果你打算先處理封存側,PDF/A、PDF/X 與 PDF/UA 驗證以及PDF/A-3 上的 ZUGFeRD 與 Factur-X 混合發票指南,涵蓋了比版本號更早決定勝負的設定檔選擇,而自動化預檢報告的筆記,則示範了如何把判定結果變成建置的一部分,而不是一個手動步驟

HotPDF 以 Delphi 與 C++Builder 的原生 VCL 程式碼,提供整個 PDF 2.0 撰寫面,所以 PDF/A-4 與 PDF/UA-2 輸出不需要任何外部引擎或可轉散發套件——支援的設定檔與 RAD Studio 版本請見 HotPDF 元件頁