Technical Article

Schema di estensione PDF/A-3 per l'XMP Factur-X in Delphi

Hai costruito una fattura Factur-X e ogni controllo del container è superato. Il catalogo porta un array /AF, l'albero dei nomi EmbeddedFiles si risolve nella specifica file corretta, il file incorporato factur-x.xml ha l'/AFRelationship corretta di Alternative, e la funzione integrata ValidateFacturXInvoice restituisce 1. Poi passi lo stesso file attraverso veraPDF, il validatore di riferimento utilizzato dai portali fiscali, e questo stabilisce che l'intero documento non è un PDF/A-3 valido. La struttura è corretta. Il problema sono i metadati, e il fallimento è uno dei più facili da trascurare nell'intero flusso di lavoro della fatturazione elettronica

Vale la pena capire a fondo il motivo, perché spiega una classe di difetti del PDF/A che non ha nulla a che fare con la pagina visibile o con l'allegato, ma ha tutto a che fare con il modo in cui l'XMP descrive se stesso. Questa è la trappola che si nasconde dietro a un controllo del container superato con successo

Le quattro proprietà che fanno fallire il file

Una fattura Factur-X scrive quattro proprietà personalizzate nel suo pacchetto XMP affinché i software a valle possano leggere il profilo della fattura senza analizzare l'XML incorporato. Esse risiedono nel namespace Factur-X sotto il prefisso fx: fx:DocumentFileName, fx:DocumentType, fx:Version e fx:ConformanceLevel. Sono esattamente i metadati di cui un lettore ha bisogno per sapere che questo PDF trasporta una fattura EN 16931 denominata factur-x.xml alla versione 1.0

Nessuna di queste quattro proprietà fa parte di alcuno schema XMP predefinito dal PDF/A. Gli schemi Dublin Core, XMP Basic, PDF e identificazione PDF/A sono noti a un lettore conforme, ma fx: non lo è. Quando veraPDF percorre l'XMP e raggiunge una proprietà il cui namespace non gli è noto, cerca una dichiarazione che gli dica cosa significa tale proprietà. Se quella dichiarazione è assente, segnala un fallimento rispetto alla clausola 6.6.2.3.1 della norma ISO 19005-3, la quale richiede che ogni proprietà non tratta da uno schema predefinito venga descritta in uno schema di estensione PDF/A (extension schema). Quattro proprietà non dichiarate, quattro modi per far rifiutare il file, e nessuna di esse è visibile a un controllo del container

Perché il PDF/A rifiuta una nuda proprietà personalizzata

La regola sembra pedante finché non ricordi a cosa serve il PDF/A. Il formato esiste affinché un file possa essere aperto e compreso a decenni da oggi, da un software a cui non sono mai state spiegate le convenzioni del 2026. Ci si aspetta che un lettore conforme dia senso al documento in base al documento stesso, senza consultare alcun registro esterno

I metadati personalizzati infrangono tale promessa, a meno che il file non porti con sé la propria descrizione. Data una nuda proprietà fx:ConformanceLevel, un futuro lettore non può sapere a quale URI del namespace si leghi il prefisso fx, se il valore sia un testo, una data o un numero intero, o se la proprietà descriva il documento stesso o qualche risorsa esterna. Il meccanismo dello schema di estensione PDF/A colma tale lacuna. Permette al file di dichiarare, in una struttura XMP fissa, il namespace, il prefisso e, per ciascuna proprietà, un tipo di valore (value type) e una categoria internal o external. Una volta che la dichiarazione è presente, la proprietà diventa autodescrittiva e la clausola 6.6.2.3.1 è soddisfatta. Senza di essa, il validatore non ha altra scelta che trattare la proprietà come incomprensibile e far fallire il file. La distinzione di categoria è importante qui: le proprietà della fattura come queste descrivono dati provenienti dall'esterno del processore PDF, perciò sono dichiarate external anziché internal

Cosa contiene la dichiarazione dello schema di estensione

La dichiarazione è un'rdf:Description nel pacchetto XMP che utilizza i tre namespace definiti dall'AIIM: pdfaExtension, pdfaSchema e pdfaProperty. All'interno di un bag pdfaExtension:schemas si trova una voce di schema che nomina lo schema Factur-X, fornisce il suo pdfaSchema:namespaceURI e il pdfaSchema:prefix, per poi elencare le quattro proprietà in una sequenza pdfaSchema:property. Ogni proprietà reca un nome, un pdfaProperty:valueType di Text e una pdfaProperty:category external. Il markup illustrativo sottostante mostra la forma di quel blocco

<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 del namespace e il prefisso non sono stringhe fisse. Esse seguono il profilo. Un documento Factur-X usa urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# con il prefisso fx, mentre un file ZUGFeRD 2.0 selezionato tramite zugferd-invoice.xml si risolve in un URI diverso sotto il proprio nome di schema. Lo schema di estensione deve dichiarare lo stesso URI del namespace effettivamente usato dal blocco delle proprietà, altrimenti il validatore non riesce comunque a collegare i due. PDFlibPas ricava entrambi i valori dal nome file e dalla versione che passi, cosicché la dichiarazione e il blocco delle proprietà concordino sempre

Come l'helper scrive entrambe le metà insieme

In PDFlibPas non assembli quell'XML a mano. Inserisci il documento in una modalità PDF/A-3 e chiami un solo metodo. La prima cosa da definire è il flag di conformità, dato che Factur-X richiede il PDF/A-3. Chiamando SetPDFAMode(7) si seleziona il livello PDF/A-3u, che imposta pdfaid:part a 3 e pdfaid:conformance a U nello schema di identificazione. Il pacchetto XMP reca ora la parte e la conformità corrette prima ancora che vengano aggiunti i metadati della fattura

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;

Una singola chiamata ad AddFacturXAssociatedFileFromString fa il lavoro che mancava al file fallito. Incorpora l'XML come file associato PDF/A-3 con la relazione indicata, e registra le quattro proprietà fx assieme al nome dello schema, all'URI del namespace e al prefisso per il profilo prescelto. Quando il documento viene salvato, un passaggio interno denominato ApplyFacturXMetadata inietta nel pacchetto sia il blocco delle proprietà che la corrispondente dichiarazione pdfaExtension:schemas, così che le proprietà personalizzate arrivino già descritte. Il metodo restituisce 0 se il documento non è in modalità PDF/A-3 o se l'XML non corrisponde al profilo dichiarato, che è lo stesso guard che impedisce a una fattura malformata di raggiungere il file fin dall'inizio

Il punto cieco che il controllo del container non può vedere

Questa è la parte da spiegare con chiarezza, poiché è il motivo per cui il bug si nasconde. ValidateFacturXInvoice controlla il container. Conferma che il catalogo possiede una voce /AF, che l'albero dei nomi EmbeddedFiles è presente, che l'XML della fattura esiste, che il nome file incorporato corrisponde al profilo, che il guideline ID nell'XML concorda col livello di conformità e che l'/AFRelationship rientra in quelle consentite dal PDF/A-3. Si tratta di controlli reali che individuano difetti reali. GetFacturXValidationIssues li segnala per nome, con identificatori quali MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship e InvalidFileNameProfile

Ciò che non controlla è se lo schema di estensione XMP è presente e corretto. Un file il cui container è impeccabile ma le cui proprietà fx non sono dichiarate passa ogni controllo dei problemi e restituisce 1, in quanto nulla di quella lista ispeziona il blocco pdfaExtension:schemas. Ecco precisamente perché una fattura costruita a mano, o prodotta da una pipeline che ha scritto il blocco delle proprietà senza la dichiarazione, può scivolare liscia attraverso il validatore integrato e far comunque fallire veraPDF sulla clausola 6.6.2.3.1. Il validatore del container e il validatore dei metadati PDF/A rispondono a domande diverse, e solo il controllore (checker) PDF/A completo risponde alla seconda

Leggere i problemi per sapere quale strato si è rotto

Poiché i due strati falliscono indipendentemente, la giusta abitudine diagnostica è quella di leggere prima i problemi del container e trattare un risultato pulito esclusivamente come un'affermazione sul container, mai sui metadati PDF/A. Esegui la convalida integrata, raccogli la lista dei problemi (issue list) e agisci su di essa prima di ricorrere a uno strumento esterno

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;

Quando quella chiamata restituisce un nome di problema, il difetto è nel container e il messaggio ti dice in quale parte. Quando restituisce pulito e veraPDF rifiuta ancora il file, il difetto è quasi sempre lo schema di estensione XMP, e la soluzione è lasciare che AddFacturXAssociatedFileFromString scriva i metadati invece di costruire il blocco delle proprietà da solo. Mantenere separate nella propria mente le due questioni è ciò che trasforma un rifiuto sconcertante in una diagnosi immediata (one-line diagnosis): i problemi del container emergono attraverso la lista dei problemi, i problemi di dichiarazione dello schema emergono solo attraverso un validatore PDF/A, e confonderli è ciò che consente al bug di nascondersi

Il quadro più ampio della conformità PDF/A e PDF/UA, che include come eseguire un passaggio di preflight prima che un file lasci la tua build, è trattato in la nostra panoramica sul preflight PDF/A e PDF/UA in Delphi. Se la tua fattura deve essere anche accessibile, l'albero della struttura da cui dipendono il PDF/A-3a e i PDF taggati (tagged PDF) è l'argomento di l'articolo sull'accessibilità dei PDF taggati. La gestione dello schema di estensione qui descritta è fornita come parte della PDFlibPas Delphi PDF Library, accanto al supporto ai profili Factur-X, ZUGFeRD e XRechnung documentato in tutto questo blog