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: округлення вгору проти банківського округлення
Оператор PostScript round щоразу розв'язує нічию на .5 у бік більшого цілого, а вбудована функція Round у Delphi — ні: вона округлює до парного (half-to-even), конвенцію банківського округлення, що чергує, в який бік падає нічия на .5, аби повторне округлення не накопичувало зміщення. Обидва варіанти узгоджуються майже всюди й розходяться саме на тій межі, що тут важить, — Round(0.5) у Delphi повертає 0, а Round(2.5) повертає 2, тоді як round у специфікації PDF хоче 1 і 3 для тих самих входів, — тож розбіжність ховається за побіжним тестуванням, а потім відтворюється як стійке відхилення на одиницю всюди, де проміжна математика програми калькулятора потрапляє точно на напівціле число. Таблиця 42 ISO 32000-1 §7.10.5 прямо каже, що 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