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

PDF/X-1a в HotPDF: проверка RGB-контента на Delphi

Когда PDFXCompliance выставлен в 'X-1a', HotPDF проверяет цвет контента в самом конце работы: EndDoc вызывает THotPDF.EnsurePDFX1aContentColorSpaces, которая отвергает любой контент DeviceRGB, CalRGB или ICCBased RGB на любой странице и поднимает исключение, называющее провинившийся ресурс. Файл никогда не будет записан как PDF/X-1a, который типография завернёт

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

Почему PDF/X-1a отвергает RGB-контент?

PDF/X-1a отвергает RGB, потому что формат обещает: каждый цвет уже финален для одного конкретного CMYK-условия печати. ISO 15930-1:2001 §6.2.3 ограничивает цвет контента DeviceCMYK, DeviceGray и плашечными красками — то есть пространствами Separation и DeviceN, чьи альтернативы CMYK или Gray. Встроенный в файл OutputIntent GTS_PDFX называет это условие печати, скажем, FOGRA39 coated offset, и принимающий конвейер вправе слать числа прямиком на пластины без всякого преобразования цвета

RGB рвёт этот договор. Значение RGB описывает цвет относительно дисплея или рабочего пространства, а не рецепт краски, поэтому кто-то ниже по цепочке должен решать, как его сепарировать, — а X-1a существует ровно затем, чтобы отнять это решение у приёмщика. ISO 32000-1 §8.6 группирует цветовые пространства PDF в семейства device, CIE-based и special, и RGB-представители встречаются более чем в одном семействе: DeviceRGB — device-пространство, CalRGB и ICCBased с тремя компонентами — CIE-based, а Indexed или Separation могут прятать RGB-базу или альтернативу в глубине. Проверяющий, ищущий только оператор rg, пропустит большинство из них. Позднейшая редакция PDF/X-1a:2003 (ISO 15930-4) держит то же цветовое правило; учтите, что значение 'X-1a' в HotPDF пишет в XMP-пакет идентификацию PDF/X-1a:2001

Если ваш контент честно RGB и должен им остаться, X-1a — неверная цель. HotPDF ограничивает эту проверку только 'X-1a', поэтому 'X-3' и 'X-4', допускающие ICC-based и калиброванный цвет, не затронуты

Что EnsurePDFX1aContentColorSpaces проверяет фактически

EnsurePDFX1aContentColorSpaces соединяет флаг времени рисования с рекурсивным обходом ресурсов каждой страницы и идёт последним в шлюзе PDF/X. EndDoc сперва обеспечивает общие требования PDF/X: Title задан, Trapped равен 'True', 'False' или 'Unknown', существует OutputIntent GTS_PDFX (EnsurePDFXOutputIntent), и каждая страница имеет TrimBox или ArtBox (EnsurePDFXPageBoxes). И только после их прохождения стартует цветовая проверка

Последовательность шлюза PDF/X внутри HotPDF EndDoc для Delphi: сперва метаданные, OutputIntent и боксы страниц, затем проверка цвета RGB-контента
цветовая проверка идёт последней в шлюзе X-1a, а флаг времени рисования означает, что RGB, нарисованный до назначения compliance, тоже будет пойман

Первый путь — общедокументный флаг. RGB-сеттеры HotPDF, встраивание битмапов и цветовые записи аннотаций фиксируют использование DeviceRGB в момент рисования, покрывая inline-операторы rg и RG, которые никогда не появляются в словаре ресурсов. Второй путь обходит /Resources каждой страницы и осматривает:

  • Именованные пространства в /ColorSpace, включая сокращение inline-изображений RGB как синоним DeviceRGB
  • /ColorSpace каждого Image XObject
  • Form XObjects и tiling Patterns, рекурсивно заходя в их собственные вложенные /Resources
  • Shading Patterns и записи словаря /Shading страницы, через /ColorSpace шейдинга
  • Базовые пространства Indexed, альтернативы Separation и DeviceN и базовые пространства Pattern — на глубину вложения до 8

Отвергаются три вещи: DeviceRGB, CalRGB и пространства ICCBased, чей поток профиля декларирует N = 3. Словари ресурсов, разделённые между Form XObjects и паттернами, посещаются однократно, поэтому взаимоссылающийся набор форм не загонит обход в петлю. Метод публичен на THotPDF, но в нормальном использовании вы его сами не вызываете — за вас это делает EndDoc

Где прячется RGB в ресурсах страниц PDF: вложенные формы, паттерны, шейдинги и альтернативы ведут обход HotPDF в Delphi к DeviceRGB, CalRGB или ICCBased RGB
обход следует вложенным ресурсам до глубины 8 и отвергает DeviceRGB, CalRGB и пространства ICCBased с N = 3 везде, где они всплывают

Страница X-1a на Delphi, проходящая проверку

Соответствующий HotPDF-документ X-1a требует CMYK OutputIntent, TrimBox на каждой странице и цвета, выставленного только через API CMYK, Gray и плашечных красок. Скелет ниже использует профиль FOGRA39; AddPDFXOutputIntent принимает идентификатор выходного условия, строку информации, поток ICC-профиля, число его компонент и альтернативное пространство

var
  Pdf: THotPDF;
  Profile: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'brochure-x1a.pdf';
    Pdf.Title := 'Spring brochure, press edition';
    Pdf.Trapped := 'False';
    Pdf.PDFXCompliance := 'X-1a';
    Pdf.BeginDoc;

    Profile := TFileStream.Create('CoatedFOGRA39.icc', fmOpenRead or fmShareDenyWrite);
    try
      Pdf.AddPDFXOutputIntent('FOGRA39 (ISO 12647-2:2004)',
        'Coated FOGRA39 offset', Profile, 4, 'DeviceCMYK');
    finally
      Profile.Free;
    end;
    Pdf.CurrentPage.SetTrimBox(0, 0, 612, 792);   // обязателен на каждой странице

    Pdf.CurrentPage.SetCMYKFillColor(0, 0.5, 0.8, 0.1);
    Pdf.CurrentPage.Rectangle(72, 600, 468, 120);
    Pdf.CurrentPage.Fill;

    Pdf.CurrentPage.SetGrayFillColor(0.2);         // текст DeviceGray
    Pdf.CurrentPage.SetFont('Arial', [], 14);
    Pdf.CurrentPage.TextOut(72, 560, 0, 'Spring catalog, press edition');
    Pdf.EndDoc;                                    // цветовая проверка идёт здесь
  finally
    Pdf.Free;
  end;
end;

Каждый вызов на базе TColor в этом контексте — ловушка. SetRGBFillColor, SetRGBStrokeColor и SetRGBColor принимают GDI-цвет и эмитят DeviceRGB, поэтому хелпер, написанный годы назад для экранных отчётов, запятнает печатную работу в момент повторного использования. Берите SetCMYKFillColor, SetCMYKStrokeColor или SetCMYKColor с компонентами в диапазоне 0–1 либо Gray-аналоги

Плашечные цвета и CMYK-градиенты

Плашечные краски в X-1a разрешены, пока их альтернатива CMYK или Gray. RegisterSeparation строит пространство Separation и возвращает имя ресурса, а RegisterAxialGradient выбирает color space шейдинга по числу переданных компонент — четыре компоненты означают DeviceCMYK

var
  SpotCS, Ramp: AnsiString;
begin
  // Плашечная краска с приблизительной CMYK-сборкой в качестве альтернативы
  SpotCS := Pdf.RegisterSeparation('Corporate Red', 'DeviceCMYK',
    [0, 1, 0.8, 0.05]);
  Pdf.CurrentPage.SetFillColorSpace(SpotCS);
  Pdf.CurrentPage.SetFillColor([1.0]);           // тон 100%
  Pdf.CurrentPage.Rectangle(72, 420, 200, 80);
  Pdf.CurrentPage.Fill;

  // 4 компоненты на точку: шейдинг DeviceCMYK, а не DeviceRGB
  Ramp := Pdf.RegisterAxialGradient(72, 300, 540, 300,
    [0, 0, 0, 0], [1, 0.6, 0, 0.2]);
  Pdf.CurrentPage.SetFillPattern(Ramp);
  Pdf.CurrentPage.Rectangle(72, 260, 468, 80);
  Pdf.CurrentPage.Fill;
end;

Передайте тому же вызову градиента три компоненты — получите шейдинг DeviceRGB, который обход ресурсов отвергнет от имени шейдинг-паттерна. То же с Separation, зарегистрированным с 'DeviceRGB' в альтернативе: имя плашки выглядит безобидно, но обходит альтернативу и находит под ней RGB. Как такие плашечные краски композитятся, когда вьюер или RIP откатывается к альтернативе, раскрыто в статье о рендеринге Separation и DeviceN плашечных цветов на Delphi

Растровые изображения требуют той же дисциплины. HotPDF прибивает Flate-сжатые битмапы к DeviceRGB, тогда как JPEG, встроенный с icJpeg, держит собственное число компонент: четырёхкомпонентный CMYK JPEG или полутоновой JPEG в порядке, обычный RGB JPEG — нет. Конвертируйте графику в печатный профиль в своём imaging-конвейере, прежде чем она дойдёт до PDF

Почему /DefaultRGB не спасает файл PDF/X-1a?

/DefaultRGB не помогает X-1a, потому что правило о том, что получает машина, а не о том, охарактеризован ли RGB. Для PDF/A HotPDF позволяет RegisterDefaultColorSpace('DefaultRGB', ...) поглотить использование DeviceRGB: как только Default-пространство отображает DeviceRGB на ICC-профиль, контент device-независим, и проверка консистентности PDF/A его принимает. Разработчики, прошедшие тот конвейер, склонны пробовать здесь ту же починку

EnsurePDFX1aContentColorSpaces намеренно игнорирует то отображение. Охарактеризованное значение RGB — всё ещё RGB, а OutputIntent X-1a — CMYK-условие печати, поэтому конвертировать его всё равно пришлось бы кому-то. Если запоминать одну разницу между двумя архивно-печатными профилями — запоминайте эту: PDF/A спрашивает, однозначен ли цвет, PDF/X-1a — является ли он уже краской

Порядок рисования и setup страницы, который не считается

Поскольку проверка идёт на EndDoc по уже написанному контенту, порядок, в котором вы настраивали вещи, безразличен. RGB, нарисованный до назначения PDFXCompliance или до регистрации OutputIntent, всё равно будет пойман. Это важно для кода, строящего страницы в общих рутинах и решающего выходной профиль поздно — иногда по пользовательской настройке, выбранной, когда половина документа уже существует

Pdf.BeginDoc;
// ... OutputIntent и TrimBox как раньше ...
Pdf.CurrentPage.SetRGBFillColor(clRed);   // легаси-хелпер, эмитит DeviceRGB
Pdf.CurrentPage.Rectangle(72, 72, 200, 100);
Pdf.CurrentPage.Fill;

Pdf.PDFXCompliance := 'X-1a';             // решено после рисования
Pdf.EndDoc;                               // всё равно поднимет: DeviceRGB записан

У флага времени рисования есть одна ловушка, которую стоит знать, если строите похожую проверку сами. Каждая новая страница инициализируется чёрным graphics state через SetRGBColor(0), эмитящий пару 0 0 0 rg и 0 0 0 RG, и если бы этот boilerplate настройки страницы позволялся выставлять флаг DeviceRGB, каждый документ выглядел бы запятнанным RGB — и для этой проверки, и для проверки консистентности цвета PDF/A. HotPDF сохраняет и восстанавливает флаг вокруг той инициализации, поэтому считается только цвет, который вы реально нарисовали

Как найти RGB-контент, когда EndDoc поднимает исключение?

Читайте локацию в начале сообщения исключения — она говорит, какой из двух путей нашёл проблему. Сообщение имеет форму PDFXCompliance 'X-1a': <where> uses <what>, затем ссылку на ISO 15930-1 и совет конвертировать в CMYK или Gray либо переключиться на X-3 или X-4. Валидатор поднимает обычный Exception на первом нарушении, поэтому матчите по тексту, а не по классу исключения

Анатомия RGB-исключения HotPDF EndDoc в Delphi: флаг времени рисования сообщает общедокументное использование, а обход ресурсов называет точную страницу и ключ
читайте локацию в начале сообщения — общедокументный отчёт отправляет в код рисования, страница и имя ресурса указывают на один ключ
  • document content uses DeviceRGB (rg/RG operators, RGB images or annotation colours recorded during drawing) — сработал флаг времени рисования. Номера страницы нет, потому что флаг общедокументный и проверяется до обхода. Ищите в коде рисования вызовы SetRGB*, Flate-сжатые битмапы, RGB JPEG и аннотационные цвета TColor
  • page 3 image XObject /Im2 uses DeviceRGB content или page 1 shading Pattern /P1 uses DeviceRGB content — от обхода ресурсов, указывает на точный ключ ресурса
  • uses an ICCBased RGB colour space (N=3) после локации ColorSpace /... — типичный итог регистрации sRGB-профиля через RegisterICCProfile с выбором через SetFillColorSpace, путь, которого флаг времени рисования не видит
  • Вложенные локации цепляются друг за друга, например page 2 Form XObject /Fm1 ColorSpace /CS1, так что можно пройти путь от страницы вниз к ресурсу, несущему RGB-пространство
try
  Pdf.EndDoc;
except
  on E: Exception do
  begin
    if Pos('X-1a', E.Message) > 0 then
      LogPrepressFailure(Pdf.FileName, E.Message);  // ваш собственный логгер
    raise;
  end;
end;

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

Куда вписывается RGB-шлюз в вашем префлайте

EnsurePDFX1aContentColorSpaces — прицельный RGB-шлюз внутри вашей сборки, и он дополняет полный префлайт PDF/X-1a, а не заменяет его. Он покрывает RGB-семейства — через обход ресурсов от /Resources каждой страницы и через флаг времени рисования, который держат цветовые сеттеры HotPDF для inline-операторов rg и RG; вложенные пространства ведутся до глубины 8 как страховка от кривых циклов, и на каждый EndDoc сообщается первое нарушение. Прочие семейства цветов вроде Lab или CalGray — забота полного префлайта, поэтому прогоняйте готовый файл через внешний валидатор как часть сборки, чтобы получить полный вердикт соответствия. Более широкая картина вывода соответствия — в гиде по PDF/A, PDF/X и PDF/UA с HotPDF, а подключение валидатора к unattended-конвейеру — в автоматизации префлайт-проверок PDF на Delphi

HotPDF — нативный VCL PDF-компонент для Delphi и C++Builder с выводом PDF/X, PDF/A и PDF/UA, API CMYK- и плашечных цветов и поддержкой ICC OutputIntent. Детали, лицензирование и пробная загрузка — на странице продукта HotPDF Delphi PDF component