Техническая статья

Схемы расширения 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 и встречает свойство с нераспознанным пространством имён, он ищет объявление, которое объяснило бы, что означает это свойство. Если такое объявление отсутствует, он сообщает об ошибке по ISO 19005-3 пункт 6.6.2.3.1, который требует, чтобы каждое свойство, не взятое из предопределённой схемы, было описано в схеме расширения PDF/A. Четыре необъявленных свойства - четыре причины для отклонения файла, и ни одна из них не видна при проверке контейнера

PDF Library for Delphi: валидатор ищет в четырёх предопределённых схемах XMP и не находит extension-схемы PDF/A для свойств fx, поэтому счёт Factur-X проваливается
veraPDF охотится за объявлением схемы, которого файл никогда не записывал, — четыре свойства fx, четыре шанса провалить пункт 6.6.2.3.1

Почему 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 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, использует другой URI в рамках своего собственного имени схемы. Схема расширения должна объявлять тот же URI пространства имён, который фактически использует блок свойств, иначе валидатор по-прежнему не сможет связать их. PDF Library for Delphi выводит оба значения из имени файла и версии, которые вы передаёте, так что объявление и блок свойств всегда согласованы

Как вспомогательный метод записывает обе части вместе

В PDF Library for Delphi вам не нужно собирать этот 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,                  // raw UTF-8 XML bytes
    'EN16931',                   // ConformanceLevel
    'factur-x.xml',              // имя встроенного файла
    'Factur-X invoice XML',      // /Desc text
    'Alternative',               // /AFRelationship
    '1.0',                       // profile version
    '');                         // необязательный код страны
  if FileID = 0 then
    Exit;                        // not PDF/A-3, or XML/profile mismatch

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

Один вызов AddFacturXAssociatedFileFromString выполняет работу, которой не хватало в файле с ошибкой. Он встраивает XML как связанный файл PDF/A-3 с указанным вами отношением и записывает четыре свойства fx вместе с именем схемы, URI пространства имён и префиксом для выбранного профиля. При сохранении документа внутренний шаг ApplyFacturXMetadata вставляет в пакет XMP и блок свойств, и соответствующее объявление pdfaExtension:schemas, так что пользовательские свойства поступают уже описанными. Метод возвращает 0, если документ не находится в режиме PDF/A-3 или если XML не соответствует заявленному профилю, что служит защитой от того, чтобы некорректный счёт попал в файл

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

Эту часть стоит назвать прямо, потому что именно она объясняет, почему ошибка прячется. ValidateFacturXInvoice проверяет контейнер. Он убеждается, что каталог имеет запись /AF, дерево имён EmbeddedFiles присутствует, XML счёта существует, имя встроенного файла совпадает с профилем, GuidelineID в 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 отвечает на второй

Анатомия extension-схемы PDF/A-3 в PDF Library for Delphi: объявление схемы Factur-X с URI пространства имён, префиксом fx и четырьмя внешними свойствами Text
Каждое заявление декларации должно совпадать с блоком fx, который счёт реально записывает, иначе валидатор всё равно отвергнет файл

Считывание проблем, чтобы понять, какой слой сломан

Поскольку два слоя могут давать сбой независимо друг от друга, правильная диагностическая привычка - сначала считывать проблемы контейнера и воспринимать чистый результат как утверждение только о контейнере, но не о метаданных 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, включая то, как запустить предпечатную проверку перед выходом файла из вашей сборки, рассмотрена в руководстве по предпечатной проверке PDF/A и PDF/UA. Если ваш счёт также должен быть доступным, структурное дерево, от которого зависят PDF/A-3a и тегированный PDF, является темой статьи о доступности тегированного PDF. Обработка схем расширения, описанная здесь, поставляется в составе PDF Library for Delphi Delphi PDF Library вместе с поддержкой профилей Factur-X, ZUGFeRD и XRechnung, документированной в этом блоге

PDF Library for Delphi: тот же счёт Factur-X проходит все проверки контейнера в ValidateFacturXInvoice, а veraPDF его отклоняет, потому что ни один слой не читает блок схем pdfaExtension
Валидатор контейнера и валидатор PDF/A отвечают на разные вопросы об одном и том же файле