Вы создали счёт 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, документированной в этом блоге