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

Пакетные отчёты о предпечатной проверке PDF в Delphi с PDFium Component CLI

Инструмент пакетной предпечатной проверки - это консольная программа без окна, направленная на папку с PDF-файлами, которая проверяет каждый из них на соответствие названным стандартам и оставляет машиночитаемые доказательства обнаруженного. Никто за ней не наблюдает. Она запускается в два часа ночи под cron или планировщиком задач Windows, или в качестве шлюза в CI-конвейере, а следующий, кого волнует её вывод, - это либо планировщик, читающий код завершения, либо аудитор, открывающий отчёт несколько недель спустя. Это меняет понимание слова «правильно». Механизм предпечатной проверки PDFium Component, библиотеки PDF с исходным кодом для Delphi, C++Builder и Lazarus, делает сами вызовы проверки почти тривиальными. Работа, определяющая, оправдает ли инструмент своё существование, сосредоточена вокруг этих вызовов: какой профиль вы проверяли, что код завершения сообщил планировщику, и существует ли ещё отчёт, способный выявить ошибку, когда кто-нибудь будет его искать

Контракт: что на самом деле видит планировщик

CI-runner или планировщик задач Windows видит от вашего инструмента ровно два параметра: код завершения и оставленные им файлы. Строки лога, цвета консоли, вывод прогресса - всё это для человека, наблюдающего в реальном времени, а в два часа ночи никого нет. Поэтому зафиксируйте словарь кодов завершения прежде, чем прикасаться к API, и держите его простым:

  • 0: каждый файл соответствовал каждому запрошенному профилю
  • 1: хотя бы один файл дал замечания по результатам проверки
  • 2: сам инструмент дал сбой хотя бы на одном файле (повреждённый ввод, блокировка, сбой)

Различие между кодами 1 и 2 - то, которое команды пропускают и о котором впоследствии жалеют. Повреждённый PDF, который не удаётся открыть, - это не ошибка проверки. Включите это в код 1, и целый грузовик повреждённых сканов появится в ваших дашбордах как внезапный провал соответствия, заставляя кого-то гоняться за регрессией стандартов, которой никогда не было, тогда как реальная история - сломанный сканер выше по потоку

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

Выбор стандартов и почему важен уровень соответствия

Перечисление TPdfPreflightStandard охватывает семейства, встречающиеся на практике: ppsPdfA для архивного соответствия ISO 19005, ppsPdfUa для доступности ISO 14289, ppsPdfX для обмена при печати, а также ppsPdfE, ppsPdfR и ppsPdfVT для инженерии, растровых данных и работы с переменными данными. Внутри семейства движок читает уровень соответствия, заявленный документом, и сообщает его для каждого стандарта в поле ConformanceName результата. Указания семейства редко бывает достаточно, потому что именно на уровне кроется реальное различие. PDF/A-2b обещает визуальную воспроизводимость и ничего более. PDF/A-3a добавляет требование к логической разметке структуры и допускает встроенные исходные файлы, что является значительно более высокой планкой для отсканированных материалов, вообще не имеющих дерева тегов. Ошибиться в любую сторону, и пакетный процесс будет вас обманывать. Если ваша политика хранения фактически требует PDF/A-2b, но вы выдаёте ошибки за отсутствующие теги структуры, отчёт заполняется замечаниями, которые никто никогда не исправит. Принять любой ярлык PDF/A без проверки уровня - значит подписаться под документами, отвечающими более слабой планке, чем вы обещали. Требования доступности со стороны государственных заказчиков всё чаще добавляют PDF/UA поверх всего этого, что не увеличивает стоимость выполнения, поскольку BuildPdfPreflightReport (из модуля FPdfPreflightReport) принимает набор стандартов:

Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);

Один вызов оценивает оба стандарта и возвращает единую сводную запись отчёта

Почему пустой список замечаний не означает прохождение проверки

В отчёте перечислены замечания по каждому стандарту, и пустой список проблем означает лишь «проблем не обнаружено в стандартах, которые фактически запускались». Это более узкое утверждение, чем «файл соответствует стандарту, который вас интересует», и именно в этом зазоре пакетная предпечатная проверка тихо деградирует. Опечатка в конфигурации, исключившая ppsPdfA из набора, производит ровно такой же пустой список проблем, как подлинно чистый файл. Поэтому воспринимайте тишину с подозрением. Пройдитесь по Report.Results и убедитесь в двух вещах для каждого стандарта, который вы собирались проверить: что запись результата для него вообще существует, и что флаг IsCompliant, подкреплённый Status = pfsPass, имеет значение true. Ночное задание, приравнивающее «нет замечаний» к «готово к архивированию» без проверки того, какие стандарты были оценены, - это классический способ, при котором папка несоответствующих файлов проходит проверку месяцами, пока внешний аудитор не откроет один из них с помощью veraPDF и весь архив не окажется под вопросом

Вторая ловушка скрывается в том, чем является само замечание. Каждый TPdfPreflightIssue содержит Code, Category, Description и Recommendation, и он называет нарушенное правило, а не страницу или объект. Это намеренное дизайнерское решение со своими последствиями для обратной связи. Отчёт сообщает производящей команде, какой класс дефекта существует - невстроенный шрифт или отсутствующий XMP-идентификатор, - а поиск конкретного нарушающего объекта - это задача инструмента исправления, а не валидатора. Создавайте потребителей отчётов на основе стабильных значений Code, никогда - на основе текста описания, доступного для чтения, который может быть переформулирован между релизами без предупреждения

Файлы отчётов для машин и для дежурного специалиста

Запись отчёта записывает одни и те же замечания в пяти форматах: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile и SaveMarkdownToFile, у каждого есть соответствующая функция вида ToJson, когда вам нужна строка в памяти, а не на диске. Не поддавайтесь искушению выбрать один. Пишите JSON для конвейера, чтобы CI мог прикрепить его к записи задания и анализировать коды замечаний и статусы по стандартам без разбора текста. Пишите HTML для человека, которого вызвали по тревоге, потому что он открывается в любом браузере без всяких инструментов. Оба вместе стоят одной дополнительной строки на файл и избавляют дежурного инженера от худшей задачи в пакетной обработке - обратной разработки сырого JSON-блоба в два часа ночи, чтобы узнать, какой файл сломался. Одна дисциплина важнее выбора формата: выводите каждое имя отчёта из имени входного файла, а не из временной метки, иначе два параллельных запуска перемешают отчёты, которые вы больше не сможете сопоставить с их вводами

Пороги серьёзности относятся к конфигурации, а не к коду. Аннотация без альтернативного описания - это жёсткий сбой для портала подачи PDF/UA и игнорируемое примечание для внутреннего архива, хотя это идентичное замечание в обоих случаях. Укажите уровень сбоя для каждого профиля, чтобы политика могла меняться без перекомпиляции, и записывайте действовавший уровень в саму сводку задания. В следующем квартале никто не вспомнит, с каким порогом запускался пакет в октябре прошлого года, и сводка - единственное место, где эта память сохраняется

Изоляция файлов, чтобы один плохой PDF не погубил весь пакет

procedure RunPreflightBatch(const InputDir, ReportDir: string;
  out FilesWithFindings, ToolFailures: Integer);
var
  SR: TSearchRec;
  Pdf: TPdf;
  Report: TPdfPreflightReport;
begin
  FilesWithFindings := 0;
  ToolFailures := 0;
  if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then
  try
    repeat
      Pdf := TPdf.Create(nil);   // fresh instance per file: no state bleed
      try
        try
          Pdf.FileName := InputDir + SR.Name;
          Pdf.Active := True;
          if not Pdf.Active then  // load failures are silent, not raised
            raise EPdfError.Create('Cannot open ' + SR.Name);
          Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
          Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));
          Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));
          if Report.TotalIssueCount > 0 then
            Inc(FilesWithFindings);
        except
          on E: Exception do
          begin
            Inc(ToolFailures);   // exit-code-2 territory, not a validation verdict
            WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
          end;
        end;
      finally
        Pdf.Free;
      end;
    until FindNext(SR) <> 0;
  finally
    FindClose(SR);
  end;
end;

В этом цикле есть три намеренных решения. Свежий экземпляр TPdf на каждый файл гарантирует, что один документ, повреждающий состояние движка, не отравит следующие файлы. Явная проверка Active заслуживает своего места, потому что Active := True поглощает ошибки загрузки вместо того, чтобы генерировать исключения; уберите этот защитный барьер, и усечённый файл проберётся в вызов проверки, прежде чем потерпит неудачу где-то дальше с вводящим в заблуждение сообщением. Внутренний try..except намеренно находится внутри области видимости каждого файла, так что одно исключение увеличивает счётчик сбоев, и цикл продолжается. Вы хотите чистые отчёты для 4999 хороших файлов, даже когда файл 5000 изорван. И оба формата отчётов записываются на диск до того, как подводится итог, что означает: доказательства сохраняются, даже если ошибка в логике сводки позже неправильно подсчитает результаты

Затем сопоставление кодов завершения сводится к нескольким строкам в файле проекта:

begin
  RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);
  if Failures > 0 then
    Halt(2)
  else if Findings > 0 then
    Halt(1);
  // falling through exits with 0: every file conformed
end.

Что предпечатная проверка не сделает за вас

Движок обнаруживает; он не исправляет. Замечание о невстроенном шрифте или цветовом пространстве, зависящем от устройства, - это наряд на работу для тех, кто производит файлы, и у валидатора нет способа исправить это на месте. Поэтому планируйте цикл обратной связи намеренно. Отчёты должны попадать туда, где производящая команда их действительно читает, иначе одни и те же замечания будут появляться каждую ночь, пока кто-нибудь наконец не спросит, почему показатель соответствия никогда не улучшается. Также стоит перекрёстно проверить выборку вердиктов с независимым валидатором - veraPDF для PDF/A или предпечатную проверку Acrobat для PDF/X - прежде чем внешний аудитор сделает это за вас. Когда два движка расходятся во мнениях по реальному клиентскому файлу, этот документ - не помеха; это именно тот регрессионный случай, которого не хватало вашему тестированию релизов. Сохраните его, назовите и запускайте при каждой сборке

Ещё одно сочетание стоит знать. Тот же движок проверки управляет интерактивными проверками в UI проверки, поэтому этот консольный CLI и ориентированный на аналитиков стол проверки входящих PDF могут использовать единый словарь проверки вместо того, чтобы расходиться со временем. И поскольку [ppsPdfA, ppsPdfUa] оценивает доступность в том же проходе, сторона PDF/UA пакета хорошо согласуется с работой на стороне просмотрщика, такой как создание доступного PDF-ридера в Delphi. Профили, форматы отчётов и полный API предпечатной проверки задокументированы на странице продукта для PDFium Component