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

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

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

В PDFlibPas вам не нужно собирать этот 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;
  // 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 с указанным вами отношением и записывает четыре свойства 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 отвечает на второй

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

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