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

Рендиране на Separation и DeviceN Spot цветове в Delphi

HotPDF рендира Separation и DeviceN спот (spot) цветове върху заредени PDF страници, като определя цветовото пространство чрез HPDFResolveColorSpace, изчислява функцията за трансформиране на нюанси (tint-transform) с HPDFEvalTintTransform и преобразува резултата през алтернативното пространство в RGB за екрана. От версия v2.375.0 насам този тръбопровод изчислява и четирите типа PDF функции, включително Type 4 PostScript калкулатори, така че подготвен за печат файл с мастила по скалата 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 е първокласна фамилия, а не грешка. Пространство, което визуализаторът не може да интерпретира, просто се заменя с определен цвят по подразбиране, вместо да срива цялата страница, което съответства на поведението на големите четеци и предотвратява изчезването на съдържанието поради едно несъвместимо запълване

Как трансформаторът на нюанси превръща една стойност на мастилото в реален цвят?

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

Методът HPDFEvalTintTransform е двигателят зад тази стъпка. Въведен във версия v2.334.0, той изчислява експоненциални функции тип 2 (C0 + x^N * (C1 - C0) с ограничаване на Domain и Range), функции за свързване тип 3 (избор на подфункция по граници с ремапиране на кодирането) и дискретизирани функции тип 0 с 8-, 16- и 32-битови проби. Тип 0 е същият механизъм с таблица за справка (lookup table), който разгледахме от страна на създаването в статията за изграждане на 3D цветни LUT от тип 0; рендиращата страна обхожда същата структура в обратен ред – от декодираните байтове на пробите обратно към стойностите на компонентите

Калкулаторните функции тип 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;

Честност относно точността: софтуерен калкулатор с плаваща запетая с двойна точност няма да съвпадне напълно с професионален RIP на хардуерно ниво, а ограничаването на границите на Range може да се различава с един бит от други имплементации. За показване на екран и тестове това е напълно пренебрежимо; ако обаче разработвате цветно калибрирана система за проби (proofing), това е само първият етап и все още се нуждаете от истински 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, чийто профил се различава силно от неговата алтернатива, ще покаже алтернативното рендиране. За показване на екран и миниатюри (thumbnails) това е компромисът, който прави всеки лек визуализатор, и това е граница, която трябва да се посочи ясно във вашата собствена документация

Бъгът, който караше всяко търсене на именовано цветово пространство да се проваля

Логиката на операторите във версия 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 на функция от тип 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;

Тестовият сценарий (fixture) е толкова важен, колкото и самата проверка. Ръчно сглобен PDF файл с размер от няколкостотин байта, съдържащ само едно Separation запълване и нищо друго, не оставя никакво съмнение относно очаквания резултат; реален файл тества повече код, но не може да ви каже кой етап е пропаднал. Сега разглеждаме броенето на пиксели с очакван цвят като минимален праг за всеки тестов тест за рендиране, тъй като това е единственият стил на проверка, който извади тези дефекти наяве

Рендирането на спот цветове и CIE цветови пространства е част от тръбопровода за заведени документи в HotPDF Component за Delphi и C++Builder, заедно с изчислителния модул за функции, рендирането на засенчване и пътищата за експортиране на растерни изображения, показани по-горе