Τεχνικό Άρθρο

Σχήματα επέκτασης PDF/A-3 για XMP Factur-X στη Delphi

Έχετε δημιουργήσει ένα τιμολόγιο 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 φέρει ένα τιμολόγιο EN 16931 με όνομα factur-x.xml στην έκδοση 1.0

Καμία από αυτές τις τέσσερις ιδιότητες δεν αποτελεί μέρος κάποιου σχήματος XMP που το PDF/A προκαθορίζει. Τα σχήματα ταυτοποίησης Dublin Core, XMP Basic, PDF και PDF/A είναι γνωστά σε έναν συμμορφούμενο αναγνώστη, αλλά το fx: δεν είναι. Όταν το veraPDF διατρέχει το XMP και φτάνει σε μια ιδιότητα της οποίας τον χώρο ονομάτων δεν αναγνωρίζει, αναζητά μια δήλωση που θα του έλεγε τι σημαίνει η ιδιότητα. Αν αυτή η δήλωση απουσιάζει, αναφέρει αποτυχία ως προς τη ρήτρα 6.6.2.3.1 του ISO 19005-3, η οποία απαιτεί κάθε ιδιότητα που δεν προέρχεται από προκαθορισμένο σχήμα να περιγράφεται σε ένα σχήμα επέκτασης PDF/A. Τέσσερις μη δηλωμένες ιδιότητες, τέσσερις τρόποι να απορριφθεί το αρχείο, και κανένας από αυτούς δεν είναι ορατός σε έναν έλεγχο περιέκτη

Γιατί το PDF/A αρνείται μια γυμνή προσαρμοσμένη ιδιότητα

Ο κανόνας φαίνεται σχολαστικός μέχρι να θυμηθείτε για ποιο σκοπό υπάρχει το PDF/A. Η μορφή υπάρχει ώστε ένα αρχείο να μπορεί να ανοιχτεί και να γίνει κατανοητό δεκαετίες αργότερα, από λογισμικό στο οποίο ποτέ δεν ειπώθηκε για τις συμβάσεις του 2026. Ένας συμμορφούμενος αναγνώστης αναμένεται να βγάλει νόημα από το έγγραφο μόνο από το ίδιο το έγγραφο, χωρίς κανένα εξωτερικό μητρώο προς αναζήτηση

Τα προσαρμοσμένα μεταδεδομένα διασπούν αυτή την υπόσχεση, εκτός αν το αρχείο φέρει τη δική του περιγραφή. Δοθείσας μιας γυμνής ιδιότητας fx:ConformanceLevel, ένας μελλοντικός αναγνώστης δεν μπορεί να γνωρίζει τον URI χώρου ονομάτων στον οποίο δεσμεύεται το πρόθεμα fx, ούτε αν η τιμή είναι κείμενο, ημερομηνία ή ακέραιος, ούτε αν η ιδιότητα περιγράφει το ίδιο το έγγραφο ή κάποιον εξωτερικό πόρο. Ο μηχανισμός σχήματος επέκτασης PDF/A καλύπτει αυτό το κενό. Επιτρέπει στο αρχείο να δηλώσει, σε μια καθορισμένη δομή XMP, τον χώρο ονομάτων, το πρόθεμα και, για κάθε ιδιότητα, έναν τύπο τιμής και μια κατηγορία internal ή external. Μόλις υπάρξει αυτή η δήλωση, η ιδιότητα αυτοπεριγράφεται, και η ρήτρα 6.6.2.3.1 ικανοποιείται. Χωρίς αυτήν, ο επικυρωτής δεν έχει άλλη επιλογή από το να αντιμετωπίσει την ιδιότητα ως ακατανόητη και να ρίξει το αρχείο. Η διάκριση της κατηγορίας έχει σημασία εδώ: ιδιότητες τιμολογίου όπως αυτές περιγράφουν δεδομένα που προέρχονται από έξω από τον επεξεργαστή PDF, οπότε δηλώνονται ως external και όχι internal

Τι περιέχει η δήλωση του σχήματος επέκτασης

Η δήλωση είναι ένα rdf:Description μέσα στο πακέτο XMP που χρησιμοποιεί τους τρεις χώρους ονομάτων που ορίζει το AIIM: pdfaExtension, pdfaSchema και pdfaProperty. Μέσα σε έναν σάκο pdfaExtension:schemas βρίσκεται μία καταχώριση σχήματος που ονομάζει το σχήμα Factur-X, δίνει τα pdfaSchema:namespaceURI και pdfaSchema:prefix, και στη συνέχεια απαριθμεί τις τέσσερις ιδιότητες σε μια ακολουθία pdfaSchema:property. Κάθε ιδιότητα φέρει ένα όνομα, έναν pdfaProperty:valueType ίσο με Text, και έναν pdfaProperty:category ίσο με external. Η ενδεικτική σήμανση παρακάτω δείχνει τη μορφή αυτού του μπλοκ

<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 δηλώνονται με τον ίδιο τρόπο -->
          </rdf:Seq>
        </pdfaSchema:property>
      </rdf:li>
    </rdf:Bag>
  </pdfaExtension:schemas>
</rdf:Description>

Ο URI χώρου ονομάτων και το πρόθεμα δεν είναι σταθερές συμβολοσειρές. Ακολουθούν το προφίλ. Ένα έγγραφο Factur-X χρησιμοποιεί το urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# με το πρόθεμα fx, ενώ ένα αρχείο ZUGFeRD 2.0 που επιλέγεται μέσω zugferd-invoice.xml αναλύεται σε διαφορετικό 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;
  // σχεδιάστε εδώ την αναγνώσιμη από άνθρωπο σελίδα του τιμολογίου

  FileID := PDF.AddFacturXAssociatedFileFromString(
    InvoiceXML,                  // ακατέργαστα bytes XML σε UTF-8
    'EN16931',                   // ConformanceLevel
    'factur-x.xml',              // όνομα ενσωματωμένου αρχείου
    'Factur-X invoice XML',      // κείμενο /Desc
    'Alternative',               // /AFRelationship
    '1.0',                       // έκδοση προφίλ
    '');                         // προαιρετικός κωδικός χώρας
  if FileID = 0 then
    Exit;                        // όχι PDF/A-3, ή αναντιστοιχία XML/προφίλ

  PDF.SaveToFile('factur-x.pdf');
end;

Μία μόνο κλήση της AddFacturXAssociatedFileFromString κάνει τη δουλειά που έλειπε από το αρχείο που αποτύγχανε. Ενσωματώνει το XML ως συσχετισμένο αρχείο PDF/A-3 με τη σχέση που ονομάσατε, και καταγράφει τις τέσσερις ιδιότητες fx μαζί με το όνομα του σχήματος, τον URI χώρου ονομάτων και το πρόθεμα για το επιλεγμένο προφίλ. Όταν αποθηκεύεται το έγγραφο, ένα εσωτερικό βήμα με όνομα ApplyFacturXMetadata εισάγει τόσο το μπλοκ ιδιοτήτων όσο και την αντίστοιχη δήλωση pdfaExtension:schemas στο πακέτο XMP, ώστε οι προσαρμοσμένες ιδιότητες να φτάνουν ήδη περιγεγραμμένες. Η μέθοδος επιστρέφει 0 αν το έγγραφο δεν βρίσκεται σε λειτουργία PDF/A-3 ή αν το XML δεν ταιριάζει με το δηλωμένο προφίλ, που είναι η ίδια δικλείδα που εμποδίζει ένα κακοσχηματισμένο τιμολόγιο να φτάσει εξαρχής στο αρχείο

Το τυφλό σημείο που ο έλεγχος περιέκτη δεν μπορεί να δει

Αυτό είναι το σημείο που πρέπει να ονομαστεί ξεκάθαρα, γιατί είναι ο λόγος που το σφάλμα κρύβεται. Η ValidateFacturXInvoice ελέγχει τον περιέκτη. Επιβεβαιώνει ότι ο κατάλογος έχει μια καταχώριση /AF, ότι το δέντρο ονομάτων EmbeddedFiles υπάρχει, ότι το XML του τιμολογίου υπάρχει, ότι το όνομα του ενσωματωμένου αρχείου ταιριάζει με το προφίλ, ότι το αναγνωριστικό κατευθυντήριας γραμμής μέσα στο XML συμφωνεί με το επίπεδο συμμόρφωσης, και ότι το /AFRelationship είναι ένα από αυτά που επιτρέπει το PDF/A-3. Αυτοί είναι πραγματικοί έλεγχοι και εντοπίζουν πραγματικά ελαττώματα. Η GetFacturXValidationIssues τα αναφέρει με το όνομά τους, με αναγνωριστικά όπως MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship και InvalidFileNameProfile

Αυτό που δεν ελέγχει είναι αν το σχήμα επέκτασης XMP υπάρχει και είναι σωστό. Ένα αρχείο του οποίου ο περιέκτης είναι άψογος αλλά οι ιδιότητες fx δεν είναι δηλωμένες περνάει κάθε έλεγχο ζητημάτων και επιστρέφει 1, γιατί τίποτα σε εκείνη τη λίστα δεν επιθεωρεί το μπλοκ pdfaExtension:schemas. Αυτός ακριβώς είναι ο λόγος που ένα χειροποίητο τιμολόγιο, ή ένα που παρήχθη από μια ροή η οποία έγραψε το μπλοκ ιδιοτήτων χωρίς τη δήλωση, μπορεί να περάσει αβρόχοις ποσί από τον ενσωματωμένο επικυρωτή και να αποτύχει ακόμη στο veraPDF ως προς τη ρήτρα 6.6.2.3.1. Ο επικυρωτής περιέκτη και ο επικυρωτής μεταδεδομένων PDF/A απαντούν σε διαφορετικά ερωτήματα, και μόνο ο πλήρης ελεγκτής PDF/A απαντά στο δεύτερο

Ανάγνωση των ζητημάτων ώστε να ξέρετε ποιο επίπεδο έσπασε

Επειδή τα δύο επίπεδα αποτυγχάνουν ανεξάρτητα, η σωστή διαγνωστική συνήθεια είναι να διαβάζετε πρώτα τα ζητήματα του περιέκτη και να αντιμετωπίζετε ένα καθαρό αποτέλεσμα ως δήλωση μόνο για τον περιέκτη, ποτέ για τα μεταδεδομένα PDF/A. Εκτελέστε την ενσωματωμένη επικύρωση, συλλέξτε τη λίστα ζητημάτων και ενεργήστε πάνω της πριν στραφείτε σε ένα εξωτερικό εργαλείο

var
  Issues: WideString;
begin
  if PDF.ValidateFacturXInvoice = 0 then
  begin
    Issues := PDF.GetFacturXValidationIssues('|');
    // αναγνωριστικά επιπέδου περιέκτη, για παράδειγμα:
    //   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, συμπεριλαμβανομένου του πώς να εκτελέσετε ένα πέρασμα preflight πριν φύγει ένα αρχείο από τη διαδικασία παραγωγής σας, καλύπτεται στον οδηγό preflight PDF/A και PDF/UA. Αν το τιμολόγιό σας πρέπει επίσης να είναι προσβάσιμο, το δέντρο δομής από το οποίο εξαρτώνται το PDF/A-3a και το tagged PDF αποτελεί το θέμα του άρθρου προσβασιμότητας tagged-PDF. Ο χειρισμός σχήματος επέκτασης που περιγράφεται εδώ παρέχεται ως μέρος της PDFlibPas Delphi PDF Library μαζί με την υποστήριξη προφίλ Factur-X, ZUGFeRD και XRechnung που τεκμηριώνεται σε όλο αυτό το blog