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

Цветопроба CMYK с наложением и устройства отрисовки в HotPDF

Голубая плашка лежит на жёлтом фоне, и графическое состояние, красящее её, несёт /op true и /OPM 1. На экране перекрытие — плоский голубой. На печатном листе то же перекрытие выходит зелёным, потому что RIP не тронул жёлтую секцию и положил голубую поверх. Оба результата следуют файлу буква в букву; экранный рендер просто никогда не спрашивал, что делает с этим флагом устройство воспроизведения, поэтому одобрение на экране прикрывает цвет, который никогда не дойдёт до бумаги

Почему страница печатается иначе, чем предпросматривается?

Наложение — инструкция устройству вывода насчёт секций, а у RGB-экрана секций нет. ISO 32000-1, §8.6.7 определяет, что происходит, когда запись OP (обводка) или op (заливка) ExtGState истинна: красочные компоненты, которые текущая краска не называет, сохраняют то, что уже было на их секции, вместо стирания в ноль. Голубой объект, нарисованный так, не убирает с жёлтой секции ничего. Вьювер, композирующий в RGB, хранить жёлтый негде, поэтому он красит голубым поверх и двигается дальше

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

Сравнение overprint в CMYK в HotPDF: обычный просмотрщик выбивает жёлтый под голубым, а предпросмотр overprint цветопробует обе триадные краски и выдаёт случайно наложенный поверх белый текст
Обычный вьювер красит одной верхней краской, тогда как цветопроба наложения складывает обе краски, обнажая ловушки вроде случайно наложенной поверх белой литеры

Включение цветопробы через RenderOverprintPreview

HotPDF выставляет цветопробу одним булевым свойством на документе — THotPDF.RenderOverprintPreview, по умолчанию False. Когда оно выставлено, растровый рендер отслеживает триадные секции под каждой накладывающейся краской и применяет правила §8.6.7 перед записью пикселя, поэтому перекрытия показывают ту комбинацию красок, какую выдала бы печатная машина. Оставлять умолчание выключенным правильно для обычного просмотра: читатели офисного PDF ждут, что он выглядит так, как показывает их привычный вьювер. Включайте его на том шаге, чей вывод уходит к предпечатному оператору

var
  Pdf: THotPDF;
  Device: THPDFBitmapRenderDevice;
  Proof: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('cover-cmyk.pdf');
    Pdf.RenderOverprintPreview := True;    // цветопроба секций, по умолчанию False
    Device := THPDFBitmapRenderDevice.Create;
    try
      if Pdf.RenderLoadedPageToDevice(0, 150, Device) then
      begin
        Proof := Device.TakeBitmap;        // владение переходит к вызывающему коду
        try
          Image1.Picture.Assign(Proof);
        finally
          Proof.Free;
        end;
      end;
    finally
      Device.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Правила секций зависят от цветового пространства накладывающейся краски, и их стоит знать: каждое объясняет отдельный сюрприз на пробе:

  • DeviceCMYK под /OPM 1: нулевой компонент оставляет свою секцию нетронутой, ненулевой — заменяет. Cyan 100 / yellow 0 поверх жёлтого фона сохраняет жёлтый и даёт зелёный
  • DeviceGray: краска называет только чёрную секцию, поэтому голубой, пурпурный и жёлтый под ней выживают. Белый DeviceGray-логотип, выставленный в наложение на фоне CMY, потому и исчезает
  • Separation и DeviceN: компонент с именем Cyan, Magenta, Yellow или Black заменяет ту триадную секцию напрямую, /None не красит вовсе, а /All пишет каждую триадную секцию
  • Плашечные компоненты: любое другое имя трактуется как плашечная краска; её tint-преобразование даёт альтернативный цвет, который перемножается по триадным секциям под ней, поэтому плашка затемняет то, что накрывает, вместо замены

Цветопроба применяется к залитым и обведённым путям и к тексту, включая текст в режиме обводки, где решает флаг обводки OP, а не op. Изображения, inline-изображения и sh-затенения продолжают через свои полные растровые цветовые конвейеры и красят с обычной прозрачностью. На практике это покрывает то, что preflight-отчёты мечают чаще всего: правила наложения, ключевые линии, текст и плоские тинты

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

Настройка участвует и в идентичности кешированных рендеров. RenderLoadedPageToBitmapCached ключует свои записи по странице, DPI и варианту, собранному из настроек рендеринга, включающих RenderOverprintPreview, а дисковый ярус использует тот же вариант. Вьювер, скачущий между обычным и цветопробным предпросмотром, получает две отдельные записи кеша и никогда не нуждается в вызове InvalidateRenderedPageCache из-за того, что переключатель сменился

Ловушка /OPM 0 в файлах DeviceCMYK

Вот случай, отправляющий людей искать баг. Файл выставляет /op true на DeviceCMYK-заливке голубым, но оставляет OPM на умолчании 0 — и цветопроба всё ещё показывает голубой, выбивающий жёлтый. Цветопроба права. В режиме наложения 0 §8.6.7 говорит, что DeviceCMYK-краска выставляет все четыре триадных компонента, нули включительно, поэтому нулевой жёлтый компонент заменяет жёлтую секцию в точности как выбивание. Режим наложения 0 защищает только секции, которые краска не называет, а для четырёхкомпонентного CMYK-цвета это плашечные секции и ничего больше

На триадных секциях, которые показывает композитная проба, режим 0 и обычное выбивание дают одни и те же пиксели, поэтому HotPDF красит те DeviceCMYK-объекты как обычные выбивания. Чтобы CMYK-объект накладывался на другие триадные краски, ExtGState, несущий /op true, нуждается ещё и в /OPM 1. Когда проба и замысел дизайнера расходятся на DeviceCMYK-объекте, откройте тот ExtGState и ищите запись OPM, прежде чем подозревать что-либо ещё

Есть и обратный сюрприз. Под /OPM 1 белый CMYK — четыре нуля, а четыре нуля под наложением оставляют каждую секцию как была. Белый текст, выставленный в наложение, исчезает на листе, и цветопроба показывает его исчезновение — в этом весь смысл её запуска

Три устройства за RenderLoadedPageToDevice

THotPDF.RenderLoadedPageToDevice(PageIndex, DPI, Device) — единственная точка входа, чью цель выбирает переданный объект устройства. Она поднимает исключение для nil-устройства, трактует DPI ниже 1 как 150 и возвращает то, что устройство сообщит из своего метода Execute. THPDFRenderDevice — абстрактная база с двумя виртуальными членами: Kind, возвращающим rdkBitmap, rdkDeviceContext или rdkEnhancedMetafile, и Execute(Document, PageIndex, DPI)

Три поставляемых устройства владеют своим выводом по-разному:

  • THPDFBitmapRenderDevice держит TBitmap в свойстве Bitmap и освобождает его в деструкторе, если TakeBitmap не передал его вам раньше
  • THPDFDeviceContextRenderDevice.Create(ADC, AWidth, AHeight) рисует в HDC, который у вас уже есть, и ничем не владеет; конструктор поднимает исключение на нулевом DC или неположительном размере, и фон не красится, поэтому экранный DC нуждается в собственной белой заливке
  • THPDFMetafileRenderDevice держит TMetafile, пока TakeMetafile его не передаст. Текст и векторные пути остаются масштабируемыми в расширенном метафайле, а image XObject приходят растрированными
Архитектура отрисовки HotPDF: один вызов RenderLoadedPageToDevice расходится на устройства отрисовки bitmap, контекст принтера и enhanced metafile со своими значениями Kind и правилами владения
Один вызов отрисовки веером расходится на устройства bitmap, device context и metafile, чей Kind сообщает цель, а методы Take передают владение

Владение следует обычным правилам Delphi. Читайте свойство Bitmap или Metafile, когда объект должен жить лишь столько, сколько устройство; вызывайте метод Take, когда он обязан его пережить. Возьмите растр, освободите устройство, забудьте про растр — и утечка растёт с числом страниц: невидимая на пятистраничном примере, очевидная на пакете в пятьсот страниц. Дайте каждому объекту собственный try/finally — и вопрос «кто что освобождает» ответится сам

Диспетчеризуйте по Kind в коде приложения вместо проверки is THPDFBitmapRenderDevice. Поскольку Execute и Kind — публичные виртуальные методы, вы можете написать собственный класс устройства, и код, проверяющий тип, продолжит работать, когда одно из ваших устройств заменит поставляемое

Почему принтерный рендер приземляется в угол?

Путь контекста устройства размеряет страницу по аргументу DPI, а не по ширине и высоте, которые вы передали. Рендерер масштабирует PDF-единицы на DPI / 72 и использует высоту только чтобы пришвартовать начало страницы к низу поверхности. Зашейте 300 и отправьте задание на принтер 600 dpi — страница выйдет в половину размера, прижатая к левому нижнему углу листа. Разрешение принтера — свойство драйвера, так что спрашивайте его у драйвера:

procedure PrintPage(Pdf: THotPDF; PageIndex: Integer);
var
  Device: THPDFDeviceContextRenderDevice;
  PrinterDPI, W, H, Tmp: Integer;
  L, B, R, T: Single;
begin
  if not Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T) then
    Exit;
  Printer.BeginDoc;
  try
    // спросите драйвер; 300, 600 и 1200 dpi — все ходовые
    PrinterDPI := GetDeviceCaps(Printer.Handle, LOGPIXELSX);
    W := Round((R - L) * PrinterDPI / 72);
    H := Round((T - B) * PrinterDPI / 72);
    if (Pdf.GetLoadedPageRotation(PageIndex) mod 180) = 90 then
    begin
      Tmp := W; W := H; H := Tmp;            // поменять для страниц 90 / 270
    end;
    Device := THPDFDeviceContextRenderDevice.Create(
      Printer.Canvas.Handle, W, H);
    try
      Pdf.RenderLoadedPageToDevice(PageIndex, PrinterDPI, Device);
    finally
      Device.Free;
    end;
  finally
    Printer.EndDoc;
  end;
end;

Ширина и высота берутся из видимой коробки страницы — CropBox, обрезанного по MediaBox, — в собственном разрешении принтера, и вызывающий меняет их местами для повёрнутых страниц до создания устройства. Это совпадает с тем, чего рендерер ждёт, пришвартовывая начало координат

Цветопроба наложения не идёт этим путём. Устройства контекста устройства и метафайла выпускают GDI-вызовы прямо в цель, а правилам секций нужны пиксели, уже лежащие под каждой краской, — они есть только у растрового рендера. Для бумажной пробы срендерите растровую цветопробу и растяните её на холст принтера. Небольшое самодельное устройство держит это за той же точкой входа:

type
  // собственное устройство: растровая цветопроба, затем растяжка на принтер
  TPrinterProofDevice = class(THPDFRenderDevice)
  public
    function Kind: THPDFRenderDeviceKind; override;
    function Execute(Document: THotPDF; PageIndex, DPI: Integer): boolean;
      override;
  end;

function TPrinterProofDevice.Kind: THPDFRenderDeviceKind;
begin
  Result := rdkDeviceContext;
end;

function TPrinterProofDevice.Execute(Document: THotPDF;
  PageIndex, DPI: Integer): boolean;
var
  Proof: TBitmap;
  OldProof: Boolean;
begin
  OldProof := Document.RenderOverprintPreview;
  Document.RenderOverprintPreview := True;
  try
    Proof := Document.RenderLoadedPageToBitmap(PageIndex, DPI);
  finally
    Document.RenderOverprintPreview := OldProof;  // вернуть настройку вызывающего как была
  end;
  Result := Proof <> nil;
  if Result then
  try
    Printer.Canvas.StretchDraw(
      Rect(0, 0, Printer.PageWidth, Printer.PageHeight), Proof);
  finally
    Proof.Free;
  end;
end;

Вызывайте его между Printer.BeginDoc и Printer.EndDoc с DPI, дающим растру достаточно пикселей на лист, — около 200–300 для пробы формата letter. Прямоугольник растяжки выше заполняет печатную область; держите в нём аспектное отношение страницы, если страница и бумага различаются формой

Куда цветопроба встаёт рядом с preflight

Цветопроба отвечает на один вопрос: какие краски выживают там, где объекты перекрываются. Она композируется на том же 24-битном RGB-растре, что и любой другой рендер, поэтому её цвета показывают комбинацию красок, а не измеренный цвет листа. Условия печатной машины — вопрос цветоуправления, и документ обходится с ними через ConfigureRenderICCWorkflow, принимающий профиль пробы, выходной профиль и флаг компенсации точки чёрного, и ClearRenderICCWorkflow, чтобы сбросить их снова

Самое продуктивное место для цветопробного рендера — рядом с проверками, которые вы уже гоняете. Проход preflight рапортует, что чёрный текст выставлен в наложение; цветопробный рендер показывает оператору, что это делает на странице; оба попадают в один отчёт. Упаковочная работа любит мешать наложение с плашечными красками, и разбор статьи о отрисовке плашечных цветов Separation и DeviceN покрывает красочную сторону той же страницы, включая односекционные просмотры через RenderLoadedPageSeparationPlate. Статьи о рендере страницы PDF в растр и о печати загруженного PDF через TPrinter описывают обе цели устройств в их обычной форме

Всё это работает в нативном коде Delphi и C++Builder без DLL рендеринга, поставляемой рядом с исполняемым файлом. На странице компонента HotPDF для Delphi перечислены возможности рендеринга и предпечатной подготовки, а пробная сборка позволяет прогнать эти примеры на ваших собственных печатных файлах