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

Преобразование PDF в PDF/A и восстановление его метаданных

ConvertToPDFA превращает обычный документ в архивный одним вызовом: удаляет то, что запрещено выбранной частью, добавляет то, что она требует, заявляет часть, на которую претендует документ, и затем проверяет результат. Претензия сообщается как выполненная, только если проверка проходит, а GetPDFAConversionReport перечисляет, что было сделано и что всё ещё стоит на пути

Именно последнее свойство — то проектное решение, на котором стоит задержаться. Конвертер, штампующий претензию без проверки, хуже, чем отсутствие конвертера вообще, потому что файл, заявляющий архивность и не являющийся таковым, беспрепятственно проходит именно те системы, которые иначе поймали бы его. Сбой всплывёт спустя годы, при аудите, на документе, который никто не может восстановить

Почему корректный на вид PDF не проходит проверку PDF/A?

Чаще всего потому, что два места, где PDF сообщает, кто его создал, расходятся. Валидатор читает и словарь информации документа, и XMP-пакет, отклоняя файл, где они различаются, причем большинство файлов, спотыкающихся об этом, просто никогда не имели записанной XMP-половины

RepairDocumentMetadata приводит их к согласию и возвращает, сколько записей было восстановлено. Если значение несёт только одна половина, вторая заполняется из неё, поэтому ничего уже записанного не выбрасывается. Никому не нужно решать, какая копия авторитетна, потому что на практике одна копия пуста

В том же вызове есть второе восстановление, которое ловит более тонкий случай. Документ, переведённый в режим PDF/A, получает восстановленную идентификацию стандартов, если она была утеряна — это происходит всякий раз, когда вызывающий код передаёт собственный XMP-пакет. Без этой идентификации валидатор читает файл как обычный PDF и сообщает о каждом правиле заявленной части как о невыполненном — зрелищный на вид сбой с одной малой причиной

var
  Lib: TPDFlib;
  Repaired: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('incoming.pdf', '');
    Repaired := Lib.RepairDocumentMetadata;
    Log(Format('%d metadata entries brought into agreement', [Repaired]));
    Lib.SaveToFile('incoming-fixed.pdf');
  finally
    Lib.Free;
  end;
end;

Выбор части перед конвертацией

SetPDFAMode и ConvertToPDFA используют одинаковую нумерацию режимов, и три из значений недавние. Режим 9 — это PDF/A-4, часть, построенная на PDF 2.0. Режим 10 — PDF/A-4e, дополнительно допускающий 3D и мультимедиа, а режим 11 — PDF/A-4f, допускающий встроенный файл любого формата

Часть 4 идентифицирует себя иначе, чем предыдущие части: номером части и годом публикации, без буквы соответствия для обычного PDF/A-4 и с буквой E или F для двух расширений. Проверка распознаёт часть 4, оценивает её файлы по PDF 2.0, а не 1.7, и сообщает о файле части 4, не заявляющем год редакции

Каждый встроенный файл в документе части 4 указывает, как он относится к документу, как того требуют и части 3, и 4. Это правило, на котором раньше спотыкались обычные вложения: отношение записывалось только для вложений после первого и никогда для последнего, поэтому документ с единственным вложением — самый частый случай — не нёс его вовсе и проваливал валидацию именно по этому пункту

var
  Verdict: Integer;
begin
  Lib.LoadFromFile('report.pdf', '');
  Verdict := Lib.ConvertToPDFA(9);        // 9 = PDF/A-4, 10 = 4e, 11 = 4f
  Memo1.Lines.Text := Lib.GetPDFAConversionReport;
  if Verdict = 1 then
    Lib.SaveToFile('report-pdfa4.pdf')
  else
    Log('conversion incomplete - see the report for what stands in the way');
end;

Зачем нужен отчёт конвертации

Чтобы решить, что делать дальше. Успешной конвертации отчёт не нужен; конвертация, которая не удалась — та самая причина, по которой отчёт существует. Одни препятствия устранимы конвертером, другие нет: шифрование, запрещённое содержимое, несущее смысл, программный шрифт, которого просто нет нигде на машине. Отчёт различает сделанное и оставшееся, что превращает «конвертация не удалась» в рабочий элемент

Считайте вердикт шлюзом в пакетном конвейере. Конвертируйте, читайте вердикт и маршрутизируйте файл: архивируйте те, что прошли, ставьте остальные в очередь человеку с приложенным отчётом. Чего не следует делать — так это сохранять вывод провалившейся конвертации в архив лишь потому, что он выглядит лучше исходного: теперь он несёт претензию, которую проверка отказалась подтвердить

Чтение метки, которую файл уже несёт

Перед конвертацией узнайте, что документ говорит о себе. Проверка PDF/A, не умеющая читать существующую метку стандартов, оценивает каждый файл по части 1 независимо от его заявлений, а это значит, что совершенно корректный документ PDF/A-2 или PDF/A-3 сообщается как не несущий метки и имеющий слишком высокую версию — противоположность истине

Метка читается независимо от того, записал ли производитель её как XMP-элемент или как атрибут. Обе формы — обычный XMP, и принятие лишь одной из них оставляет файлы от других производителей выглядящими неразмеченными. Если вы когда-либо задавались вопросом, почему документ, проходящий валидацию в другом месте, падает в вашем конвейере, это хорошее место для первого осмотра

Очистка перед архивированием и баг, о котором стоит знать

Архивная конвертация и очистка часто идут вместе, потому что содержимое, которое политика безопасности хочет удалить, сильно пересекается с содержимым, запрещённым в PDF/A. SanitizeDocument удаляет JavaScript, а удаление последнего скрипта также удаляет пустое дерево имён, которое он оставляет после себя — дерево, которое иначе по-прежнему сообщало бы читалке, что документ нёс скрипты

Вторая половина была усвоена трудным путём: ошибка на единицу в списке пакетов приводила к тому, что очистка сообщала об удалении скриптов, не удаляя ни одного, поэтому прошедший очистку документ по-прежнему запускал скрипты при открытии. Это весомый аргумент в пользу общего принципа, на котором стоит вся статья: проверяйте результат, а не доверяйте операции — как в собственном конвейере, так и в библиотеке

Для сопутствующей архивной работы см. руководства по профилированию PDF/A и PDF/UA, истинному редактированию и удалению содержимого и схемам расширения XMP PDF/A-3 для Factur-X, описывающим сторону метаданных, когда архивный документ также несёт структурированные данные счёта

PDFlibPas — это нативная Pascal-библиотека PDF для Delphi, C++Builder и Lazarus, поэтому конвертация, восстановление и валидация происходят внутри вашего процесса без внешнего инструмента в цепочке — см. страницу продукта PDFlibPas для поддерживаемых частей PDF/A и платформ