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

Функции PDF типов 2/3/4 в Delphi: экспоненциальные, сшивающие, PostScript

HotPDF, нативный компонент PDF на VCL для Delphi и C++Builder, вычисляет три типа функций PDF, построенных на формулах, а не на сеточных выборках: экспоненциальную интерполяцию типа 2, сшивание типа 3 и калькуляторные функции PostScript типа 4, соответствующие ISO 32000-1 §7.10.3, §7.10.4 и §7.10.5. Тип 2 смешивает два выходных вектора вдоль кривой, тип 3 объединяет в цепочку несколько подфункций на одной входной области определения, а тип 4 выполняет ограниченную программу на PostScript, способную ветвиться, сравнивать и вычислять почти всё, что нужно потоку содержимого из своих входных данных. Ошибитесь чуть-чуть в любом из трёх — и сбой никогда не заявит о себе как об ошибке: он проявится как градиент с мёртвой плоской полосой, плашечный цвет, отрисовывающийся чистым чёрным, или калькуляторная функция, дающая расхождение ровно на единицу именно на тех входных значениях, которые набор тестов случайно не попробовал

Эти три соседствуют с четвёртым типом, типом 0, который хранит выборочную сетку вместо формулы и описан отдельно в сопутствующей статье о таблицах цветового поиска типа 0. Оба семейства решают одну и ту же задачу — отображение входа в выход, — но тип 0 представляет собой данные, вычисленные один раз и запечённые в файл, тогда как типы 2, 3 и 4 — это код, который читающая программа вычисляет при каждом вызове. Все четыре типа разделяют одну точку диспетчеризации в рендерере HotPDF, ключом для которой служит запись /FunctionType словаря функции, так что затенению, преобразованию оттенка или плашечной функции полутонов никогда не нужно знать, какой из четырёх типов она получила, прежде чем запросить цвет

Как работает экспоненциальная функция PDF типа 2?

Функция PDF типа 2 вычисляет одну формулу — y = C0 + x^N × (C1 − C0), применяемую покомпонентно, — где x является единственным входом функции, нормализованным относительно её /Domain перед выполнением формулы (ISO 32000-1 §7.10.3). /C0 и /C1 — это выходные векторы на двух концах этого диапазона, по одному числу на каждый выходной компонент, а /N — показатель степени, задающий форму кривой между ними: N = 1 даёт прямой линейный переход, лежащий в основе большинства градиентных остановок и преобразований дуплекса, N выше 1 притягивает кривую к C0, а N между 0 и 1 подталкивает её к C1. RegisterExponentialFunction строит этот словарь из пяти аргументов и возвращает объект функции, готовый к подключению к затенению, плашечной функции полутонов или куда угодно ещё, где спецификация допускает ключ /Function

Соотношение количества компонентов между C0 и C1 важно дважды: один раз при создании функции типа 2, и ещё раз всякий раз, когда HotPDF приходится отрисовывать чужую функцию, которую он не создавал. На стороне создания RegisterExponentialFunction проверяет C0 и C1 друг относительно друга и выбрасывает исключение при их несовпадении, так что вызов, дошедший до BeginDoc, уже даёт самосогласованный объект функции. Однако на стороне рендеринга оценщику приходится доверять любым массивам /C0 и /C1, реально объявленным в исходном файле, — скажем, файле из типографии, открытом для предпросмотра, или подписанном документе, показываемом пользователю обратно, — а версии до 2.376.0 считывали эти массивы в буфер, рассчитанный на четыре компонента, случай CMYK. Экспоненциальный оттенок DeviceGray или DeviceRGB с одно- или трёхэлементными /C0 и /C1 при таком чтении молча проваливался, и оба массива оставались нулевыми, так что оттенок отрисовывался сплошным чёрным вместо задуманного цвета. Версия 2.376.0 изменила размер буфера чтения под реально объявленное количество выходов функции вместо фиксированного буфера — именно тот тип ошибки, который вскрывает только тестовый случай не-CMYK, поскольку существующий набор тестов повсюду использовал CMYK, где четыре-в-четыре всегда помещалось

var
  EaseIn: THPDFDictionaryObject;
begin
  // Type 2: one input, N > 1 biases the ramp toward C0 (an ease-in curve)
  EaseIn := Pdf.RegisterExponentialFunction(
    [0,1],           // Domain: single input, clamped to [0,1]
    [0, 0, 0],       // C0: output at x = 0
    [0.8, 0, 0],     // C1: output at x = 1
    3,               // N: exponent, 1 = linear, > 1 eases toward C0
    []);             // Range omitted: defaults to a [0,1] clamp per output
end;

Сшивание типа 3: объединение подфункций через массив Bounds

Функция PDF типа 3 сшивает k подфункций в одно кусочное отображение по /Domain единственного входа, и работу здесь обеспечивают два массива — /Bounds и /Encode (ISO 32000-1 §7.10.4). /Bounds содержит k − 1 внутренних точек разбиения, разрезающих /Domain на k последовательных интервалов; оценщик выбирает первый интервал, верхняя граница которого превышает вход, либо последний интервал, если вход достиг последней границы, и передаёт управление подфункции этого интервала. Затем /Encode заново отображает вход из его позиции внутри этого интервала на тот входной диапазон, который ожидает сама выбранная подфункция, — обычно [0, 1], если подфункция представляет собой ещё один экспоненциальный сегмент, — прежде чем вычисление продолжится на уровень глубже, уже в собственных /Domain и /Range этой подфункции

Оценщик сшивания в HotPDF раньше умел обрабатывать только ровно две подфункции, а его считыватель /Bounds требовал полного восьмиэлементного массива, так что единственная точка разбиения, реально нужная двухсегментному градиенту — одно число в /Bounds, — всегда не проходила разбор, и функция ничего не возвращала. /Encode вообще не применялся. Версия 2.376.0 переписала выбор как общий поиск по k подфункциям, описанный спецификацией, и стала считывать /Bounds по его реальной объявленной длине, так что трёх-, четырёх- или пятиостановочный градиент, сшитый из соответствующего числа экспоненциальных сегментов, теперь разрешается так же, как двухсегментный якобы всегда и разрешался. Пример ниже строит двухсегментный переход чёрный-через-красный-к-белому — форму, к которой осевое или радиальное затенение обращается всякий раз, когда одна экспоненциальная кривая не может перенести все цветовые остановки, которые требует дизайн

var
  ToRed, ToWhite, Ramp: THPDFDictionaryObject;
begin
  // Two linear segments: black->red over [0, 0.5], red->white over [0.5, 1]
  ToRed   := Pdf.RegisterExponentialFunction([0,1], [0, 0, 0],   [0.8, 0, 0], 1, []);
  ToWhite := Pdf.RegisterExponentialFunction([0,1], [0.8, 0, 0], [1, 1, 1],   1, []);

  Ramp := Pdf.RegisterStitchingFunction(
    [0,1],                  // Domain: the stitched function's own input range
    [ToRed, ToWhite],       // Functions: k = 2 sub-functions
    [0.5],                  // Bounds: k - 1 = 1 split point
    [0,1, 0,1],             // Encode: 2 numbers per sub-function
    []);                    // Range omitted: inherited from each sub-function
end;

Что может калькуляторная функция PostScript типа 4, чего не могут типы 2 и 3?

Функция PDF типа 4 выполняет настоящую, хотя и намеренно ограниченную, программу: калькулятор PostScript, который помещает свои входные данные в стек операндов, выполняет арифметические операторы, операторы сравнения, манипуляции со стеком и логические операторы, а также условные конструкции if/ifelse, и оставляет свои выходы в стеке по завершении (ISO 32000-1 §7.10.5, таблица 42). Здесь нет ни конструкции цикла, ни хранения именованных переменных — только стек, что делает соответствующую спецификации программу лёгкой для понимания, — но в рамках этого ограниченного набора операторов тип 4 может выразить то, чего не могут типы 2 и 3, например настоящую формулу смешивания нескольких красок для разделения DeviceN или плашечную функцию полутонов с условным порогом. Оценщик HotPDF, HPDFEvalPostScriptCalculator, один раз токенизирует программу — числа, операторы и блоки процедур { }, — а затем обходит стек операндов на 100 элементов (глубина, требуемая ISO 32000-1 §7.10.5) под жёстким потолком в 50 000 вычисленных операторов как защитным барьером против патологических или написанных вручную программ

Оператор roll: направление легко перепутать

roll — оператор, который с наибольшей вероятностью окажется перепутан по направлению с первой попытки, потому что и порядок его аргументов, и направление вращения противоположны тому, как их описывает естественный язык. n j roll снимает со стека счётчик n и величину вращения j, затем циклически сдвигает верхние n записей стека на j позиций, оборачивая элементы, выпадающие с одного конца, обратно на другой; канонический пример прямо из спецификации — a b c 3 1 roll, дающий c a b: верхний элемент перемещается в низ группы, а не наоборот, и каждый другой элемент сдвигается вверх на одну позицию, освобождая место. Оценщик HotPDF вычисляет новую позицию элемента стека i как (i + j) mod n, что в точности соответствует этому примеру, но это цикл в две строки, который так же легко написать с перевёрнутым направлением вращения, а зеркальный roll всё равно даёт правдоподобно выглядящий цвет — просто не тот цвет, который запросил автор файла

const
  Prog = '{ 3 1 roll }';   // (a b c) -> (c a b): the third input moves to the front
var
  Reorder: THPDFStreamObject;
begin
  // Type 4: 3 inputs, 3 outputs, no extra clamping beyond Domain/Range
  Reorder := Pdf.RegisterPostScriptFunction(
    [0,1, 0,1, 0,1],   // Domain: 2 numbers per input
    [0,1, 0,1, 0,1],   // Range: 2 numbers per output (required for Type 4)
    Prog);
end;

round — не то же самое, что Round в Delphi: округление вверх от .5 против банковского округления

Оператор PostScript round всегда разрешает ничью на .5 в сторону большего целого числа, а встроенная функция Delphi Round — нет: она округляет по правилу «половина к чётному», банковское округление, которое чередует направление округления .5, чтобы повторяющееся округление не накапливало смещение. Оба совпадают почти везде и расходятся именно на той границе, которая здесь важна, — Round(0.5) в Delphi возвращает 0, а Round(2.5) возвращает 2, тогда как round из спецификации PDF хочет получить 1 и 3 для тех же входов, — так что это несовпадение прячется при обычном тестировании, а затем проявляется как стабильное расхождение на единицу везде, где промежуточная арифметика калькуляторной программы попадает точно на полуцелое число. ISO 32000-1 §7.10.5, таблица 42, прямо указывает, что round продвигает дробное .5 к большему целому, поэтому HotPDF реализует этот оператор как Floor(x + 0.5) вместо вызова Round из Delphi, и любой код, который переписывает заново или вручную выборочно проверяет арифметику программы типа 4, нуждается в той же замене

function PostScriptRound(const X: Double): Double;
begin
  // ISO 32000-1 7.10.5 Table 42: round pushes .5 toward the greater
  // integer. Delphi's Round() is banker's rounding and disagrees here:
  // Round(0.5) = 0, Round(2.5) = 2 - both one short of the spec value.
  Result := Floor(X + 0.5);
end;

Проверка при регистрации выявляет неисправную калькуляторную программу заранее

Некорректно построенную программу типа 4 дёшево поймать на этапе создания и дорого поймать где-либо ещё, поэтому RegisterPostScriptFunction не просто сохраняет исходный текст: он один раз пробно вычисляет программу в средней точке объявленного /Domain, прежде чем объект функции вообще будет записан в документ. Несбалансированные блоки { }, нераспознанный оператор, опустошение стека или количество выходов, не совпадающее с /Range, — всё это приводит к сбою этого пробного прогона и немедленному выбросу исключения, причём стек вызовов указывает на сам вызов RegisterPostScriptFunction, а не на артефакт рендеринга, обнаруженный при контроле качества уже отгруженного файла. Пробный прогон в средней точке не доказывает, что программа корректна на всей области /Domain, — условное ветвление, которое ведёт себя неправильно только вблизи одного края входного диапазона, всё ещё может проскользнуть мимо единственной точки выборки, — но он отсекает целый класс программ, структурно сломанных, а не просто неверных в одном частном случае

Где градиенты и плашечные цвета применяют эти функции на практике

Типы 2, 3 и 4 редко появляются в реальном PDF изолированно; они возникают везде, где спецификация допускает ключ /Function, а два самых частых потребителя — затенения и преобразования оттенка плашечных цветов. Оператор sh осевого или радиального градиента (ISO 32000-1 §8.7.4.5) вычисляет свою /Function один раз для каждой позиции вдоль оси градиента, что в точности тот многоостановочный случай, для которого существует сшивание типа 3. Преобразование оттенка цветового пространства Separation или DeviceN — ещё одно частое пристанище для этих трёх типов, и именно здесь тип 4 оправдывает своё существование: одна плашечная краска обычно сводится к кривой типа 2 или типа 0, но смесь DeviceN из нескольких красок с реальным поведением треппинга и наложения часто нуждается в условной логике, которую может выразить только калькулятор PostScript, — случай, описанный в статье о рендеринге плашечных цветов Separation и DeviceN. RegisterSeparationFunc — парный вызов на стороне создания: он принимает имя красителя, альтернативное цветовое пространство и любой объект, возвращённый семейством функций Register*Function, и подключает это преобразование оттенка к ресурсу цветового пространства Separation, который остальная часть страницы может выбрать через scn/SCN

Вместе выборочные сетки типа 0 и эти три формульно-управляемых типа покрывают каждую /Function, которую может объявить PDF, и выбор нужного типа — это в основном вопрос того, что у вас уже есть: таблица поиска, вычисленная где-то ещё, становится типом 0, смешение с двумя конечными точками становится типом 2, несколько смешений, объединённых цепочкой по области определения, становятся типом 3, а всё, что содержит настоящую условную логику, становится типом 4. RegisterExponentialFunction, RegisterStitchingFunction и RegisterPostScriptFunction входят в стандартный компонент HotPDF для Delphi и C++Builder, наряду с остальным его API функций и затенений по ISO 32000-1