Technical Article

Схеми за разширение на PDF/A-3 за Factur-X XMP в Delphi

Изградили сте фактура Factur-X и всяка проверка на контейнера преминава успешно. Каталогът съдържа масив /AF, дървото с имена EmbeddedFiles се разпознава до правилната файлова спецификация, вграденият factur-x.xml има правилната връзка (relationship) /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 са известни на съответстващия четец (conforming reader), но fx: не е. Когато veraPDF обхожда XMP и достигне до свойство, чието пространство от имена не разпознава, той търси декларация, която би му казала какво означава свойството. Ако тази декларация липсва, той съобщава за провал спрямо клауза 6.6.2.3.1 на ISO 19005-3, която изисква всяко свойство, което не е извлечено от предварително дефинирана схема, да бъде описано в схема за разширение (extension schema) на 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 (bag) стои един запис за схема, който назовава схемата Factur-X, дава нейния pdfaSchema:namespaceURI и pdfaSchema:prefix, и след това изброява четирите свойства в последователност (sequence) 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 declared the same way -->
          </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, се разпознава (resolves) до различно 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 пакетът вече носи правилната част (part) и съответствие (conformance), преди да бъдат добавени каквито и да е метаданни на фактурата

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;

Едно единствено извикване към AddFacturXAssociatedFileFromString върши работата, която липсваше в проваления файл. То вгражда XML като асоцииран файл за PDF/A-3 с връзката (relationship), която сте посочили, и записва четирите свойства fx заедно с името на схемата, URI на пространството от имена и префикса за избрания профил. Когато документът бъде запазен, една вътрешна стъпка, наречена ApplyFacturXMetadata, инжектира както блока със свойства, така и съвпадащата декларация pdfaExtension:schemas в XMP пакета, така че персонализираните свойства пристигат вече описани. Методът връща 0, ако документът не е в режим PDF/A-3 или ако XML не съответства на декларирания профил, което е същата защита, която спира лошо оформена фактура да достигне до файла на първо място

Сляпото петно, което проверката на контейнера не може да види

Това е частта, която трябва да се назове ясно, защото тя е причината бъгът да се крие. ValidateFacturXInvoice проверява контейнера. То потвърждава, че каталогът има запис /AF, че дървото с имена EmbeddedFiles присъства, че XML фактурата съществува, че името на вградения файл съвпада с профила, че ID-то на ръководството (guideline ID) в XML-а съвпада с нивото на съответствие и че /AFRelationship е такова, каквото PDF/A-3 позволява. Това са реални проверки и те улавят реални дефекти. GetFacturXValidationIssues ги съобщава по име, с идентификатори като MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship и InvalidFileNameProfile

Това, което функцията не проверява, е дали XMP схемата за разширение присъства и е правилна. Файл, чийто контейнер е безупречен, но чиито свойства fx са недекларирани, преминава всяка проверка за проблем и връща 1, защото нищо в този списък не инспектира блока pdfaExtension:schemas. Точно затова ръчно изградена фактура или такава, произведена от работен процес (pipeline), който е записал блока със свойства без декларацията, може да премине гладко през вградения валидатор и въпреки това да се провали във 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('|');
    // 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;

Когато това извикване върне име на проблем, грешката е в контейнера и съобщението ви казва в коя част. Когато се върне чисто и veraPDF все още отхвърля файла, грешката почти винаги е в XMP схемата за разширение и поправката е да оставите AddFacturXAssociatedFileFromString да запише метаданните, вместо да конструирате блока със свойства сами. Поддържането на двата въпроса отделно в собствения ви ум е това, което превръща едно озадачаващо отхвърляне в диагноза на един ред: проблемите с контейнера изплуват чрез списъка с проблеми, проблемите с декларацията на схемата изплуват само чрез валидатор за PDF/A, а объркването на двете е това, което позволява на бъга да се скрие

По-широката картина на съответствието с PDF/A и PDF/UA, включително как да стартирате pass за preflight, преди файлът да напусне вашия билд, е обхваната в ръководството за PDF/A и PDF/UA preflight. Ако вашата фактура също трябва да бъде достъпна (accessible), дървото на структурата, от което зависят PDF/A-3a и тагнатият PDF, е предмет на статията за структура на достъпността на тагнат PDF. Обработката на схеми за разширение, описана тук, се доставя като част от PDFlibPas Delphi PDF Library заедно с поддръжката на профили за Factur-X, ZUGFeRD и XRechnung, документирани в този блог