Teknisk artikel

PDF/A-3-tilläggsscheman för Factur-X XMP i Delphi

Du har byggt en Factur-X-faktura och varje container-kontroll blir godkänd (passes). Katalogen bär en /AF-array, namnträdet EmbeddedFiles matchar rätt filspecifikation, den inbäddade factur-x.xml har ett korrekt /AFRelationshipAlternative, och den inbyggda (built-in) ValidateFacturXInvoice returnerar 1. Sedan kör du samma fil genom veraPDF, referenskontrollanten (reference checker) som skatteportaler använder, och den beslutar att (rules) hela dokumentet inte är en giltig PDF/A-3. Strukturen är rätt. Metadatan är problemet, och det felet är ett av de enklaste att missa i hela arbetsflödet för e-fakturor

Orsaken är värd att förstå fullt ut, eftersom den förklarar en klass av PDF/A-defekter som inte har någonting att göra med den synliga sidan eller bilagan (attachment), och allt att göra med hur XMP beskriver sig självt. Det här är fällan som gömmer sig bakom en grön container-kontroll

De fyra egenskaperna som fäller filen

En Factur-X-faktura skriver fyra anpassade egenskaper (custom properties) i sitt XMP-paket så att programvara längre ned i kedjan (downstream software) kan läsa fakturaprofilen utan att tolka (parsing) den inbäddade XML:en. De lever i Factur-X-namnrymden under fx-prefixet: fx:DocumentFileName, fx:DocumentType, fx:Version och fx:ConformanceLevel. De är exakt den metadata en läsare behöver för att veta att denna PDF bär en EN 16931-faktura med namnet factur-x.xml vid version 1.0

Ingen av dessa fyra egenskaper är en del av något XMP-schema som PDF/A fördefinierar (predefines). Scheman för Dublin Core, XMP Basic, PDF och PDF/A-identifiering är kända för en överensstämmande läsare (conforming reader), men fx: är det inte. När veraPDF vandrar igenom (walks) XMP:n och når en egenskap vars namnrymd (namespace) den inte känner igen, letar den efter en deklaration som skulle berätta för den vad egenskapen betyder. Om den deklarationen saknas (absent) rapporterar den ett fel gentemot ISO 19005-3 klausul 6.6.2.3.1, som kräver att varje egenskap som inte hämtats (drawn) från ett fördefinierat schema beskrivs i ett PDF/A-tilläggsschema (PDF/A extension schema). Fyra odeklarerade egenskaper, fyra sätt för filen att bli avvisad (rejected), och inte ett enda av dem är synligt för en container-kontroll

Varför PDF/A vägrar en naken anpassad egenskap

Regeln ser pedantisk ut tills du kommer ihåg vad PDF/A är till för. Formatet existerar så att en fil kan öppnas och förstås årtionden (decades) från nu, av programvara som aldrig fick veta om 2026 års konventioner. En överensstämmande läsare förväntas bli klok på (make sense of) dokumentet enbart (alone) utifrån dokumentet, utan något externt register att rådfråga (consult)

Anpassad metadata bryter det löftet såvida inte (unless) filen bär sin egen beskrivning. Givet en naken (bare) fx:ConformanceLevel-egenskap kan en framtida läsare inte veta vilken namnrymds-URI som fx-prefixet binder till, om (whether) värdet är text eller ett datum eller ett heltal, eller om egenskapen beskriver dokumentet självt eller någon extern resurs. PDF/A-tilläggsschema-mekanismen (PDF/A extension schema mechanism) stänger den luckan. Den låter filen deklarera, i en fast (fixed) XMP-struktur, namnrymden, prefixet, och för varje egenskap en värdetyp och en kategori av internal eller external. Så snart (Once) den deklarationen är närvarande är egenskapen självbeskrivande, och klausul 6.6.2.3.1 är uppfylld. Utan den har valideraren inget annat val än att behandla egenskapen som obegriplig (unintelligible) och fälla filen (fail the file). Kategori-distinktionen (category distinction) spelar roll här: faktura-egenskaper som dessa beskriver data som kommer från utanför PDF-processorn, så de deklareras som external snarare än internal

Vad tilläggsschema-deklarationen innehåller

Deklarationen är en rdf:Description i XMP-paketet som använder de tre AIIM-definierade namnrymderna pdfaExtension, pdfaSchema och pdfaProperty. Inuti en pdfaExtension:schemas-bag sitter en schema-post (schema entry) som namnger Factur-X-schemat, ger dess pdfaSchema:namespaceURI och pdfaSchema:prefix, och sedan listar de fyra egenskaperna i en pdfaSchema:property-sekvens. Varje egenskap bär ett namn, en pdfaProperty:valueType av typen Text, och en pdfaProperty:category satt till external. Den illustrativa markeringen (markup) nedan visar formen (shape) på det blocket

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

Namnrymds-URI:n och prefixet är inte fasta strängar. De följer profilen. Ett Factur-X-dokument använder urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# med prefixet fx, medan en ZUGFeRD 2.0-fil som valts genom zugferd-invoice.xml mappar till en annan URI under sitt eget schemanamn. Tilläggsschemat måste deklarera samma namnrymds-URI som egenskap-blocket faktiskt använder, annars kan valideraren fortfarande inte koppla ihop de två. PDFlibPas härleder (derives) båda värdena från filnamnet och versionen du skickar med, så deklarationen och egenskap-blocket (property block) stämmer alltid överens

Hur hjälparen (the helper) skriver båda halvor tillsammans

I PDFlibPas monterar (assemble) du inte ihop den XML:en för hand. Du sätter dokumentet i ett PDF/A-3-läge och anropar en metod. Den första saken att ordna (settle) är överensstämmelseflaggan (conformance flag), eftersom Factur-X kräver PDF/A-3. Att anropa SetPDFAMode(7) väljer PDF/A-3u-nivån, vilket sätter pdfaid:part till 3 och pdfaid:conformance till U i identifierings-schemat. XMP-paketet bär nu rätt del och överensstämmelse (part and conformance) innan någon faktura-metadata läggs till

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;

Ett enda anrop till AddFacturXAssociatedFileFromString gör det arbete som den fällande filen (failing file) saknade. Det inbäddar XML:en som en associerad PDF/A-3-fil med relationen du namngav, och det registrerar de fyra fx-egenskaperna tillsammans med schemanamnet, namnrymds-URI:n och prefixet för den valda profilen. När dokumentet sparas injicerar (injects) ett internt steg, vid namn ApplyFacturXMetadata, både egenskap-blocket och den matchande pdfaExtension:schemas-deklarationen in i XMP-paketet, så att de anpassade egenskaperna anländer redan beskrivna. Metoden returnerar 0 om dokumentet inte befinner sig i ett PDF/A-3-läge eller om XML:en inte matchar den deklarerade profilen, vilket är samma vakt (guard) som stoppar en felformaterad (malformed) faktura från att nå filen över huvud taget (in the first place)

Den döda vinkeln (blind spot) som container-kontrollen inte kan se

Detta är den del som bör namnges klarspråkigt (plainly), eftersom det är anledningen till att buggen gömmer sig. ValidateFacturXInvoice kontrollerar containern. Den bekräftar att katalogen har en /AF-post, att namnträdet EmbeddedFiles är närvarande, att fakturans XML existerar, att det inbäddade filnamnet matchar profilen, att riktlinje-ID:t (guideline ID) i XML:en stämmer överens med överensstämmelsenivån (conformance level), och att /AFRelationship är en som PDF/A-3 tillåter. Det där är riktiga kontroller och de fångar (catch) riktiga defekter. GetFacturXValidationIssues rapporterar dem vid namn, med identifierare som (such as) MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship och InvalidFileNameProfile

Vad den inte kontrollerar är huruvida XMP-tilläggsschemat är närvarande och korrekt. En fil vars container är felfri (flawless) men vars fx-egenskaper är odeklarerade går igenom (passes) varje issue-kontroll och returnerar 1, eftersom ingenting i den listan inspekterar pdfaExtension:schemas-blocket. Det är precis (precisely) därför en handbyggd faktura, eller en som producerats av en pipeline som skrev egenskap-blocket utan deklarationen, kan segla igenom den inbyggda valideraren (built-in validator) och likväl (still) falla i veraPDF på klausul 6.6.2.3.1. Container-valideraren och PDF/A-metadata-valideraren svarar på olika frågor, och endast den fullständiga PDF/A-kontrollanten svarar på den andra (second one)

Att läsa av fel (issues) så att du vet vilket lager som gick sönder (broke)

Eftersom de två lagren (layers) fallerar oberoende (independently), är den rätta diagnostiska vanan att läsa av container-felen först, och behandla ett rent (clean) resultat som ett utlåtande enbart om containern, aldrig om PDF/A-metadatan. Kör den inbyggda valideringen, samla in (collect) fellistan och agera (act on it) på den innan du sträcker dig efter ett externt verktyg

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;

När det anropet returnerar ett problem-namn (issue name) ligger felet i containern, och meddelandet berättar för dig vilken del. När den returnerar rent (clean) och veraPDF ändå avvisar filen, är felet nästan alltid XMP-tilläggsschemat, och lösningen (fix) är att låta AddFacturXAssociatedFileFromString skriva metadatan snarare än att konstruera egenskap-blocket själv. Att hålla isär (Keeping separate) de två frågorna i ditt eget sinne (mind) är vad som förvandlar en förbryllande (baffling) avvisning till en enrads-diagnos (one-line diagnosis): container-problem flyter upp (surface) genom fellistan, schema-deklarations-problem flyter endast upp genom en PDF/A-validerare, och att blanda ihop de två är det som låter buggen gömma sig

Den bredare PDF/A- och PDF/UA-efterlevnadsbilden (conformance picture), inklusive hur man kör ett preflight-pass innan en fil lämnar ditt bygge, täcks (covered) i genomgången om (walkthrough) PDF/A- och PDF/UA-preflight. Om din faktura också måste vara tillgänglighetsanpassad (accessible), är det strukturträd som PDF/A-3a och taggad PDF förlitar sig på ämnet (subject) för artikeln om tillgänglighet med taggad PDF. Hanteringen av tilläggsscheman som beskrivs här skeppas som en del av PDFlibPas Delphi PDF Library vid sidan av det stöd för Factur-X-, ZUGFeRD- och XRechnung-profiler som dokumenterats över denna blogg