您已建立好 Factur-X 發票,所有容器檢查都通過了。目錄帶有 /AF 陣列、EmbeddedFiles 名稱樹解析至正確的檔案規格、嵌入的 factur-x.xml 具有正確的 /AFRelationship 值 Alternative,而內建的 ValidateFacturXInvoice 也回傳 1。然後您將同一個檔案送入稅務入口所使用的參考檢查器 veraPDF,它卻判定整份文件不是有效的 PDF/A-3。結構是正確的,問題出在中繼資料上,而這個失敗是整個電子發票工作流程中最容易被忽略的失敗之一
這個原因值得完整理解,因為它解釋了一整類 PDF/A 缺陷 - - 這類缺陷與可見頁面或附件毫無關係,卻與 XMP 如何描述自身有一切關係。這就是隱藏在容器檢查通過背後的陷阱
讓檔案失敗的四個屬性
Factur-X 發票將四個自訂屬性寫入其 XMP 封包,以便下游軟體無需解析嵌入的 XML 即可讀取發票設定檔。它們位於 Factur-X 命名空間中,使用 fx 前綴:fx:DocumentFileName、fx:DocumentType、fx:Version 和 fx:ConformanceLevel。這四個屬性正是讀取器需要用來識別這份 PDF 帶有 1.0 版本、名為 factur-x.xml 的 EN 16931 發票的全部中繼資料
這四個屬性中沒有任何一個屬於 PDF/A 預先定義的 XMP 結構描述。都柏林核心、XMP Basic、PDF 以及 PDF/A 識別結構描述對符合規範的讀取器是已知的,但 fx: 並非如此。當 veraPDF 遍歷 XMP 並遇到它無法識別命名空間的屬性時,它會尋找能告知它該屬性含義的宣告。若該宣告不存在,它會根據 ISO 19005-3 第 6.6.2.3.1 條回報失敗 - - 該條要求每個非來自預定義結構描述的屬性都必須在 PDF/A 延伸結構描述中加以描述。四個未宣告的屬性,四種被拒絕的方式,而其中沒有任何一種在容器檢查中是可見的
為何 PDF/A 拒絕裸露的自訂屬性
這條規則看起來迂腐,直到您想起 PDF/A 的用途。這個格式之所以存在,是為了讓檔案能在數十年後、由從未被告知 2026 年慣例的軟體開啟並理解。符合規範的讀取器應當能夠單憑文件本身理解文件,而無需查詢任何外部登錄檔
自訂中繼資料打破了這個承諾,除非檔案本身攜帶其描述。面對一個裸露的 fx:ConformanceLevel 屬性,未來的讀取器無從得知 fx 前綴所綁定的命名空間 URI、該值是文字、日期還是整數,或者該屬性描述的是文件本身還是某個外部資源。PDF/A 延伸結構描述機制填補了這個空缺:它允許檔案在固定的 XMP 結構中宣告命名空間、前綴,以及每個屬性的值類型和 internal 或 external 類別。一旦該宣告存在,屬性就能自我描述,第 6.6.2.3.1 條便得到滿足。沒有它,驗證器別無選擇,只能將屬性視為無法理解而讓檔案失敗。這裡的類別區分很重要:像這些發票屬性描述的是來自 PDF 處理器外部的資料,因此宣告為 external 而非 internal
延伸結構描述宣告的內容
該宣告是 XMP 封包中的一個 rdf:Description,使用三個 AIIM 定義的命名空間:pdfaExtension、pdfaSchema 和 pdfaProperty。在 pdfaExtension:schemas bag 中有一個結構描述項目,用以命名 Factur-X 結構描述,提供其 pdfaSchema:namespaceURI 和 pdfaSchema:prefix,然後在 pdfaSchema:property 序列中列出四個屬性。每個屬性帶有名稱、值為 Text 的 pdfaProperty:valueType,以及值為 external 的 pdfaProperty:category。下方的示範標記展示了該區塊的結構
<rdf:Description rdf:about=""
xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#">
<pdfaExtension:schemas>
<rdf:Bag>
<rdf:li rdf:parseType="Resource">
<pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
<pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
<pdfaSchema:prefix>fx</pdfaSchema:prefix>
<pdfaSchema:property>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>DocumentFileName</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
</rdf:li>
<!-- DocumentType, Version, ConformanceLevel declared the same way -->
</rdf:Seq>
</pdfaSchema:property>
</rdf:li>
</rdf:Bag>
</pdfaExtension:schemas>
</rdf:Description>
命名空間 URI 和前綴並非固定字串,它們隨設定檔而定。Factur-X 文件使用 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# 加上 fx 前綴,而透過 zugferd-invoice.xml 選定的 ZUGFeRD 2.0 檔案則在其自身的結構描述名稱下解析為不同的 URI。延伸結構描述必須宣告與屬性區塊實際使用的相同命名空間 URI,否則驗證器仍然無法將兩者連結起來。PDFlibPas 從您傳入的檔案名稱和版本中導出這兩個值,因此宣告與屬性區塊始終保持一致
輔助方法如何同時寫入兩個部分
在 PDFlibPas 中,您不必手動組裝這段 XML。您只需將文件置於 PDF/A-3 模式並呼叫一個方法。首先要確定的是符合旗標,因為 Factur-X 需要 PDF/A-3。呼叫 SetPDFAMode(7) 可選取 PDF/A-3u 層級,這會在識別結構描述中將 pdfaid:part 設為 3,並將 pdfaid:conformance 設為 U。在加入任何發票中繼資料之前,XMP 封包現在已帶有正確的部分和符合性
var
FileID: Integer;
begin
PDF.SetPDFAMode(7); // PDF/A-3u: pdfaid:part=3, conformance=U
PDF.NewDocument;
// draw the human-readable invoice page here
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML, // raw UTF-8 XML bytes
'EN16931', // ConformanceLevel
'factur-x.xml', // embedded file name
'Factur-X invoice XML', // /Desc text
'Alternative', // /AFRelationship
'1.0', // profile version
''); // optional country code
if FileID = 0 then
Exit; // not PDF/A-3, or XML/profile mismatch
PDF.SaveToFile('factur-x.pdf');
end;
一次呼叫 AddFacturXAssociatedFileFromString 就能完成失敗檔案所缺少的工作。它以您所命名的關係類型將 XML 嵌入為 PDF/A-3 關聯檔案,並記錄四個 fx 屬性,以及所選設定檔的結構描述名稱、命名空間 URI 和前綴。當文件儲存時,一個名為 ApplyFacturXMetadata 的內部步驟會將屬性區塊和相符的 pdfaExtension:schemas 宣告注入 XMP 封包,因此自訂屬性到達時已附帶描述。若文件不在 PDF/A-3 模式下,或 XML 與宣告的設定檔不符,此方法會回傳 0,這也是阻止格式不正確的發票進入檔案的相同防護措施
容器檢查看不到的盲點
這部分需要直白說明,因為這正是問題隱藏的原因。ValidateFacturXInvoice 檢查容器:它確認目錄有 /AF 條目、EmbeddedFiles 名稱樹存在、發票 XML 存在、嵌入的檔案名稱與設定檔相符、XML 中的指導方針 ID 與符合層級一致,以及 /AFRelationship 是 PDF/A-3 所允許的值。這些都是真實的檢查,能捕捉真實的缺陷。GetFacturXValidationIssues 以名稱回報這些問題,帶有識別碼如 MissingCatalogAF、NotPDFA3、ConformanceGuidelineMismatch、InvalidAFRelationship 和 InvalidFileNameProfile
但它不檢查 XMP 延伸結構描述是否存在且正確。一份容器完美無缺但 fx 屬性未宣告的檔案,會通過所有問題檢查並回傳 1,因為清單中沒有任何項目會檢查 pdfaExtension:schemas 區塊。這正是為什麼手工建立的發票,或由寫入了屬性區塊卻未附宣告的處理管線所產出的發票,能通過內建驗證器的一切檢查,卻仍然因第 6.6.2.3.1 條而被 veraPDF 拒絕。容器驗證器與 PDF/A 中繼資料驗證器回答的是不同問題,只有完整的 PDF/A 檢查器才能回答第二個問題
讀取問題以了解哪個層次發生了故障
由於這兩個層次的失敗是獨立的,正確的診斷習慣是先讀取容器問題,並將乾淨的結果視為僅關於容器的說明,而非關於 PDF/A 中繼資料的說明。執行內建驗證、收集問題清單,並在使用外部工具之前先對其採取行動
var
Issues: WideString;
begin
if PDF.ValidateFacturXInvoice = 0 then
begin
Issues := PDF.GetFacturXValidationIssues('|');
// container-level identifiers, for example:
// MissingCatalogAF, NotPDFA3, MissingEmbeddedFilesNameTree,
// ConformanceGuidelineMismatch, InvalidAFRelationship
WriteLn('Container issues: ', Issues);
end
else
WriteLn('Container OK; verify XMP extension schema with a PDF/A checker.');
end;
當該呼叫回傳問題名稱時,故障在容器中,訊息告訴您是哪個部分。當它回傳乾淨的結果而 veraPDF 仍然拒絕檔案時,故障幾乎總是 XMP 延伸結構描述,修正方法是讓 AddFacturXAssociatedFileFromString 寫入中繼資料,而非自行建構屬性區塊。在您自己的思維中將這兩個問題分開,才能將令人困惑的拒絕轉化為一行診斷:容器問題透過問題清單浮現,結構描述宣告問題只透過 PDF/A 驗證器浮現,混淆兩者正是讓問題得以隱藏的原因
更廣泛的 PDF/A 和 PDF/UA 合規性全貌,包括如何在檔案離開建置環境前執行預檢,在PDF/A 與 PDF/UA 預檢逐步說明中有所介紹。如果您的發票也必須符合無障礙要求,PDF/A-3a 和標記 PDF 所依賴的結構樹是標記 PDF 無障礙文章的主題。此處所描述的延伸結構描述處理作為 PDFlibPas Delphi PDF Library 的一部分提供,並附有本部落格各處所記載的 Factur-X、ZUGFeRD 和 XRechnung 設定檔支援