HotPDF рендерить плашкові (spot) кольори Separation та DeviceN на завантажених сторінках PDF шляхом розпізнавання колірного простору через HPDFResolveColorSpace, обчислення функції перетворення відтінків через HPDFEvalTintTransform, та конвертації результату через альтернативний колірний простір в RGB для екрана. Починаючи з версії v2.375.0 цей конвеєр підтримує всі чотири типи функцій PDF, включаючи калькулятори PostScript Type 4, тому підготовлений до друку файл із чорнилами Pantone відображає свої реальні кольори замість заглушки. У цій статті розглядається, як працює цей конвеєр, і, що не менш корисно, які помилки виникали під час його створення
Ситуація, яка це викликає, завжди однакова. Клієнт отримує PDF з друкарні: заголовок виконано в іменованому плашковому кольорі, макет упаковки використовує суміш двох чорнил 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), кусочні (stitching) функції Type 3 (рекурсія підфункцій за вибором меж із перемапуванням Encode) та табличні (sampled) функції 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 округлює половину до більшого значення, тому бухгалтерське округлення Delphi Round використовувати не можна; тригонометричні оператори працюють у градусах, причому 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;
Слід чесно сказати про точність: програмний калькулятор з обчисленнями подвійної точності не збігатиметься з растровим процесором (RIP) біт у біт, а обмеження на межі Range може відрізнятися на найменш значущий біт від інших реалізацій. Для відображення на екрані та регресійного тестування це не має значення; якщо ви створюєте систему кольоропроби під керуванням кольором, перетворення відтінку — це лише перший етап, і вам все одно знадобиться повноцінний движок керування кольором (CMM)
CalGray, CalRGB, Lab та ICCBased без системи ICC
Колірні простори на основі CIE йдуть іншою гілкою того самого резолвера. HotPDF конвертує значення Lab через стандартний ланцюжок від Lab до XYZ та sRGB, включаючи куб точки зламу 6/29 у зворотній функції перетворення, та обробляє CalRGB з гамою на кожен канал і лінійною матриєю 3x3, а також CalGray з однією гамою. Деталь, важлива для продуктивності — обробка точки білого: матриця перетворення XYZ на sRGB адаптується за методом Бредфорда (Bradford-adapted) до точки білого, оголошеної в колірному просторі, та кешується в отриманому записі 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, де колір Separation приводиться до відомого відтінку, а потім підрахувати пікселі з переважанням цього відтінку. Рендеринг у бітову карту для перевірки використовує ту саму точку входу 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;
Шаблон-фікстура важлива так само, як і перевірка. Створений вручну PDF розміром у кілька сотень байтів, що містить одну заливку Separation і більше нічого, не залишає сумнівів щодо очікуваного результату; файл із реального життя охоплює більше коду, але не може вказати, на якому саме етапі стався збій. Темої ми розглядаємо підрахунок пікселів очікуваного кольору як мінімальний поріг для будь-вого димового тесту рендерингу, оскільки це єдиний спосіб перевірки, який змусив виявити ці дефекти
Рендеринг плашкових кольорів та колірних просторів CIE є частиною конвеєра обробки завантажених документів у складі HotPDF Component для Delphi та C++Builder, разом із движком обчислення функцій, рендерингом затінень та експортом у бітові карти, що описані вище