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

Валидация PDF/X в Delphi с помощью компонента PDFium

Компонент PDFium для Delphi проверяет готовые к печати документы PDF/X с помощью метода TPdf.ValidatePdfX, реализующего проверку по стандарту ISO 15930 на двух уровнях: восемь побайтовых проверок содержимого (запрет сжатия LZW, JavaScript, полей форм, ссылок OPI, отсутствующего TrimBox, незаданного ключа Trapped и т. д.) плюс обход объектной модели PDFium с использованием FPDFFont_GetIsEmbedded для проверки внедрения шрифтов в каждом текстовом объекте на всех страницах. Результатом является запись TPdfXValidationResult, указывающая обнаруженный уровень соответствия и перечисляющая нарушения в виде типизированного перечисления. Это позволяет вашему приложению Delphi точно сообщать клиенту, почему файл будет отклонен типографией, еще до начала процесса печати

Если вы когда-либо отправляли заказ в коммерческую типографию и получали его обратно с однострочным отказом вроде «нет TrimBox», «шрифты не внедрены» или «Trapped не задан», вы знаете цену столь позднего обнаружения проблем. Стандарт PDF/X — это допечатный аналог архивного PDF/A: в то время как архивный PDF/A гарантирует идентичное отображение документа через десятилетия, стандарт PDF/X гарантирует, что документ будет одинаково разделен на формы, выведен на фотовывод и обрезан на стороннем растровом процессоре (RIP) завтра утром. Оба стандарта используют общие механизмы (идентификацию XMP, OutputIntents, встроенные профили ICC), но решают разные задачи, поэтому компонент поставляется с отдельными валидаторами для каждого из них — подробнее о PDF/A рассказано в руководстве по preflight-валидации PDF/A с помощью компонента PDFium

Что на самом деле требует стандарт ISO 15930 от готового к печати PDF?

Стандарт ISO 15930 создан для обеспечения «слепого обмена» (blind exchange): дизайнер передает файл типографии, с которой он никогда не общался, и печатник может получить корректный результат без дополнительных звонков, писем об отсутствующих шрифтах и без поиска связанных изображений, оставшихся на компьютере дизайнера. Каждое правило стандарта служит этой цели. Шрифты должны быть внедрены, так как нельзя предполагать наличие нужной гарнитуры на стороне растрового процессора. Внешние ссылки запрещены, так как файл должен быть самодостаточным. Интерактивные функции исключены, поскольку у типографской краски нет обработчиков onclick

Компонент PDFium распознает три семейства уровней соответствия и сообщает о них через перечисление TPdfXConformance в результатах валидации: pxc1a для версии PDF/X-1a:2001 (ISO 15930-1, строгий базовый уровень CMYK и плашечных цветов на базе PDF 1.3/1.4), pxc3 для PDF/X-3:2002 (ISO 15930-3, допускающий использование RGB, Lab и цветов с управлением ICC) и pxc4 для PDF/X-4:2010 (ISO 15930-7, разрешающий реальную прозрачность и слои на базе PDF 1.6). Файл без каких-либо признаков соответствия PDF/X возвращает значение pxcNone, что само по себе является полезным ответом: документ изначально не заявлялся как готовый к печати, а все обнаруженные ошибки показывают, что требуется исправить для достижения этого статуса

Запреты становятся очевидными, если взглянуть на задачу с точки зрения разработчика растровых процессоров. Фильтр /LZWDecode запрещен во всех спецификациях PDF/X, чтобы программа вывода не зависела от алгоритмов с долгой историей лицензирования и совместимости; сжатие Flate выполняет ту же задачу без лишних проблем. Код JavaScript, поля форм AcroForm и словари дополнительных действий /AA запрещены, так как файл печати должен представлять собой фиксированное описание меток на бумаге — любые изменения вида документа при открытии нарушают гарантию того, что оттиск будет соответствовать пробе. Плейсхолдеры OPI (Open Prepress Interface) также запрещены, поскольку они представляют собой ссылки на изображения высокого разрешения, хранящиеся где-то извне, а такое внешнее хранение прямо противоречит принципу «слепого обмена»

Почему типографии отклоняют PDF без TrimBox?

TrimBox (обрезная рамка) описывает формат готовой страницы — прямоугольник, остающийся после резака. Рамка MediaBox, присутствующая на любой странице PDF, описывает лишь размер печатного листа: она включает поля под обрез (вылеты), метки реза, приводные кресты и цветовые шкалы. Программы спуска полос позиционируют страницы на печатном листе по их обрезным рамкам TrimBox. При их отсутствии печатник вынужден гадать, где именно проходят границы вашей визитки, а неверный выбор приведет к срезанию вылетов под обрез или появлению белых полос по краям. Именно поэтому стандарт ISO 15930 требует наличия TrimBox (или ArtBox) на каждой странице, а метод ValidatePdfX генерирует ошибку pvxiMissingTrimBox при отсутствии ключа /TrimBox на любой из страниц документа

Ключ /Trapped отвечает на другой технологический вопрос. Треппинг — это допечатный прием небольшого перекрытия соседних цветов, предотвращающий появление белых просветов из-за несовмещения красок при печати. Печатнику необходимо знать, была ли выполнена эта операция ранее: повторный треппинг уже обработанного файла удвоит перекрытия, а пропуск этой стадии для необработанного файла приведет к риску появления зазоров. Поэтому стандарт PDF/X требует явного указания /Trapped /True или /Trapped /False в словаре Info — отсутствие ключа или значение /Unknown заставляет оператора проверять файл вручную, что противоречит принципу «слепого обмена». Компонент помечает это как ошибку pvxiTrappedNotSet

Выполнение двухэтапной валидации с помощью TPdf.ValidatePdfX

Метод TPdf.ValidatePdfX не принимает аргументов и возвращает запись TPdfXValidationResult с тремя полями: Conformance (обнаруженная версия PDF/X), Issues (множество флагов TPdfXValidationIssue) и вспомогательное свойство IsCompliant. Изнутри метод сохраняет загруженный документ во временный поток в памяти, выполняет побайтовое сканирование, а затем проходит по элементам объектной модели PDFium для проверки внедрения шрифтов. Базовый код проверки выглядит следующим образом:

uses PDFium, FPdfPdfx;

procedure CheckPrintReadiness(const FileName: string);
var
  Pdf: TPdf;
  Res: TPdfXValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;

    Res := Pdf.ValidatePdfX;
    Writeln('Detected conformance: ',
      Ord(Res.Conformance)); // pxc1a, pxc3, pxc4, pxcNone...

    if Res.IsCompliant then
      Writeln('PDF/X checks passed')
    else
    begin
      if pvxiMissingTrimBox in Res.Issues then
        Writeln('ОТКЛОНЕНО: на страницах отсутствует /TrimBox');
      if pvxiTrappedNotSet in Res.Issues then
        Writeln('ОТКЛОНЕНО: отсутствует ключ /Trapped или имеет значение /Unknown');
      if pvxiPdfiumFontNotEmbedded in Res.Issues then
        Writeln('ОТКЛОНЕНО: страница использует невнедренный шрифт');
      if pvxiLzwForbidden in Res.Issues then
        Writeln('ОТКЛОНЕНО: присутствует фильтр LZWDecode');
    end;
  finally
    Pdf.Free;
  end;
end;

Так как свойство Issues представляет собой обычное множество Pascal, вы можете разделять его по требованиям вашего техпроцесса — обрабатывать структурные проблемы как критические ошибки, помечать pvxiMissingTitle (рекомендация стандарта, а не обязательное требование) как предупреждение, а всё остальное записывать в журнал. Тот же тип записи используется генератором отчетов компонента, поэтому, если вам предпочтительнее создавать понятный человеку отчет вместо разбора кодов ошибок в коде, вы можете использовать шаблоны из статьи о построении консольного preflight-валидатора на базе компонента PDFium

Что выявляет побайтовый анализ и что он пропускает

Побайтовый уровень представляет собой сканирование токенов в структуре документа с исключением содержимого потоков, благодаря чему изображение JPEG, случайно содержащее байты /JavaScript, не вызовет ложноположительного сбоя. Помимо проверки маркеров (XMP-маркера pdfxid:GTS_PDFXVersion, наличия OutputIntent со встроенным ICC-профилем, /ID в трейлере и запрета шифрования), побайтовый проход выполняет восемь дополнительных проверок, каждая из которых имеет свое значение в перечислении:

  • pvxiLzwForbidden — фильтр /LZWDecode присутствует в файле (запрещен во всех версиях PDF/X)
  • pvxiJavaScriptForbidden — присутствует действие /JavaScript или дерево имен
  • pvxiFormFieldsForbidden — обнаружен словарь /AcroForm или запись /XFA
  • pvxiAdditionalActions — обнаружен словарь дополнительных действий /AA
  • pvxiEmbeddedFilesForbidden — присутствуют элементы /EmbeddedFiles или аннотация /FileAttachment
  • pvxiOpiForbidden — запись /OPI или /Alternates ссылается на заменяемое внешнее изображение
  • pvxiMissingTrimBox — рамка /TrimBox не найдена на страницах
  • pvxiTrappedNotSet — параметр /Trapped отсутствует или имеет значение /Unknown

Побайтовый сканер работает очень быстро и не требует движка рендеринга, но он имеет фундаментальное «слепое пятно» при работе со шрифтами: на этом уровне валидатор может применять лишь грубую эвристику, выдавая ошибку только при полном отсутствии внедренных шрифтов в файле. Файл с девятью внедренными шрифтами и одним незаметно пропущенным системным шрифтом успешно пройдет такой анализ. Для устранения этого пробела и служит второй уровень проверок

Проверка внедрения шрифтов через объектную модель PDFium

Анализ на уровне объектной модели PDFium дает точный ответ по шрифтам. После побайтового прохода метод TPdf.ValidatePdfX обходит каждую страницу, считывает количество объектов через FPDFPage_CountObjects и для каждого текстового объекта получает дескриптор шрифта с помощью FPDFTextObj_GetFont, проверяя его через FPDFFont_GetIsEmbedded. Наличие хотя бы одного невнедренного шрифта в любом месте документа добавляет флаг pvxiPdfiumFontNotEmbedded в список обнаруженных проблем. Обход оптимизирован на двух уровнях: сканирование объектов на странице и загрузка последующих страниц прекращаются в тот момент, когда нарушение подтверждается. На каталоге из 300 страниц с ошибкой вердикт часто выносится уже на первой странице

Важно знать две детали. Во-первых, этот уровень проверок требует загруженной библиотеки PDFium и наличия экспортируемой функции FPDFFont_GetIsEmbedded. При её отсутствии проверка пропускается без генерации ложных ошибок, благодаря чему старые версии библиотек DLL не вызовут проблем. Во-вторых, этот тест проверяет лишь сам факт внедрения и ничего больше — он не отличает полное внедрение от подмножеств (subsetting) и не анализирует состав символов. Если файл не проходит проверку и вам нужно узнать, какой именно шрифт на какой странице вызвал проблему, методы из статьи об анализе свойств шрифтов PDF с помощью PDFium в Delphi помогут детально изучить структуру

Валидация потоков без загрузки документа или библиотеки DLL

Побайтовый валидатор также доступен как отдельная функция ValidatePdfXCompliance(Source: TStream) в модуле FPdfPdfx. Это чистый код на Object Pascal без зависимостей от библиотеки PDFium DLL, что позволяет использовать его там, где движок рендеринга не нужен или нежелателен: на веб-сервере для проверки загрузок, в задачах CI для проверки макетов или в сервисах Lazarus на платформах, куда не хочется переносить бинарные файлы. Передайте функции любой поток с поддержкой позиционирования:

uses Classes, FPdfPdfx;

function QuickPdfXGate(const FileName: string): Boolean;
var
  Fs: TFileStream;
  Res: TPdfXValidationResult;
begin
  Fs := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfXCompliance(Fs);
    Result := Res.IsCompliant and (Res.Conformance <> pxcNone);
  finally
    Fs.Free;
  end;
end;

Компромисс очевиден: автономный метод выполняет проверку маркеров и все восемь тестов структуры, но не использует объектный уровень PDFium для шрифтов, полагаясь на грубую эвристику. Разумная архитектура предполагает использование ValidatePdfXCompliance в качестве быстрого первичного фильтра, а полный метод TPdf.ValidatePdfX — только для успешно прошедших его файлов

Где заканчивается этот валидатор и начинается полный preflight-контроль

Объективность важна для инструментов допечатной подготовки, поэтому укажем точные границы. Метод ValidatePdfX проверяет идентификационные маркеры, структурные ограничения, параметры геометрии страниц, признак Trapped и внедрение шрифтов для каждого текстового объекта. Он не рассчитывает суммарную плотность красок (total ink coverage), не проверяет допустимость цветовых пространств для выбранной версии (например, работу только с CMYK по правилам X-1a), не сверяет разрешение растра с линиатурой и не анализирует наложение красок (overprint) или сведение прозрачностей (transparency flattening) — для этого требуется специализированный процессор допечатной подготовки с поддержкой профилей. Двухэтапная валидация закрывает до 80% причин отказов типографий, выявляя структурные нарушения за миллисекунды прямо в коде Delphi, избавляя от писем с отказами от печатников

Оба уровня валидации, API добавления маркеров PDF/X для создания совместимых файлов, а также валидаторы для стандартов PDF/A, PDF/UA, PDF/E и PDF/VT, разделяющие ту же архитектуру, поставляются в составе компонента PDFium Component для Delphi и C++Builder — единого решения для рендеринга и контроля допечатной подготовки