PDF/A и PDF/UA отвечают на два вопроса, не имеющих друг к другу никакого отношения, и обращение с ними как с одним общим флажком «доступность и архивирование» — вот как сломанные файлы попадают в архив под ярлыком соответствия. PDF/A спрашивает, будет ли файл по-прежнему точно отображаться через двадцать лет. PDF/UA спрашивает, может ли вспомогательная технология прочитать его уже сегодня. Документ может уверенно пройти одну проверку и провалить другую, поэтому единственный честный вердикт получается только при прогоне обеих проверок, причём до того, как файл записан на выход, а не после того, как нижестоящая система поверит идентификатору соответствия, запечённому в её метаданных. Этот идентификатор — самодекларация. Ничто в формате не требует, чтобы он был правдой, и приложение, записывающее «PDF/A-1b» в XMP без проверки по стандарту, производит файл, который выглядит соответствующим для любого потребителя, читающего только ярлык. losLab PDF Library (PDF Library for Delphi) закрывает этот разрыв для Delphi и C++Builder, встраивая оба валидатора прямо в библиотеку, так что проверка выполняется в том же процессе без необходимости поднимать внешний сервис
Два стандарта, которые браковывают файлы по противоположным причинам
ISO 19005 (PDF/A) — это контракт на воспроизведение. Соответствующий файл обязан отображаться идентично через десятилетия на программном обеспечении, которое никогда не видело систему, его создавшую, поэтому правила атакуют внешние зависимости: каждый шрифт встроен, цвет привязан к встроенному ICC-профилю OutputIntent или выражен в независимом от устройства пространстве, никакого шифрования в PDF/A-1, никакого JavaScript, метаданные XMP согласуются со словарём информации о документе. ISO 14289 (PDF/UA), напротив, — это контракт на семантику. Вспомогательная технология должна пройти по документу и вынести из него смысл, который живёт в совершенно другом слое: полное дерево структуры, альтернативный текст для рисунков, заголовок документа, заданный для отображения, уровни заголовков без пропусков, связи заголовков таблиц, переживающие момент, когда страница уходит с экрана
Поскольку два стандарта следят за разными слоями, кусают именно те файлы, что сидят между ними. Идеальный с точки зрения архива документ может быть совершенно немым для программы чтения с экрана. Прекрасно размеченный документ может ссылаться на настольный шрифт, которого через десять лет уже не будет. Публикации госсектора — обычное место, где оба требования предъявляются одновременно, и конвейер там не может схлопнуть их в один общий шлюз. Находки уходят разным людям. Невстроенные шрифты — это дефект в коде, генерирующем PDF, тогда как отсутствующий альтернативный текст относится к тому, кто владеет шаблонами контента, а отчёт, смешивающий оба типа находок, просто пересылается дважды
То, на какую часть PDF/A вы целитесь, имеет не меньшее значение, чем сам факт попадания в неё. PDF/A-1 заморожен на PDF 1.4 и отвергает прозрачность и JPEG2000 — то и другое современный вывод отчётов использует не задумываясь. PDF/A-2 (ISO 19005-2, построенный на ISO 32000-1) принимает оба и является разумным значением по умолчанию для нового архива. PDF/A-3 идёт дальше и разрешает встроенные файлы любого типа, на что и опираются регулируемые форматы электронных счетов-фактур. Команда, всё ещё стандартизирующаяся на PDF/A-1b в 2026 году, обычно несёт на себе требование, которое кто-то написал пятнадцать лет назад, а пересогласовать целевую часть зачастую дешевле, чем вычищать прозрачность из каждой диаграммы, которую выдаёт система
Структурированные находки на этапе приёмки
Точка входа плоского API — CheckFileCompliance, с селектором теста 1 для PDF/A и 2 для PDF/UA. Она возвращает дескриптор списка строк, элементы которого — отдельные находки, по одной на строку, и это ровно та форма, по которой хочет проходить автоматический шлюз:
function GateArchiveUpload(Pdf: TPDFlib; const FileName: string): Boolean;
var
ListId, I: Integer;
begin
ListId := Pdf.CheckFileCompliance(FileName, '', 1, 0); // 1 = PDF/A
if ListId = 0 then
begin
// 0 означает и «находок нет», и «файл нечитаем» — разрешите неоднозначность до передачи дальше
Result := Pdf.LastErrorCode = 0;
Exit;
end;
for I := 0 to Pdf.GetStringListCount(ListId) - 1 do
LogFinding(FileName, Pdf.GetStringListItem(ListId, I));
Pdf.ReleaseStringList(ListId);
Result := False;
end;
Два нюанса решают, будет ли это работать без присмотра. Первый — возвращаемое значение, означающее две противоположные вещи. CheckFileCompliance возвращает 0, когда файл полностью соответствует стандарту, и точно так же 0, когда файл вообще не удалось открыть, потому что внутри пустой список результатов в обоих случаях схлопывается в 0. Шлюз, читающий 0 как «прошёл», пропустит битые загрузки прямиком в архив, поэтому разрешайте эту неоднозначность через LastErrorCode, прежде чем доверять нулю, — так, как это делает шлюз выше. Второй нюанс касается того, на каком этапе жизненного цикла находится файл. Проверяльщик работает на потоковом читателе библиотеки, а не на полной модели документа, открывая файл напрямую с разделяемым доступом на чтение и ни разу не вызывая LoadFromFile, поэтому он способен прогрызть многогигабайтный вход, не строя дерево объектов. То же самое потоковое открытие терпит неудачу, пока другой процесс всё ещё держит файл на запись, а загрузка в процессе — это ровно такое состояние. Ставьте шлюз после завершения передачи
Потоковая архитектура снова окупается под нагрузкой. Каждая проверка открывает свой вход только для чтения и делится им для чтения, так что аудит корпуса документов масштабируется по рабочим потокам или процессам — по одному экземпляру TPDFlib на каждый воркер, без конкуренции между ними. Ресурс, требующий дисциплины, — это сам дескриптор. Любой ненулевой результат от CheckFileCompliance остаётся выделенным, пока вы не вызовете ReleaseStringList, а долго работающий шлюз, забывающий их освобождать, не падает — он просто медленно истекает памятью, пока кто-нибудь не пойдёт разбираться, почему
Отчёты для людей, диффы для шлюзов сборки
Список находок — подходящая форма для шлюза и неподходящая форма для письма команде, отвечающей за шаблоны. CreatePreflightReport отображает тот же анализ в виде читаемой прозы, CreatePreflightReportEx добавляет селектор формата отчёта, а SavePreflightReport записывает его на диск, чтобы отчёт мог путешествовать внутри поставляемого пакета документов. Немало архивных контрактов делают этот отчёт самостоятельным поставляемым артефактом, а не просто внутренним рабочим файлом
Тот член этого семейства, что тихо оправдывает своё место, — ComparePreflightReports. Соответствие стандарту — это такая же поверхность для регрессий, как и любое другое поведение. Правка шаблона, недавно приобретённая корпоративная лицензия на шрифт или обновление библиотеки — любое из этого может внести находку, которой не было в прошлом релизе, и ни одно из них само о себе не заявит. Держите эталонные отчёты для набора репрезентативных документов под контролем версий, перегенерируйте их после каждого изменения и запускайте ComparePreflightReports для вычисления разницы. Пустой дифф — это артефакт релиза, который стоит хранить. Неожиданная находка ломает сборку, а это гораздо более дешёвое место для её обнаружения, чем аудит
Генерация вывода, проходящего проверку с первого раза
Preflight оправдывает себя на файлах, приходящих извне. Для документов, которые производит ваш собственный код, обнаруживать нарушения после генерации и латать их задним числом — медленный путь. В PDF Library for Delphi есть режим на стороне генерации для каждого стандарта, и оба можно включить для одного и того же документа:
var
Pdf: TPDFlib;
Diag: WideString;
begin
Pdf := TPDFlib.Create;
try
Pdf.NewDocument;
Pdf.SetPDFAMode(1);
Pdf.LoadOutputIntentProfile('sRGB-IEC61966-2.1.icc', 'RGB');
Pdf.SetPDFUAMode('en-US');
Pdf.SetInformation(1, 'Quarterly Statement'); // /Title: обязателен для PDF/UA
// ... здесь рисуется размеченный контент ...
Diag := Pdf.GetPDFUADiagnostics;
if Diag <> '' then
Writeln('fix before shipping: ', Diag);
Pdf.SaveToFile('statement.pdf');
// значение имеет только preflight, запущенный на сохранённом файле:
Writeln(Pdf.CreatePreflightReport('statement.pdf', '', 1, 0));
finally
Pdf.Free;
end;
end;
Ловушка прячется в момент сохранения. Несколько исправлений для соответствия стандарту происходят именно во время сериализации документа, а не в момент включения режима: принудительная установка флага печати на аннотациях, запись значения AFRelationship по умолчанию для встроенных файлов PDF/A-3, нормализация порядка обхода полей и описаний полей форм для PDF/UA. Документ, лежащий в памяти, не побайтно идентичен тому, что попадает на диск, поэтому единственный имеющий смысл вердикт preflight — это тот, что вычислен по сохранённому файлу. Проверяйте сам statement.pdf. Не делайте вывод о соответствии по объекту, всё ещё находящемуся в памяти, потому что байты, которые вы бы оценивали, — не те байты, что вы поставили
Сценарии выставления счетов, несущие машиночитаемый XML рядом с визуальным документом, следуют схеме ZUGFeRD и Factur-X, построенной на PDF/A-3. Для них нужно явно задать связь вложения через SetPDFA3DefaultAFRelationship, поскольку ISO 19005-3 требует, чтобы каждый встроенный файл объявлял свою роль по отношению к документу. Оставьте её незаданной, и встроенный XML станет просто бинарным блобом без заявленной цели, что валидатор непременно заметит
Независимые арбитры: veraPDF и Acrobat
Производитель не должен быть единственным судьёй собственного вывода. Проверяльщики PDF Library for Delphi дают быстрые структурированные вердикты прямо в процессе, а это именно то, что нужно на горячем пути, но релизный шлюз для архивного пакета всё же должен пропускать вывод через валидатор, который никто в вашей команде не писал. veraPDF — это поддерживаемая сообществом эталонная реализация для PDF/A и инструмент, который большинство архивов называют в своих критериях приёмки, так что именно на него и нужно ориентироваться. Профили preflight в Acrobat служат полезным арбитром, когда veraPDF и внутрипроцессная проверка расходятся во мнениях. Записывайте имя валидатора и его версию рядом с каждым сохранённым отчётом. Утверждение, что файл прошёл проверку veraPDF, мало что говорит без номера сборки, которая его пропустила, поскольку инструмент ужесточает свои правила от релиза к релизу
Валидаторы действительно расходятся во мнениях на границах стандартов, и в этом случае правильный ответ — не выбрать тот инструмент, который вам нравится больше. Сократите файл до минимального образца, который всё ещё вызывает расхождение, и прочитайте его против текста стандарта. Час такой работы обычно выявляет одно из двух: настоящий баг инструмента, который стоит завести в апстрим, или пункт стандарта, который ваша команда читала неправильно и который стоит записать в заметках по соответствию, чтобы следующий человек не пересматривал этот спор заново
Для зашифрованного входа есть короткий путь. Обе проверки принимают аргумент с паролем, но файл PDF/A-1 со словарём шифрования уже не соответствует стандарту, потому что ISO 19005-1 прямо запрещает шифрование, так что зашифрованную загрузку можно отклонить ещё до запуска какого-либо более глубокого анализа. Разобраться в том, что именно предоставляет словарь шифрования, — отдельная задача, рассмотренная в статье об аудите шифрования и разрешений PDF
Находки PDF/UA почти всегда восходят к тому, как изначально было создано дерево структуры, а техники разметки, стоящие за этим, описаны в статье о построении размеченных деревьев структуры PDF в Delphi. Архивам, которые также требуют цифровые подписи, стоит связать этот шлюз с рабочим процессом из статьи о подписании и проверке PAdES. Полный справочник API preflight находится на странице продукта losLab PDF Library for Delphi