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

Рендеринг смесевых цветов Separation и DeviceN в Delphi

HotPDF выполняет рендеринг смесевых цветов Separation и DeviceN на загруженных страницах PDF путем разрешения цветового пространства с помощью HPDFResolveColorSpace, вычисления функции преобразования оттенков через HPDFEvalTintTransform, и конвертации результата через альтернативное цветовое пространство в RGB для экрана. Начиная с версии v2.375.0 этот конвейер вычисляет все четыре типа функций PDF, включая калькуляторы PostScript Type 4, поэтому готовый к печати файл с чернилами Pantone отображает свои реальные цвета вместо плейсхолдера. В этой статье мы расскажем о том, как работает этот конвейер, и, что не менее полезно, о том, какие ошибки возникали при его создании

Триггерный случай всегда один и тот же. Клиент получает PDF из типографии: заголовок набран смесевыми чернилами (spot ink) с конкретным именем, оформление упаковки использует смесь двух чернил DeviceN, а основной текст — обычный черный. В Acrobat все выглядит идеально. В вашем приложении на Delphi заголовок отрисовывается черным или, что еще хуже, вообще не отображается, и клиент отправляет баг-репорт на ваше программное обеспечение, а не на файл. Но файл в порядке. Просто рендерер не поддерживает цветовые пространства, используемые в файле

Почему смесевые цвета отрисовываются черным в просмотрщике PDF?

Смесевые цвета отображаются черными или пропадают вовсе, когда рендерер реализует только аппаратно-зависимые операторы цвета (rg, g, k) и игнорирует универсальные. Стандарт ISO 32000-1 §8.6 определяет три группы цветовых пространств: аппаратные (DeviceGray, DeviceRGB, DeviceCMYK), пространства на основе CIE (CalGray, CalRGB, Lab, ICCBased) и специальные пространства (Indexed, Separation, DeviceN, Pattern). Все, что находится за пределами аппаратной группы, выбирается с помощью универсальных операторов: cs и CS выбирают пространство по имени из словаря ресурсов страницы, а затем sc, SC, scn и SCN передают значения компонентов. Рендерер, который пропускает эти операторы, сохраняет тот цвет, который был установлен последним, что для страницы, начинающейся с заголовка смесевого цвета, означает исходный черный цвет DeviceGray

HotPDF добавил набор универсальных операторов в свой рендерер страниц в версии v2.333.0 вместе с единым путем разрешения: каждая запись ресурса /ColorSpace, будь то простое имя, встроенный массив или косвенная ссылка, разбирается в одну запись THPDFColorSpace, а каждый запрос цвета заливки или обводки проходит через один вызов HPDFResolveColor. Перечисление семейств наглядно показывает область покрытия

type
  THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
                           csfIndexed, csfCalGray, csfCalRGB, csfLab,
                           csfICCBased, csfSeparation, csfDeviceN,
                           csfUnsupported);

function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;

function HPDFResolveColor(const CS: THPDFColorSpace;
  const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;

Одно архитектурное решение оправдывает себя снова и снова: csfUnsupported — это полноценное семейство, а не ошибка. Пространство, которое рендерер не может интерпретировать, сводится к определенному запасному варианту (fallback) вместо прерывания рендеринга страницы. Это соответствует поведению основных программ просмотра и предотвращает ситуацию, когда одна экзотическая заливка делает всю страницу пустой

Как преобразование оттенка превращает значение интенсивности чернил в реальный цвет?

Пространство Separation содержит три элемента информации: имя чернил, альтернативное цветовое пространство и функцию преобразования оттенка. Массив [/Separation /PANTONE485 /DeviceCMYK f] говорит следующее: когда поток содержимого записывает 0.8 scn, передайте значение оттенка 0.8 в функцию f и закрасьте полученный в результате CMYK-квадруплет. DeviceN обобщает это на N чернил с функцией, принимающей N входных параметров. Само имя чернил носит лишь рекомендательный характер на экране; преобразование оттенка составляет всю семантику рендеринга, поэтому рендерер, который разбирает пространство, но пропускает функцию, не делает ничего полезного

Метод HPDFEvalTintTransform представляет собой функциональный движок, стоящий за этим шагом. Появившись в версии v2.334.0, он вычисляет экспоненциальные функции Type 2 (C0 + x^N * (C1 - C0) с ограничением Domain и Range), кусочные функции Type 3 (рекурсия подфункций по границам с перерегулированием Encode) и интерполированные функции Type 0 с 8-, 16- и 32-битными отсчетами. Type 0 — это та же структура справочных таблиц (LUT), которую мы рассматривали со стороны записи в статье о создании таблиц LUT цвета Type 0. Сторона рендеринга обходит ту же структуру в обратном порядке: от декодированных байтов выборки обратно к значениям компонентов

Калькуляторные функции Type 4 PostScript оставались последним рубежом. До версии v2.375.0 они аккуратно сводились к нейтральному плейсхолдеру; начиная с v2.375.0 HPDFEvalPostScriptCalculator выполняет полный набор операторов стандарта ISO 32000-1 §7.10.5 на ограниченном стеке операндов: арифметические, сравнения, логические и побитовые операторы, манипуляции со стеком, включая roll, а также ветвления if/ifelse. Семантика крайних случаев строже, чем кажется. Оператор PostScript round округляет половину в сторону большего значения, поэтому банковское округление Round в Delphi использовать нельзя; тригонометрические операторы работают в градусах, при этом atan возвращает значения в диапазоне [0, 360), а exp — это возведение в степень с двумя операндами, а не натуральная экспонента. Тот же вычислитель управляет градиентными заливками на основе функций, поэтому рендеринг осевых и радиальных заливок получил поддержку калькуляторных цветовых переходов в том же релизе

// Evaluate a Separation/DeviceN tint transform (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
  const Inputs: THPDFColorComps; InputCount: Integer;
  out AltComps: THPDFColorComps): Boolean;

// Type 4 PostScript calculator, ISO 32000-1 7.10.5 operator set.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
  const Inputs: THPDFColorComps; InputCount: Integer;
  var Outputs: THPDFColorComps; OutCount: Integer): Boolean;

Если говорить честно о точности: программный вычислитель с двойной точностью (double precision) не совпадет бит-в-бит с аппаратным растровым процессором (RIP), а ограничение по границе Range может отличаться на наименьший значащий бит в зависимости от реализации. Для отображения на экране и регрессионного рендеринга это не имеет значения; но если вы создаете систему цветопробы с управлением цветом, преобразование оттенка — это лишь первый этап, и вам по-прежнему потребуется полноценный модуль CMM далее по конвейеру

CalGray, CalRGB, Lab и ICCBased без использования движка ICC

Цветовые семейства на основе CIE идут по другой ветви того же распознавателя. HotPDF преобразует значения Lab через стандартную цепочку Lab -> XYZ -> sRGB, включая кубическое уравнение границы 6/29 в обратной функции преобразования, а также поддерживает CalRGB с его поканальной гаммой и линейной матрицей 3x3 и CalGray с его единой гаммой. Важная для производительности деталь — обработка точки белого: матрица преобразования XYZ в sRGB адаптируется по методу Брэдфорда к точке белого, объявленной в цветовом пространстве, и кэшируется в разрешенной записи THPDFColorSpace, поэтому попиксельная работа сводится к одному умножению 3x3 независимо от того, насколько экзотичен заявленный источник освещения

Пространства ICCBased получают намеренно прагматичную обработку. Спецификация PDF требует, чтобы каждый поток ICCBased объявлял альтернативное пространство /Alternate или подразумевал его через количество компонентов /N, как раз для того, чтобы просмотрщики без движка управления цветом могли отображать страницы корректно. HotPDF разрешает ICCBased через этот альтернативный вариант, либо предполагает DeviceGray, DeviceRGB или DeviceCMYK, если /N равен 1, 3 или 4 при отсутствии записи, и никогда не разбирает байты самого профиля. Это избавляет от зависимости от lcms и затрат на чтение профиля ценой колориметрической точности: пространство ICCBased, профиль которого сильно отличается от его альтернативного варианта, будет отображаться с альтернативным цветом. Для просмотра на экране и создания миниатюр это тот же компромисс, на который идет любой легкий просмотрщик, и это ограничение стоит четко прописать в вашей документации

Баг, из-за которого каждый поиск именованного цветового пространства давал промах

Связующий код операторов в версии v2.333.0 был выпущен с дефектом, который оставался незамеченным на протяжении сорока двух релизов: обработчики cs и CS искали свой операнд с ведущим слэшем (/CS0) в ключах словаря ресурсов, сохраненных без слэша (CS0). Каждый поиск именованного цветового пространства завершался промахом в 100% случаев, и код молча откатывался к стандартному DeviceGray. Визуальный симптом был скрытым: заливка Separation со значением 1 scn превращалась в DeviceGray 1.0, что рисует белым цветом, а белые чернила на белой странице — это не та ошибка отрисовки, которую легко заметить. Исправлением в версии v2.375.0 стало внедрение общего вспомогательного метода нормализации имен, применяемого при каждом сопоставлении операнда с ресурсом

В ходе того же расследования были обнаружены еще два сопутствующих бага. Во-первых, косвенные ссылки на объекты-массивы возвращались неразрешенными: модуль доступа к документу рендерера имел типизированные распознаватели только для потоков и словарей, поэтому ссылка /CS0 5 0 R, указывающая на отдельный массив [/Separation ...], возвращалась как сырая ссылка, и пространство разбиралось как неподдерживаемое. Во-вторых, функция HPDFReadNumericArray строго контролировала длину, требуя, чтобы массив PDF был как минимум равен размеру переданного буфера. Чтение параметров /C0 и /C1 функции Type 2 в четырехэлементный буфер завершалось ошибкой для одно- и трехкомпонентных альтернативных пространств, оставляя оба массива заполненными нулями. Из-за этого любой не-CMYK экспоненциальный оттенок отображался черным, пока в версии v2.376.0 не был представлен мягкий считыватель HPDFReadNumericArrayUpTo. Полезный урок: любой ключ PDF, который согласно спецификации содержит числовой массив переменной длины, должен считываться методом "заполняй то, что есть", так как буфер фиксированного размера со строгим сопоставлением превращает валидные файлы в скрытые нули

Как тестировать рендеринг смесевых цветов без ложных выводов?

Неприятный вопрос заключается в том, почему набор тестов оставался зеленым во время всех этих сбоев. Исходный регрессионный тест SeparationRendersDistinguishable лишь утверждал, что отрисованный растр не был полностью черным. Конвейер, который сбрасывал каждый смесевой цвет в DeviceGray, выдавал серый и белый цвет (что не является черным), поэтому утверждение выполнялось, хотя вся функция была неработоспособна. Слабые утверждения вида "не пустой", "не полностью черный" или "хэш не равен нулю" не могут отличить работающий рендерер от сломанного, потому что практически любой сценарий сбоя все равно генерирует какие-то пиксели

Стиль проверок, который действительно выявляет такие сбои, жестко фиксирует ожидаемый цвет: выполните рендеринг созданного вручную минимального PDF, в котором смесевой цвет разрешается в известный оттенок, а затем подсчитайте пиксели, в которых доминирует этот оттенок. Рендеринг в растровое изображение для проверки использует ту же точку входа RenderLoadedPageToBitmap, которая описана в руководстве по рендерингу страниц в растровые изображения

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  X, Y, RedHits: Integer;
  Px: TColor;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
      try
        RedHits := 0;
        for Y := 0 to Bmp.Height - 1 do
          for X := 0 to Bmp.Width - 1 do
          begin
            Px := Bmp.Canvas.Pixels[X, Y];
            if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
               (GetBValue(Px) < 100) then
              Inc(RedHits);
          end;
        // Lock the expected ink: demand a real area of red-dominant
        // pixels, never settle for "not all black".
        Assert(RedHits > 500);
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Тестовый файл (fixture) важен не меньше, чем само утверждение в тесте. Созданный вручную PDF длиной всего несколько сотен байт с единственной заливкой Separation исключает неоднозначность в отношении ожидаемого вывода. Файл из реального мира задействует больше кода, но не сможет подсказать, на каком этапе произошел сбой. Теперь мы рассматриваем подсчет пикселей ожидаемого цвета как минимальную планку для любого дымового теста рендеринга (smoke test), так как это единственный стиль проверок, позволивший выявить эти дефекты

Рендеринг смесевых цветов и цветовых пространств CIE является частью конвейера загруженных документов в составе HotPDF Component для Delphi и C++Builder, наряду с движком функций, рендерингом заливок и экспортом в растровые изображения, показанными выше