Technical Article

Schémas d'extension PDF/A-3 pour le XMP Factur-X dans Delphi

Vous avez construit une facture Factur-X et chaque vérification de conteneur réussit. Le catalogue contient un tableau /AF, l'arbre de noms EmbeddedFiles résout la bonne spécification de fichier, le factur-x.xml intégré a le bon /AFRelationship de Alternative, et le validateur intégré ValidateFacturXInvoice renvoie 1. Ensuite, vous passez le même fichier dans veraPDF, le vérificateur de référence utilisé par les portails fiscaux, et il juge que l'ensemble du document n'est pas un PDF/A-3 valide. La structure est correcte. Le problème vient des métadonnées, et l'échec est l'un des plus faciles à manquer dans tout le flux de travail de facturation électronique

Il est important de comprendre la raison dans son intégralité, car elle explique une classe de défauts PDF/A qui n'a rien à voir avec la page visible ou la pièce jointe, et tout à voir avec la façon dont XMP se décrit lui-même. C'est le piège qui se cache derrière une vérification de conteneur au vert

Les quatre propriétés qui font échouer le fichier

Une facture Factur-X écrit quatre propriétés personnalisées dans son paquet XMP afin que les logiciels en aval puissent lire le profil de la facture sans analyser le XML intégré. Elles vivent dans l'espace de noms Factur-X sous le préfixe fx : fx:DocumentFileName, fx:DocumentType, fx:Version et fx:ConformanceLevel. Ce sont exactement les métadonnées dont un lecteur a besoin pour savoir que ce PDF transporte une facture EN 16931 nommée factur-x.xml à la version 1.0

Aucune de ces quatre propriétés ne fait partie d'un schéma XMP que le PDF/A prédéfinit. Les schémas d'identification Dublin Core, XMP Basic, PDF et PDF/A sont connus d'un lecteur conforme, mais fx: ne l'est pas. Lorsque veraPDF parcourt le XMP et atteint une propriété dont il ne reconnaît pas l'espace de noms, il cherche une déclaration qui lui dirait ce que signifie la propriété. Si cette déclaration est absente, il signale un échec par rapport à la clause 6.6.2.3.1 de la norme ISO 19005-3, qui exige que chaque propriété qui n'est pas tirée d'un schéma prédéfini soit décrite dans un schéma d'extension PDF/A. Quatre propriétés non déclarées, quatre façons pour le fichier d'être rejeté, et aucune d'entre elles n'est visible lors d'une vérification de conteneur

Pourquoi le PDF/A refuse une propriété personnalisée nue

La règle semble pédante jusqu'à ce que l'on se souvienne de l'utilité du PDF/A. Le format existe pour qu'un fichier puisse être ouvert et compris dans des décennies, par des logiciels qui n'ont jamais été informés des conventions de 2026. On attend d'un lecteur conforme qu'il comprenne le document à partir du document seul, sans aucun registre externe à consulter

Les métadonnées personnalisées rompent cette promesse à moins que le fichier ne porte sa propre description. Face à une propriété fx:ConformanceLevel nue, un lecteur futur ne peut pas connaître l'URI de l'espace de noms auquel le préfixe fx est lié, si la valeur est du texte, une date ou un entier, ou si la propriété décrit le document lui-même ou une ressource externe. Le mécanisme de schéma d'extension PDF/A comble cette lacune. Il permet au fichier de déclarer, dans une structure XMP fixe, l'espace de noms, le préfixe et pour chaque propriété un type de valeur et une catégorie internal ou external. Une fois cette déclaration présente, la propriété est auto-descriptive et la clause 6.6.2.3.1 est satisfaite. Sans elle, le validateur n'a pas d'autre choix que de traiter la propriété comme inintelligible et de faire échouer le fichier. La distinction de catégorie est importante ici : les propriétés de facture telles que celles-ci décrivent des données qui proviennent de l'extérieur du processeur PDF, elles sont donc déclarées external plutôt que internal

Ce que contient la déclaration du schéma d'extension

La déclaration est une rdf:Description dans le paquet XMP qui utilise les trois espaces de noms définis par l'AIIM pdfaExtension, pdfaSchema et pdfaProperty. À l'intérieur d'un sac (bag) pdfaExtension:schemas se trouve une entrée de schéma qui nomme le schéma Factur-X, donne son pdfaSchema:namespaceURI et pdfaSchema:prefix, puis liste les quatre propriétés dans une séquence pdfaSchema:property. Chaque propriété porte un nom, un pdfaProperty:valueType de Text et une pdfaProperty:category de external. Le balisage illustratif ci-dessous montre la forme de ce bloc

<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>

L'URI et le préfixe de l'espace de noms ne sont pas des chaînes fixes. Ils suivent le profil. Un document Factur-X utilise urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# avec le préfixe fx, tandis qu'un fichier ZUGFeRD 2.0 sélectionné via zugferd-invoice.xml se résout en une URI différente sous son propre nom de schéma. Le schéma d'extension doit déclarer la même URI d'espace de noms que celle réellement utilisée par le bloc de propriétés, sinon le validateur ne peut toujours pas relier les deux. PDFlibPas dérive les deux valeurs à partir du nom de fichier et de la version que vous transmettez, de sorte que la déclaration et le bloc de propriétés concordent toujours

Comment l'assistant écrit les deux moitiés ensemble

Dans PDFlibPas, vous n'assemblez pas ce XML à la main. Vous placez le document dans un mode PDF/A-3 et appelez une méthode. La première chose à régler est le drapeau de conformité, car Factur-X nécessite PDF/A-3. L'appel de SetPDFAMode(7) sélectionne le niveau PDF/A-3u, qui définit pdfaid:part sur 3 et pdfaid:conformance sur U dans le schéma d'identification. Le paquet XMP porte désormais la bonne partie et conformité avant l'ajout de toute métadonnée de facture

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;

Un seul appel à AddFacturXAssociatedFileFromString effectue le travail qui manquait au fichier défaillant. Il intègre le XML en tant que fichier associé PDF/A-3 avec la relation que vous avez nommée, et il enregistre les quatre propriétés fx ainsi que le nom du schéma, l'URI de l'espace de noms et le préfixe pour le profil choisi. Lorsque le document est enregistré, une étape interne nommée ApplyFacturXMetadata injecte à la fois le bloc de propriétés et la déclaration pdfaExtension:schemas correspondante dans le paquet XMP, de sorte que les propriétés personnalisées arrivent déjà décrites. La méthode renvoie 0 si le document n'est pas dans un mode PDF/A-3 ou si le XML ne correspond pas au profil déclaré, ce qui est la même protection qui empêche une facture mal formée d'atteindre le fichier en premier lieu

L'angle mort que la vérification de conteneur ne peut pas voir

C'est la partie à nommer clairement, car c'est la raison pour laquelle le bogue se cache. ValidateFacturXInvoice vérifie le conteneur. Il confirme que le catalogue a une entrée /AF, que l'arbre de noms EmbeddedFiles est présent, que le XML de la facture existe, que le nom de fichier intégré correspond au profil, que l'ID de directive dans le XML concorde avec le niveau de conformité, et que la /AFRelationship est l'une de celles autorisées par le PDF/A-3. Ce sont de vraies vérifications et elles détectent de vrais défauts. GetFacturXValidationIssues les signale par leur nom, avec des identifiants tels que MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship et InvalidFileNameProfile

Ce qu'il ne vérifie pas, c'est si le schéma d'extension XMP est présent et correct. Un fichier dont le conteneur est impeccable mais dont les propriétés fx ne sont pas déclarées réussit chaque vérification de problème et renvoie 1, car rien dans cette liste n'inspecte le bloc pdfaExtension:schemas. C'est précisément la raison pour laquelle une facture construite à la main, ou produite par un pipeline qui a écrit le bloc de propriétés sans la déclaration, peut passer sans encombre le validateur intégré et tout de même échouer dans veraPDF sur la clause 6.6.2.3.1. Le validateur de conteneur et le validateur de métadonnées PDF/A répondent à des questions différentes, et seul le vérificateur PDF/A complet répond à la seconde

Lire les problèmes pour savoir quelle couche s'est cassée

Comme les deux couches échouent indépendamment, la bonne habitude de diagnostic est de lire d'abord les problèmes de conteneur et de traiter un résultat propre comme une déclaration concernant uniquement le conteneur, jamais concernant les métadonnées PDF/A. Exécutez la validation intégrée, collectez la liste des problèmes et agissez en conséquence avant de faire appel à un outil externe

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;

Lorsque cet appel renvoie un nom de problème, le défaut se trouve dans le conteneur et le message vous indique quelle partie. Lorsqu'il renvoie un résultat propre et que veraPDF rejette toujours le fichier, le défaut réside presque toujours dans le schéma d'extension XMP, et la solution consiste à laisser AddFacturXAssociatedFileFromString écrire les métadonnées plutôt que de construire le bloc de propriétés vous-même. Garder les deux questions séparées dans votre propre esprit est ce qui transforme un rejet déroutant en un diagnostic d'une ligne : les problèmes de conteneur font surface via la liste des problèmes, les problèmes de déclaration de schéma ne font surface que via un validateur PDF/A, et confondre les deux est ce qui permet au bogue de se cacher

Le tableau plus large de la conformité PDF/A et PDF/UA, y compris la façon d'exécuter une passe de preflight avant qu'un fichier ne quitte votre build, est couvert dans la description détaillée du preflight PDF/A et PDF/UA dans Delphi. Si votre facture doit également être accessible, l'arbre de structure dont dépendent le PDF/A-3a et le PDF balisé (tagged PDF) fait l'objet de l'article sur l'accessibilité des PDF balisés. La gestion des schémas d'extension décrite ici est fournie dans le cadre de PDFlibPas Delphi PDF Library, avec la prise en charge des profils Factur-X, ZUGFeRD et XRechnung documentée tout au long de ce blog