Technical Article

Rozšiřující schémata PDF/A-3 pro XMP ve Factur-X v Delphi

Vytvořili jste fakturu Factur-X a každá kontrola kontejneru proběhla v pořádku. Katalog obsahuje pole /AF, strom názvů EmbeddedFiles ukazuje na správnou specifikaci souboru, vložený soubor factur-x.xml má správnou relaci /AFRelationship s hodnotou Alternative a vestavěná metoda ValidateFacturXInvoice vrací hodnotu 1. Poté ale stejný soubor proženete nástrojem veraPDF, což je referenční kontrolor, jenž daňové portály používají, a ten rozhodne, že celý dokument není platným PDF/A-3. Struktura je přitom správná. Problémem jsou metadata a toto selhání představuje jednu z nejčastěji přehlížených chyb v celém pracovním postupu s elektronickými fakturami

Důvod stojí za to plně pochopit, jelikož vysvětluje celou třídu defektů PDF/A, které nemají nic společného s viditelnou stránkou nebo s přílohou a týkají se výhradně toho, jak XMP popisuje samo sebe. Toto je přesně ta past, která se skrývá za zeleným světlem při kontrole kontejneru

Čtyři vlastnosti, na kterých soubor pohoří

Faktura Factur-X zapisuje do svého XMP paketu čtyři vlastní vlastnosti, aby navazující software mohl přečíst profil faktury, aniž by musel analyzovat vložené XML. Tyto vlastnosti žijí ve jmenném prostoru Factur-X pod předponou fx: fx:DocumentFileName, fx:DocumentType, fx:Version a fx:ConformanceLevel. Představují přesně ta metadata, která čtecí program potřebuje, aby poznal, že toto PDF nese fakturu odpovídající normě EN 16931, pojmenovanou jako factur-x.xml ve verzi 1.0

Žádná z těchto čtyř vlastností není součástí schématu XMP, které by formát PDF/A měl předdefinované. Standardnímu čtecímu programu jsou známá identifikační schémata Dublin Core, XMP Basic, PDF a PDF/A, avšak fx: nikoliv. Když nástroj veraPDF prochází XMP a narazí na vlastnost, jejíž jmenný prostor nezná, hledá deklaraci, která by mu řekla, co tato vlastnost znamená. Pokud deklarace chybí, nahlásí selhání vůči normě ISO 19005-3 klauzule 6.6.2.3.1, jež vyžaduje, aby každá vlastnost, která nepochází z předdefinovaného schématu, byla popsána v rozšiřujícím schématu PDF/A. Máme tu tedy čtyři nedeklarované vlastnosti, což znamená čtyři způsoby, jak může být soubor zamítnut, a ani jeden z nich není při pouhé kontrole kontejneru viditelný

Proč PDF/A odmítá holou vlastní vlastnost

Toto pravidlo vypadá pedantsky, dokud si nevzpomenete, k čemu PDF/A vlastně slouží. Tento formát existuje proto, aby bylo možné soubor otevřít a pochopit i za několik desetiletí softwarem, kterému nikdo nikdy o konvencích z roku 2026 neřekl. Od správného čtecího programu se očekává, že dokumentu porozumí pouze na základě samotného dokumentu, bez nahlížení do jakýchkoli externích registrů

Vlastní metadata tento slib porušují, pakliže soubor nenese svůj vlastní popis. Majíce k dispozici jen holou vlastnost fx:ConformanceLevel, budoucí čtecí program nemá jak poznat, k jakému URI jmenného prostoru se předpona fx váže, zda je hodnotou text, datum nebo celé číslo, a už vůbec ne to, zda vlastnost popisuje samotný dokument nebo nějaký vnější zdroj. Mechanismus rozšiřujícího schématu v PDF/A tuto mezeru zaplňuje. Umožňuje souboru deklarovat (v pevně dané struktuře XMP) jmenný prostor, předponu a u každé vlastnosti její datový typ a kategorii internal nebo external. Jakmile je tato deklarace přítomna, vlastnost se stává sebepopisující a klauzule 6.6.2.3.1 je splněna. Bez ní validátoru nezbývá nic jiného, než považovat vlastnost za nesrozumitelnou a prohlásit soubor za neplatný. Rozlišení kategorie je zde důležité: vlastnosti faktury, jako jsou tyto, popisují data, která pocházejí zvnějšku procesoru PDF, takže jsou deklarovány jako external a nikoliv internal

Co obsahuje deklarace rozšiřujícího schématu

Deklarace je prvek rdf:Description v XMP paketu, který používá tři jmenné prostory definované standardem AIIM: pdfaExtension, pdfaSchema a pdfaProperty. Uvnitř kontejneru pdfaExtension:schemas sídlí jeden záznam schématu, který pojmenovává schéma Factur-X, uvádí jeho pdfaSchema:namespaceURI a pdfaSchema:prefix a následně vypisuje všechny čtyři vlastnosti v sekvenci pdfaSchema:property. Každá vlastnost nese název, pdfaProperty:valueType s hodnotou Text a pdfaProperty:category nastavenou na external. Ilustrativní zápis níže ukazuje, jaký má tento blok tvar

<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 jmenného prostoru a předpona nejsou fixní řetězce. Řídí se profilem. Dokument Factur-X používá urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# s předponou fx, zatímco soubor ZUGFeRD 2.0 vybraný přes zugferd-invoice.xml se pod vlastním názvem schématu překládá na odlišné URI. Rozšiřující schéma musí deklarovat stejné URI jmenného prostoru, jaké skutečně používá blok vlastností, jinak validátor tyto dvě věci stále nedokáže propojit. Knihovna PDFlibPas odvozuje obě hodnoty z názvu souboru a verze, kterou jí předáte, takže deklarace a blok vlastností spolu budou vždy stoprocentně souhlasit

Jak pomocník zapisuje obě poloviny současně

V knihovně PDFlibPas toto XML ručně sestavovat nemusíte. Přepnete dokument do režimu PDF/A-3 a zavoláte jedinou metodu. První věcí, kterou je třeba vyřešit, je příznak shody, protože Factur-X vyžaduje PDF/A-3. Volání metody SetPDFAMode(7) vybírá úroveň PDF/A-3u, což nastaví pdfaid:part na 3 a pdfaid:conformance na U v identifikačním schématu. Nyní tak XMP paket nese tu správnou část a shodu ještě předtím, než se vůbec přidají jakákoli metadata faktury

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;

Jediné volání AddFacturXAssociatedFileFromString provede přesně tu práci, která selhávajícímu souboru chyběla. Vloží XML jako přidružený soubor ve formátu PDF/A-3 s relací, kterou jste zadali, a zaznamená ony čtyři vlastnosti fx spolu s názvem schématu, URI jmenného prostoru a předponou pro zvolený profil. Při uložení dokumentu se spustí vnitřní krok pojmenovaný ApplyFacturXMetadata, který do paketu XMP vstříkne jak blok s vlastnostmi, tak odpovídající deklaraci pdfaExtension:schemas, a díky tomu dorazí tyto vlastní vlastnosti již náležitě popsané. Tato metoda vrací 0 v případě, že dokument není v režimu PDF/A-3 nebo pokud samotné XML neodpovídá deklarovanému profilu, což je naprosto stejná pojistka, která zastaví deformovanou fakturu ještě předtím, než se vůbec dostane do souboru

Slepé místo, které kontrola kontejneru nevidí

Tuto část je nutné pojmenovat napřímo, jelikož právě to je důvodem, proč se nám chyba tak schovává. Nástroj ValidateFacturXInvoice kontroluje pouze kontejner. Ověřuje, zda katalog obsahuje záznam /AF, zda je přítomen strom názvů EmbeddedFiles, zda existuje XML faktury, název vloženého souboru odpovídá profilu, guideline ID v XML souhlasí s úrovní shody a zda použitá relace /AFRelationship je jednou z těch, jež povoluje norma PDF/A-3. Jde o reálné kontroly a tyto kontroly odchytávají reálné chyby. Funkce GetFacturXValidationIssues je dokáže vypsat pod jejich jmény, a to včetně identifikátorů jako MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship a InvalidFileNameProfile

Nekontroluje se však, zda je přítomno a správně zapsáno rozšiřující schéma v XMP. Soubor, jehož kontejner je naprosto bezchybný, ale jehož vlastnosti fx nejsou deklarovány, projde každou z těchto kontrol a vrátí 1, poněvadž nic v tomto seznamu nezkoumá blok pdfaExtension:schemas. Přesně to je také ten důvod, proč ručně vytvořená faktura (nebo faktura vytvořená procesem, který zapsal blok vlastností bez deklarace) dokáže proplout přes náš vestavěný validátor, ale stále selže v nástroji veraPDF kvůli klauzuli 6.6.2.3.1. Validátor kontejneru a validátor metadat PDF/A zkrátka odpovídají na jiné otázky, přičemž na tu druhou dokáže odpovědět pouze komplexní kontrolní nástroj pro PDF/A

Jak číst problémy, abyste poznali, která vrstva se rozbila

Vzhledem k tomu, že obě vrstvy selhávají naprosto nezávisle na sobě, správným diagnostickým návykem je přečíst si nejdříve hlášené chyby z kontejneru a považovat čistý výsledek pouze za výpověď o samotném kontejneru, nikdy ne o metadatech PDF/A. Spusťte vestavěnou validaci, projděte si případný seznam nalezených problémů a postupujte podle něj ještě dříve, než se obrátíte na nějaký externí nástroj

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;

Pokud zmíněné volání vrátí určitý název problému, chyba se nachází v kontejneru a vypsaná zpráva vám obratem prozradí v které jeho části. Jestliže vrátí čistý výsledek a nástroj veraPDF i tak soubor odmítne, chybou je téměř vždy rozšiřující schéma pro XMP a samotná oprava se skrývá v tom, že necháte metodu AddFacturXAssociatedFileFromString zapsat metadata za vás, než abyste si blok vlastností stavěli po vlastní ose. Udržet tyto dvě otázky v hlavě striktně oddělené je přesně tím, co promění nepochopitelné zamítnutí v jednořádkovou diagnózu: problémy s kontejnerem vyplavou na povrch prostřednictvím výpisu problémů, problémy s deklarací schématu se objeví teprve v PDF/A validátoru a pouze jejich záměna dovolí chybě, aby se úspěšně maskovala

Širší obraz shody pro PDF/A a PDF/UA, včetně návodu na provedení preflight kontroly předtím, než soubor opustí váš build proces, je rozebrán v našem průvodci k PDF/A a PDF/UA preflight kontrolám v Delphi. Pakliže vaše faktura musí být zároveň přístupná, samotný strom struktury, na němž úroveň PDF/A-3a i tagované PDF závisí, je předmětem článku o přístupnosti v tagovaném PDF. Zpracování rozšiřujících schémat, které jsme zde podrobně popsali, je dodáváno jako nedílná součást produktu PDFlibPas Delphi PDF Library po boku podpory profilů pro Factur-X, ZUGFeRD a XRechnung, jež jsou dokumentovány napříč tímto blogem