Artykuł techniczny

Schematy rozszerzeń PDF/A-3 dla XMP Factur-X w Delphi

Zbudowałeś fakturę Factur-X i wszystkie sprawdzenia kontenera przechodzą. Katalog zawiera tablicę /AF, drzewo nazw EmbeddedFiles wskazuje na właściwą specyfikację pliku, osadzony factur-x.xml ma prawidłowy /AFRelationship wynoszący Alternative, a wbudowane ValidateFacturXInvoice zwraca 1. Następnie uruchamiasz ten sam plik przez veraPDF - narzędzie referencyjne, z którego korzystają portale podatkowe - i stwierdza ono, że cały dokument nie jest prawidłowym PDF/A-3. Struktura jest poprawna. Problem leży w metadanych, a błąd ten jest jednym z najłatwiejszych do przeoczenia w całym przepływie pracy z fakturami elektronicznymi

Przyczyna jest warta pełnego zrozumienia, ponieważ wyjaśnia klasę defektów PDF/A, która nie ma nic wspólnego z widoczną stroną ani załącznikiem, lecz wszystko ze sposobem, w jaki XMP opisuje siebie. To jest pułapka ukryta za zielonym sprawdzeniem kontenera

Cztery właściwości, które powodują odrzucenie pliku

Faktura Factur-X zapisuje cztery niestandardowe właściwości do pakietu XMP, aby oprogramowanie pośredniczące mogło odczytać profil faktury bez parsowania osadzonego XML. Żyją w przestrzeni nazw Factur-X pod prefiksem fx: fx:DocumentFileName, fx:DocumentType, fx:Version i fx:ConformanceLevel. To dokładnie te metadane, których czytnik potrzebuje, aby wiedzieć, że ten PDF zawiera fakturę EN 16931 o nazwie factur-x.xml w wersji 1.0

Żadna z tych czterech właściwości nie należy do żadnego schematu XMP, który PDF/A predefiniuje. Schematy Dublin Core, XMP Basic, PDF oraz identyfikacyjny PDF/A są znane zgodnemu czytnikowi, ale fx: już nie. Gdy veraPDF przechodzi przez XMP i trafia na właściwość o przestrzeni nazw, której nie rozpoznaje, szuka deklaracji, która powiedziałaby mu, co ta właściwość znaczy. Jeśli tej deklaracji brak, zgłasza niepowodzenie wobec klauzuli 6.6.2.3.1 ISO 19005-3, która wymaga, by każda właściwość niepochodząca z predefiniowanego schematu była opisana w schemacie rozszerzenia PDF/A. Cztery niezadeklarowane właściwości, cztery sposoby na odrzucenie pliku i ani jednego widocznego dla kontroli kontenera

PDF Library for Delphi: Walidator przeszukuje cztery predefiniowane schematy XMP i nie znajduje schematu rozszerzenia PDF/A dla właściwości fx, więc faktura Factur-X pada
veraPDF poluje na deklarację schematu, której plik nigdy nie zapisał — cztery właściwości fx to cztery szanse na niezaliczenie klauzuli 6.6.2.3.1

Dlaczego PDF/A odmawia gołej właściwości niestandardowej

Reguła wygląda pedancko, dopóki nie przypomnisz sobie, po co jest PDF/A. Ten format istnieje po to, by plik dało się otworzyć i zrozumieć za dziesięciolecia, oprogramowaniem, które nigdy nie słyszało o konwencjach z 2026 roku. Od zgodnego czytnika oczekuje się, że zrozumie dokument z samego dokumentu, bez zewnętrznego rejestru do konsultacji

Niestandardowe metadane łamią tę obietnicę, chyba że plik zawiera własny opis. Mając gołą właściwość fx:ConformanceLevel, przyszły czytnik nie może wiedzieć, do jakiego URI przestrzeni nazw wiąże się prefiks fx, czy wartość jest tekstem, datą czy liczbą całkowitą, ani czy właściwość opisuje sam dokument czy jakiś zasób zewnętrzny. Mechanizm schematu rozszerzeń PDF/A wypełnia tę lukę. Pozwala plikowi zadeklarować w stałej strukturze XMP przestrzeń nazw, prefiks i dla każdej właściwości typ wartości oraz kategorię internal lub external. Gdy ta deklaracja jest obecna, właściwość jest samoopisująca i klauzula 6.6.2.3.1 jest spełniona. Bez niej walidator nie ma wyboru i musi potraktować właściwość jako niezrozumiałą i odrzucić plik. Rozróżnienie kategorii ma tu znaczenie: właściwości faktury, takie jak te, opisują dane pochodzące spoza procesora PDF, więc są deklarowane jako external, a nie internal

Co zawiera deklaracja schematu rozszerzenia

Deklaracja to rdf:Description w pakiecie XMP, korzystające z trzech przestrzeni nazw zdefiniowanych przez AIIM: pdfaExtension, pdfaSchema i pdfaProperty. Wewnątrz worka pdfaExtension:schemas siedzi jeden wpis schematu, który nazywa schemat Factur-X, podaje jego pdfaSchema:namespaceURI i pdfaSchema:prefix, a potem wylicza cztery właściwości w sekwencji pdfaSchema:property. Każda właściwość niesie nazwę, pdfaProperty:valueType równy Text i pdfaProperty:category równą external. Przykładowy markup poniżej pokazuje kształt tego bloku

Anatomia schematu rozszerzenia PDF/A-3 w PDF Library for Delphi: deklaracja schematu Factur-X z URI przestrzeni nazw, prefiksem fx i czterema zewnętrznymi właściwościami Text
Każde twierdzenie deklaracji musi się zgadzać z blokiem fx, który faktura faktycznie zapisuje, bo inaczej walidator nadal odrzuci plik
<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 przestrzeni nazw i prefiks nie są stałymi ciągami. Są zależne od profilu. Dokument Factur-X używa urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# z prefiksem fx, podczas gdy plik ZUGFeRD 2.0 wybrany przez zugferd-invoice.xml wskazuje na inny URI pod własną nazwą schematu. Schemat rozszerzenia musi deklarować ten sam URI przestrzeni nazw, który faktycznie używa blok właściwości, w przeciwnym razie walidator nadal nie może połączyć obu. PDF Library for Delphi wyprowadza obie wartości z nazwy pliku i wersji, które przekazujesz, więc deklaracja i blok właściwości zawsze się zgadzają

Jak pomocnik zapisuje obie połowy razem

W PDF Library for Delphi nie składasz tego XML ręcznie. Umieszczasz dokument w trybie PDF/A-3 i wywołujesz jedną metodę. Pierwszą rzeczą do ustalenia jest flaga zgodności, ponieważ Factur-X wymaga PDF/A-3. Wywołanie SetPDFAMode(7) wybiera poziom PDF/A-3u, który ustawia pdfaid:part na 3 i pdfaid:conformance na U w schemacie identyfikacji. Pakiet XMP zawiera teraz właściwą część i zgodność przed dodaniem jakichkolwiek metadanych faktury

var
  FileID: Integer;
begin
  PDF.SetPDFAMode(7);            // PDF/A-3u: pdfaid:part=3, conformance=U
  PDF.NewDocument;
  // tutaj narysuj stronę faktury czytelną dla człowieka

  FileID := PDF.AddFacturXAssociatedFileFromString(
    InvoiceXML,                  // raw UTF-8 XML bytes
    'EN16931',                   // ConformanceLevel
    'factur-x.xml',              // nazwa osadzonego pliku
    'Factur-X invoice XML',      // /Desc text
    'Alternative',               // /AFRelationship
    '1.0',                       // profile version
    '');                         // opcjonalny kod kraju
  if FileID = 0 then
    Exit;                        // not PDF/A-3, or XML/profile mismatch

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

Pojedyncze wywołanie AddFacturXAssociatedFileFromString wykonuje pracę, której brakowało w wadliwym pliku. Osadza XML jako plik powiązany PDF/A-3 z podaną relacją i zapisuje cztery właściwości fx wraz z nazwą schematu, URI przestrzeni nazw i prefiksem dla wybranego profilu. Gdy dokument jest zapisywany, wewnętrzny krok o nazwie ApplyFacturXMetadata wstrzykuje zarówno blok właściwości, jak i pasującą deklarację pdfaExtension:schemas do pakietu XMP, więc niestandardowe właściwości przybyją już opisane. Metoda zwraca 0, jeśli dokument nie jest w trybie PDF/A-3 lub XML nie pasuje do zadeklarowanego profilu - to ta sama ochrona, która zapobiega trafieniu wadliwej faktury do pliku

Co pokrywa kontrola kontenera

Ten fragment warto trzymać pojętego, bo właśnie dlatego problem się ukrywa. ValidateFacturXInvoice sprawdza kontener. Potwierdza, że katalog ma wpis /AF, że drzewo nazw EmbeddedFiles istnieje, że XML faktury jest obecny, że nazwa osadzonego pliku pasuje do profilu, że ID wytycznych w XML zgadza się z poziomem zgodności i że /AFRelationship jest jedną z dozwolonych przez PDF/A-3. To są prawdziwe kontrole i łapią prawdziwe defekty. GetFacturXValidationIssues raportuje je z nazwiska, identyfikatorami takimi jak MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship i InvalidFileNameProfile

Schemat rozszerzenia XMP należy do innej warstwy. Plik, którego kontener jest bez skazy, ale właściwości fx niezadeklarowane, przechodzi każdą kontrolę kontenera i zwraca 1, bo deklarowanie właściwości XMP to pytanie o metadane PDF/A, a nie o kontener. Dokładnie dlatego faktura zbudowana ręcznie albo przez potok, który zapisał blok właściwości bez deklaracji, może przejść walidację kontenera i nadal paść w veraPDF na klauzuli 6.6.2.3.1. Walidator kontenera i walidator metadanych PDF/A odpowiadają na różne pytania, a na drugie odpowiada tylko pełny checker PDF/A

PDF Library for Delphi: Ta sama faktura Factur-X przechodzi każdą kontrolę kontenera w ValidateFacturXInvoice, podczas gdy veraPDF ją odrzuca, bo blok schematów pdfaExtension to pytanie o metadane PDF/A poza kontrolami kontenera
Walidator kontenera i walidator PDF/A odpowiadają na różne pytania o ten sam plik

Odczytywanie problemów, aby wiedzieć, która warstwa się zepsuła

Bo obie warstwy zawodzą niezależnie, właściwym nawykiem diagnostycznym jest najpierw przeczytać problemy kontenera i traktować czysty wynik jako oświadczenie wyłącznie o kontenerze, nigdy o metadanych PDF/A. Uruchom wbudowaną walidację, zbierz listę problemów i zareaguj na nią, zanim sięgniesz po narzędzie zewnętrzne

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;

Gdy to wywołanie zwraca nazwę problemu, usterka siedzi w kontenerze, a komunikat mówi, której części. Gdy zwraca czysty wynik, a veraPDF nadal odrzuca plik, usterką jest prawie zawsze schemat rozszerzenia XMP, a lekarstwo to pozwolić AddFacturXAssociatedFileFromString zapisać metadane, zamiast składać blok właściwości samodzielnie. Rozdzielenie tych dwóch pytań w głowie zamienia zagadkowe odrzucenie w diagnozę jednej linijki: problemy kontenera wychodzą przez listę problemów, problemy deklaracji schematu wychodzą tylko przez walidator PDF/A, a mylenie ich jest tym, co pozwala problemowi się ukrywać

Szerszy obraz zgodności PDF/A i PDF/UA, w tym jak uruchomić wstępną inspekcję przed opuszczeniem pliku przez build, jest omówiony w przewodniku inspekcji wstępnej PDF/A i PDF/UA. Jeśli twoja faktura musi być też dostępna, drzewo struktury, od którego zależy PDF/A-3a i otagowany PDF, jest tematem artykułu o dostępności otagowanego PDF. Obsługa schematu rozszerzenia opisana tutaj jest dostarczana jako część PDF Library for Delphi Delphi PDF Library wraz z obsługą profili Factur-X, ZUGFeRD i XRechnung udokumentowaną w tym blogu