你构建了一张 Factur-X 发票,每一个容器检查都通过了。文档目录带有一个 /AF 数组,EmbeddedFiles 名称树解析到了正确的文件规范,嵌入的 factur-x.xml 具有正确的 /AFRelationship 为 Alternative,并且内置的 ValidateFacturXInvoice 返回 1。然后你通过 veraPDF(税务门户网站使用的参考检查器)运行同一个文件,它判定整个文档不是有效的 PDF/A-3。结构是正确的。问题出在元数据上,这是整个电子发票工作流中最容易忽略的失败之一
了解全部原因是值得的,因为它解释了一类与可见页面或附件无关,而完全与 XMP 如何描述自身有关的 PDF/A 缺陷。这就是隐藏在绿色容器检查背后的陷阱
导致文件失败的四个属性
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 schema 的一部分。Dublin Core、XMP Basic、PDF 和 PDF/A identification schema 为合规的阅读器所知,但 fx: 不是。当 veraPDF 遍历 XMP 并遇到一个其命名空间不认识的属性时,它会寻找一个能告诉它该属性含义的声明。如果缺少该声明,它就会报告针对 ISO 19005-3 条款 6.6.2.3.1 的失败,该条款要求,每一个不来自预定义 schema 的属性,都必须在 PDF/A 扩展 schema 中描述。四个未声明的属性,四种文件被拒绝的方式,而容器检查却一个也看不见
为什么 PDF/A 拒绝裸露的自定义属性
除非你记起 PDF/A 的用途,否则这条规则看起来很迂腐。该格式的存在是为了让文件在几十年后,能被从未了解过 2026 年约定的软件打开并理解。合规的阅读器被期望能仅凭文档本身来理解文档,而无需查阅任何外部注册表
除非文件携带自身的描述,否则自定义元数据会打破这一承诺。给定一个裸露的 fx:ConformanceLevel 属性,未来的阅读器无法知道 fx 前缀绑定的命名空间 URI,不知道该值是文本、日期还是整数,也不知道该属性描述的是文档本身还是某个外部资源。PDF/A extension schema 机制填补了这一空白。它允许文件在一个固定的 XMP 结构中声明命名空间、前缀,并为每个属性声明一个值类型以及属于 internal 还是 external 的类别。一旦存在该声明,属性就具有自我描述性,从而满足了条款 6.6.2.3.1 的要求。如果没有它,验证器别无选择,只能将该属性视为无法理解并让文件失败。这里的类别区分很重要:像这样描述来自 PDF 处理器外部数据的发票属性,应声明为 external 而不是 internal
Extension schema 声明包含什么
该声明是 XMP 数据包中的一个 rdf:Description,它使用了三个由 AIIM 定义的命名空间:pdfaExtension、pdfaSchema 和 pdfaProperty。在一个 pdfaExtension:schemas 包里面有一个 schema 条目,该条目指定了 Factur-X schema 的名称,给出了它的 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 文件在其自己的 schema 名称下解析为不同的 URI。extension schema 必须声明属性块实际使用的相同的命名空间 URI,否则验证器仍然无法将两者连接起来。PDFlibPas 从你传递的文件名和版本派生这两个值,因此声明和属性块总是保持一致
助手如何将两半写在一起
在 PDFlibPas 中,你不需要手动组装那个 XML。你将文档置入 PDF/A-3 模式并调用一个方法。首先要解决的是一致性标志,因为 Factur-X 需要 PDF/A-3。调用 SetPDFAMode(7) 选择 PDF/A-3u 级别,它在 identification schema 中将 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 属性,连同所选配置文件的 schema 名称、命名空间 URI 和前缀。保存文档时,一个名为 ApplyFacturXMetadata 的内部步骤将属性块和匹配的 pdfaExtension:schemas 声明一起注入 XMP 数据包中,因此自定义属性到来时已经是被描述好的。如果文档未处于 PDF/A-3 模式,或者 XML 与声明的配置文件不匹配,则方法返回 0,这与一开始阻止错误格式的发票进入文件是同一个守卫
容器检查看不到的盲区
这部分必须明确指出来,因为这是错误隐藏的原因。ValidateFacturXInvoice 检查的是容器。它确认目录有一个 /AF 条目,EmbeddedFiles 名称树存在,发票 XML 存在,嵌入的文件名匹配配置文件,XML 中的 guideline ID 与一致性级别一致,并且 /AFRelationship 是 PDF/A-3 允许的。那些是真实的检查,它们捕捉真实的缺陷。GetFacturXValidationIssues 按名称报告它们,具有如 MissingCatalogAF、NotPDFA3、ConformanceGuidelineMismatch、InvalidAFRelationship 和 InvalidFileNameProfile 等标识符
它不检查的是 XMP extension schema 是否存在且正确。一个容器完美无瑕但 fx 属性未声明的文件能通过每一个 issue 检查并返回 1,因为该列表中没有任何内容会检查 pdfaExtension:schemas 块。这正是为什么一个手工构建的发票,或者是由在没有声明的情况下写入了属性块的流水线所生产的发票,能够顺利通过内置验证器,但却在 veraPDF 的条款 6.6.2.3.1 上失败的原因。容器验证器和 PDF/A 元数据验证器回答不同的问题,并且只有完整的 PDF/A 检查器回答第二个问题
读取 issue 以便你知道哪一层出错了
因为这两层是独立失败的,正确的诊断习惯是首先读取容器 issue,并将干净的结果仅视为关于容器的声明,而绝对不是关于 PDF/A 元数据的。在求助于外部工具之前,先运行内置验证,收集 issue 列表,并对其采取行动
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;
当该调用返回一个 issue 名称时,故障在容器中,且消息会告诉你是哪一部分。当它返回干净而 veraPDF 仍然拒绝文件时,故障几乎总是 XMP extension schema,而修复方法是让 AddFacturXAssociatedFileFromString 写入元数据,而不是你自己去构造属性块。在自己的头脑中将这两个问题分开,是将令人困惑的拒绝转变为单行诊断的关键:容器问题通过 issue 列表浮现,schema 声明问题仅通过 PDF/A 验证器浮现,而混淆两者正是让错误隐藏的原因
更广泛的 PDF/A 和 PDF/UA 兼容性图景,包括在文件离开构建之前如何运行印前检查遍历,在PDF/A 和 PDF/UA 印前检查演练中有所涉及。如果你的发票还必须是可访问的,PDF/A-3a 和 tagged PDF 所依赖的结构树是tagged-PDF 可访问性文章的主题。此处描述的 extension schema 处理与本博客中记录的 Factur-X、ZUGFeRD 和 XRechnung 配置文件支持一起,作为适用于 Delphi 和 C++Builder 的 PDFlibPas Delphi PDF Library 的一部分发布