Technical Article

Delphi 中用于 Factur-X XMP 的 PDF/A-3 Extension Schema

你构建了一张 Factur-X 发票,每一个容器检查都通过了。文档目录带有一个 /AF 数组,EmbeddedFiles 名称树解析到了正确的文件规范,嵌入的 factur-x.xml 具有正确的 /AFRelationshipAlternative,并且内置的 ValidateFacturXInvoice 返回 1。然后你通过 veraPDF(税务门户网站使用的参考检查器)运行同一个文件,它判定整个文档不是有效的 PDF/A-3。结构是正确的。问题出在元数据上,这是整个电子发票工作流中最容易忽略的失败之一

了解全部原因是值得的,因为它解释了一类与可见页面或附件无关,而完全与 XMP 如何描述自身有关的 PDF/A 缺陷。这就是隐藏在绿色容器检查背后的陷阱

导致文件失败的四个属性

Factur-X 发票将四个自定义属性写入其 XMP 数据包中,以便下游软件可以读取发票配置文件,而无需解析嵌入的 XML。它们存在于 Factur-X 命名空间中的 fx 前缀下:fx:DocumentFileNamefx:DocumentTypefx:Versionfx: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 定义的命名空间:pdfaExtensionpdfaSchemapdfaProperty。在一个 pdfaExtension:schemas 包里面有一个 schema 条目,该条目指定了 Factur-X schema 的名称,给出了它的 pdfaSchema:namespaceURIpdfaSchema:prefix,然后在一个 pdfaSchema:property 序列中列出了那四个属性。每个属性带有一个名称、一个为 TextpdfaProperty:valueType,以及一个为 externalpdfaProperty: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 按名称报告它们,具有如 MissingCatalogAFNotPDFA3ConformanceGuidelineMismatchInvalidAFRelationshipInvalidFileNameProfile 等标识符

它不检查的是 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 的一部分发布