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

Вывод PDF/X-1a, X-3 и X-4 в Delphi с помощью PDFlibPas

PDFlibPas выпускает печатные файлы PDF/X из Delphi одним вызовом — TPDFlib.SetPDFXMode, который выбирает PDF/X-1a:2003, PDF/X-3:2003 или PDF/X-4. Вызов записывает XMP-идентификацию pdfxid, добавляет output intent GTS_PDFX, объявляет /Trapped /False, фиксирует версию PDF и с этого момента отказывает рисующим вызовам, запрещённым выбранной частью. Аудит сохранённого файла делает CheckFileCompliance с тестом 3

Это короткий ответ. Длинный важен, потому что типографии бракуют файлы не за уродство — они бракуют их на preflight, обычно в пятницу и обычно из-за того, что мог поймать генерирующий код: RGB-логотип в задании X-1a, шрифт по ссылке вместо встроенного, страница без обрезного размера. PDF/X существует, чтобы сделать такие провалы структурными, а не визуальными, и библиотечная часть работы — не дать вам их породить вообще

Какую часть PDF/X выбрать приложению на Delphi?

Берите PDF/X-4, если принимающий workflow не говорит иного, и берите PDF/X-1a, когда говорит. Три части различаются ровно по трём осям — цвет, прозрачность и базовая версия PDF, — и каждая ось ложится на свой гейт внутри PDFlibPas:

  • PDF/X-1a:2003 (ISO 15930-4): синтаксис PDF 1.4, только CMYK, серые и плашечные цвета, без цветов на базе ICC, без прозрачности. Файл уже разведён по печатным краскам, и типографии не нужно делать никакого цветоделения
  • PDF/X-3:2003 (ISO 15930-6): всё ещё PDF 1.4 и по-прежнему без прозрачности, но color-managed пространства разрешены, а device RGB принимается, когда сам output intent описывает RGB-условие
  • PDF/X-4 (ISO 15930-7): синтаксис PDF 1.6 с живой прозрачностью и optional content (слоями). Именно под эту часть сделаны современные RIP: немалая доля старой боли X-1a рождалась как раз из уплощения прозрачности на выходе

X-1a до сих пор повсюду в сдаче рекламы и в старых печатных workflow, которым нужен файл, выводимый на печать без раздумий. X-4 — лучшее инженерное решение везде, где типография его принимает: прозрачность доживает до RIP нетронутой, а цветовые решения принимаются на RIP с реальным профилем печатной машины, а не в вашем коде. Номера режимов — 1, 2 и 3 соответственно, активный режим возвращает GetInformation(203). PDFlibPas определяет и более высокие номера режимов для семейства PDF/X-6 на PDF 2.0, но там другие правила версий и output intent, и здесь они вне области разговора

Что SetPDFXMode записывает в файл

TPDFlib.SetPDFXMode берёт на себя идентификацию и шаблонный каркас, которые читатель PDF/X проверяет в первую очередь, так что коду приложения остаётся дать настоящий печатный профиль и контент. Конкретно успешный вызов делает четыре вещи:

  • Записывает строку издания в XMP как pdfxid:GTS_PDFXVersion (PDF/X-1a:2003, PDF/X-3:2003 или PDF/X-4). Издания 2003 года идентифицируют часть полностью этой строкой, поэтому отдельное свойство соответствия не пишется
  • Добавляет output intent, если его у документа нет, и выставляет его подтип в /S /GTS_PDFX — тот подтип, который ищут читатели PDF/X (словарь output intent определён в ISO 32000-1 §14.11.5)
  • Выставляет /Trapped /False в информационном словаре документа: писатель никогда не делает треппинг, а PDF/X требует, чтобы состояние треппинга было объявлено, а не оставлено неизвестным
  • Фиксирует базу версии: 1.4 для X-1a и X-3, 1.6 для X-4

Ещё два исправления происходят при сохранении. Каждая страница без TrimBox получает его равным своему MediaBox, поскольку PDF/X требует объявленного обрезного размера на каждой странице (границы страницы описывает ISO 32000-1 §14.11.2), а аннотации получают печатный флаг, которого ждёт стандарт. Фоллбэк TrimBox — страховочная сетка, а не решение по вёрстке: задание «навылет» требует настоящего trim box внутри большего media box, и руководство по умолчаниям TrimBox, BleedBox и CropBox в PDFlibPas объясняет, как эти боксы задавать и чем может удивить наследование

uses
  PDFlibrary;

procedure BuildX1aAd(const OutFile, PressProfile: string);
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.SetPDFXMode(1) <> 1 then          // 1 = PDF/X-1a:2003
      raise Exception.CreateFmt('PDF/X mode refused (error %d)',
        [Pdf.LastErrorCode]);

    // Заменить профиль по умолчанию на реальное печатное условие
    if Pdf.LoadOutputIntentProfile(PressProfile, 'DeviceCMYK') <> 1 then
      raise Exception.Create('Could not load the CMYK output profile');

    // Embed = 1 встраивает полный шрифтовый файл; PDF/X не примет 0
    if Pdf.AddTrueTypeFont('Arial {1252}', 1) = 0 then
      raise Exception.Create('Font could not be loaded for embedding');

    Pdf.SetTextSize(24);
    Pdf.SetFillColorCMYK(0, 1, 1, 0);           // триадный красный, без RGB
    Pdf.DrawText(72, 720, 'Spring catalog, page 1');

    Pdf.SaveToFile(OutFile);
  finally
    Pdf.Free;
  end;
end;

Порядок в этом наброске не случаен. LoadOutputIntentProfile заменяет профиль уже существующего output intent, поэтому вызывать его нужно после того, как SetPDFXMode создал intent. Из ничего этот вызов intent тоже не сотворит: на документе без output intent он просто вернёт 0

Почему output intent по умолчанию нужно заменять?

Output intent, который SetPDFXMode добавляет для режимов 1–3, объявляет DeviceCMYK, но ICC-байты за ним — это входящий в комплект библиотеки профиль sRGB с переписанным на 4 числом компонент. Дефолт существует ради обратной совместимости, и его хватает, чтобы файл структурно был PDF/X, однако встроенный профиль не описывает ни одной печатной машины. Preflight-инструмент, который заглянет в заголовок профиля, увидит RGB-профиль с биркой CMYK, а типограф, полагающийся на intent при цветопробе, получит бессмыслицу

Поэтому считайте LoadOutputIntentProfile(FileName, 'DeviceCMYK') обязательным для настоящей печатной работы и передавайте ICC-профиль, который ваш печатный провайдер назвал для этой бумаги и машины. TPDFlib.LoadOutputIntentProfile подменяет поток DestOutputProfile и обновляет /N, остальной словарь intent оставляя как написан. В частности, идентификатор выходного условия и описательные строки дефолтного intent продолжают нести значения в духе sRGB, так что если типография матчится по идентификатору, а не по встроенному профилю, заранее уточните у неё, что именно она проверяет. Это честный пробел дефолтов эпохи 2003 года, а не ошибка вашего кода

Подводный камень первый: версия, которую вы не выбирали

Новый документ PDFlibPas стартует с писательского дефолта PDF 1.5, что выше базы 1.4, которую требуют PDF/X-1a и PDF/X-3. TPDFlib.SetPDFXMode в общем случае решает это молча: если версия сохранения не заблокирована, он опускает версию документа до базы части и продолжает работу. Первая реализация вместо этого отказывала, и каждый SetPDFXMode(1) на свежем документе возвращал 0 — хороший пример правила, верного на бумаге и бесполезного на практике

Отказ остался для единственного случая, где он прав. Если что-то раньше вызвало LockSaveVersion с версией выше базы, SetPDFXMode возвращает 0 с LastErrorCode 601: вызывающий зафиксировал версию намеренно, и библиотека не станет это переспоривать. В общем коде настройки документа, где хелпер блокирует версию по посторонним причинам, именно там X-1a-задание и умирает:

Pdf := TPDFlib.Create;
try
  Pdf.LockSaveVersion('1.7');     // выставлен универсальным хелпером «домашних дефолтов»
  if Pdf.SetPDFXMode(1) = 0 then
    // LastErrorCode = 601: заблокирована 1.7, PDF/X-1a нужна 1.4.
    // Для печатных заданий блокировку обходите. PDF/X-4 тут не спасение:
    // его база 1.6 тоже ниже блокировки 1.7 и падает так же.
    LogError('PDF/X refused, error %d', [Pdf.LastErrorCode]);
finally
  Pdf.Free;
end;

Тем же гейтом объясняется смежное ограничение. Режим PDF/X — переключатель создания документа: SetPDFXMode работает на новом документе и возвращает 0 на загруженном из файла, потому что библиотека не может ручаться за контент, который не писала. Конвертация существующего PDF в PDF/X — другая работа, требующая инспекции и починки, а не флага

Подводный камень второй: гейты, отказывающие молча

Пока режим PDF/X активен, PDFlibPas отказывает каждому контентному вызову, запрещённому выбранной частью, в момент самого вызова, а не даёт файлу провалиться на preflight позже. Отказы покрывают заливки и обводки в device RGB (вся X-1a; X-3 и X-4 — если только output intent не RGB), цвета на базе ICC в X-1a, прозрачность и blend modes в X-1a и X-3, optional content в X-1a и X-3, JavaScript-действия, вложенные файлы, фильтр изображений LZW, шрифты Standard 14, которые остались бы невстроенными, и шифрование с открытым ключом. Парольное шифрование через Encrypt на этапе создания не гейтится, но валидатор пометит любой зашифрованный файл, так что держите печатные задания незашифрованными

Чего у отказов нет общего — кода ошибки, и вот это ловушка. Гейты цветовых пространств в SetFillColor, SetLineColor, вызовы цвета на базе ICC и EncryptPublicKey ставят LastErrorCode в 601 — код соответствия библиотеки, которым пользуются и гейты PDF/A. Гейты прозрачности, JavaScript, вложений, LZW и AddStandardFont просто возвращают 0 и не трогают LastErrorCode, а значит, там может ещё сидеть значение от более раннего вызова. Код, который после пачки рисующих вызовов проверяет только LastErrorCode, пропустит половину отказов, поэтому проверяйте каждое возвращаемое значение:

procedure Require(Ok: Integer; const What: string);
begin
  if Ok = 0 then
    raise EPrintConformance.CreateFmt('%s refused under PDF/X', [What]);
end;

Require(Pdf.SetFillColorCMYK(0, 0, 0, 1), 'CMYK fill');
Require(Pdf.SetTransparency(40), 'transparency');   // 0 в X-1a / X-3
Require(Pdf.AddLinkToJavaScript(72, 72, 120, 20,
  'app.alert(1)', 0), 'JavaScript link');           // 0 в любой части

Шрифты заслуживают отдельного предупреждения. AddStandardFont возвращает 0 под PDF/X, потому что библиотека не поставляет контуры Standard 14 и не может их встроить. AddTrueTypeFont устроен иначе: в режиме PDF/A он сам повышает Embed = 0 до полного встраивания, но в режиме PDF/X — нет, поэтому системный шрифт по ссылке попадает в файл, и ловит его только валидатор. Передавайте 1 (полное) или 2 (сабсет) сами или один раз при настройке вызовите SetEmbedAllFonts(1), чтобы случайный 0 не проскочил

Как проверить PDF/X-файл до отправки в типографию?

Прогоните CheckFileCompliance с ComplianceTest = 3 на сохранённом файле. TPDFlib.CheckFileCompliance возвращает 0, если находок не зафиксировано, иначе — хэндл списка строк, элементы которого снабжены префиксом PDFX-. Аудит PDF/X читает файл тем же стриминговым читателем, что и preflight PDF/A и PDF/UA в PDFlibPas, и проверяет правила, которые печатный preflight проверяет в первую очередь:

  • Файл не зашифрован, XMP несёт известный GTS_PDFXVersion, а версия PDF не превышает базу части
  • Существует ровно один output intent, его подтип — GTS_PDFX, и он несёт целевой профиль и идентификатор выходного условия
  • Информационный словарь объявляет /Trapped
  • У каждой страницы есть TrimBox внутри MediaBox, а BleedBox, если присутствует, охватывает TrimBox
  • У каждого шрифта, использованного на странице, есть встроенная программа, причём Type0 проверяются через их потомка-шрифт
  • Нет запрещённых типов аннотаций (RichMedia, Screen, 3D, Movie, Sound, FileAttachment), нет open-действия JavaScript и document-level JavaScript, нет вложенных файлов и нигде в графе объектов нет фильтра LZWDecode
function PreflightPDFX(Pdf: TPDFlib; const FileName: string;
  Report: TStrings): Boolean;
var
  ListId, I: Integer;
begin
  ListId := Pdf.CheckFileCompliance(FileName, '', 3, 0);  // 3 = PDF/X
  Result := ListId = 0;
  if Result then
    Exit;
  try
    for I := 1 to Pdf.GetStringListCount(ListId) do       // элементы нумеруются с единицы
      Report.Add(Pdf.GetStringListItem(ListId, I));
  finally
    Pdf.ReleaseStringList(ListId);
  end;
end;

Отдавайте себе отчёт, что значит этот «прошёл». Валидатор PDFlibPas аудитирует перечисленные выше структурные правила, и именно они ловят ошибки генератора. Это не цвето-точный preflight: он не проверяет суммарное покрытие краской, разрешение изображений и то, совпадает ли встроенный профиль с условием, на котором печатает ваша типография. Используйте его как гейт в сборочном или экспортном конвейере, а проверки, специфичные для печатной машины, оставьте preflight-профилю типографии — его она всё равно прогонит

FAQ

Поддерживает ли PDFlibPas прозрачность PDF/X-4?

Да. В SetPDFXMode(3) PDFlibPas разрешает прозрачность, blend modes и группы optional content и пишет файл PDF 1.6. Те же вызовы возвращают 0 под PDF/X-1a и PDF/X-3

Может ли PDFlibPas конвертировать существующий PDF в PDF/X?

Нет. SetPDFXMode применим только к документам, созданным в текущей сессии TPDFlib, и возвращает 0 для загруженного файла. Любой существующий PDF вы по-прежнему можете аудитировать через CheckFileCompliance с тестом 3

Какой код ошибки возвращает отклонённый вызов PDF/X?

Отказы цветовых пространств и шифрования с открытым ключом ставят LastErrorCode в 601. Прочие гейты — прозрачность, JavaScript, шрифты Standard 14 — лишь возвращают 0, поэтому всегда проверяйте результат функции

Вывод PDF/X, валидатор PDF/X и остальной набор инструментов печати и архивирования — часть PDFlibPas, нативной Object Pascal PDF-библиотеки losLab для Delphi. Издания, платформы и полную справку по API смотрите на странице продукта PDFlibPas Delphi PDF Library