HotPDF renderuje kolory rozbarwień (Separation) oraz spotowe DeviceN na załadowanych stronach PDF poprzez rozpoznanie przestrzeni kolorów za pomocą HPDFResolveColorSpace, ocenę funkcji transformacji odcieni (tint-transform) przy użyciu HPDFEvalTintTransform i konwersję wyniku przez przestrzeń alternatywną na format RGB dla ekranu. Od wersji v2.375.0 potok ten obsługuje wszystkie cztery typy funkcji PDF, w tym kalkulatory PostScript typu Type 4, dzięki czemu gotowy do druku plik z atramentami Pantone wyświetla swoje rzeczywiste kolory zamiast symbolu zastępczego. Ten artykuł opisuje zasadę działania tego potoku, a także — co równie istotne — błędy, z którymi mierzyliśmy się podczas jego budowy
Scenariusz początkowy jest zawsze taki sam. Klient otrzymuje plik PDF z drukarni: nagłówek jest zapisany we wbudowanym kolorze spotowym, grafika opakowania używa mieszanki dwóch atramentów DeviceN, a treść tekstu jest zwykła czarna. W programie Acrobat wygląda to idealnie. W Twojej aplikacji w Delphi nagłówek renderuje się na czarno lub, co gorsza, nie renderuje się wcale, a klient zgłasza błąd w Twoim oprogramowaniu zamiast w pliku. Plik jest w porządku. Renderer po prostu nie obsługuje przestrzeni kolorów użytych w pliku
Dlaczego kolory spotowe renderują się na czarno w przeglądarce PDF?
Standard ISO 32000-1 §8.6 definiuje trzy grupy przestrzeni kolorów: przestrzenie urządzeń (DeviceGray, DeviceRGB, DeviceCMYK), przestrzenie oparte na CIE (CalGray, CalRGB, Lab, ICCBased) oraz przestrzenie specjalne (Indexed, Separation, DeviceN, Pattern). Wszystko poza grupą urządzeń jest wybierane za pomocą operatorów ogólnych: cs i CS wybierają przestrzeń po nazwie ze słownika zasobów strony, po czym sc, SC, scn oraz SCN dostarczają wartości składowych. Renderer, który pomija te operatory, zachowuje ostatnio ustawiony kolor, co dla strony rozpoczynającej się nagłówkiem w kolorze spotowym oznacza początkową czerń DeviceGray
HotPDF dodał zestaw operatorów ogólnych do swojego renderera stron w wersji v2.333.0, wraz ze zunifikowaną ścieżką rozpoznawania: każdy wpis zasobu /ColorSpace, niezależnie od tego, czy jest to czysta nazwa, tablica wbudowana czy referencja pośrednia, jest analizowany do rekordu THPDFColorSpace, a każde żądanie koloru wypełnienia lub obrysu przechodzi przez pojedyncze wywołanie HPDFResolveColor. Wyliczenie rodzin pokazuje ten zakres w skrócie
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;
Jedna decyzja projektowa przynosi korzyści wielokrotnie: csfUnsupported jest traktowane jako pełnoprawna rodzina, a nie błąd. Przestrzeń, której renderer nie potrafi zinterpretować, zostaje zdegradowana do określonego wariantu awaryjnego (fallback), zamiast przerywać renderowanie strony, co odpowiada zachowaniu popularnych przeglądarker i zapobiega sytuacji, w której jedno nietypowe wypełnienie psuje cały dokument
Jak transformacja odcienia przekształca wartość atramentu w rzeczywisty kolor?
Przestrzeń kolorów Separation przenosi trzy informacje: nazwę atramentu, alternatywną przestrzeń kolorów oraz funkcję transformacji odcienia. Tablica [/Separation /PANTONE485 /DeviceCMYK f] oznacza: gdy strumień zawartości zapisze 0.8 scn, przekaż wartość odcienia 0.8 do funkcji f i pomaluj wynikową czwórką CMYK. DeviceN uogólnia to do N atramentów za pomocą funkcji o N wejściach. Sama nazwa atramentu ma na ekranie jedynie charakter doradczy; transformacja odcienia stanowi pełną semantykę renderowania, stąd renderer, który analizuje przestrzeń, lecz pomija funkcję, nie wykonuje żadnej użytecznej pracy
Silnikiem wykonawczym za tym krokiem jest HPDFEvalTintTransform. Wprowadzony w wersji v2.334.0, oblicza funkcje wykładnicze typu Type 2 (C0 + x^N * (C1 - C0) z ograniczeniem Domain i Range), funkcje zszywające typu Type 3 (rekurencja podfunkcji wybieranej na podstawie Bounds z mapowaniem Encode) oraz funkcje próbkowane typu Type 0 z próbkami 8-, 16- i 32-bitowymi. Typ Type 0 to ten sam mechanizm tablicy przeglądowej (lookup table), który opisywaliśmy od strony zapisu w artykule o budowaniu tablic LUT kolorów typu Type 0; strona renderowania przechodzi tę samą strukturę w odwrotnym kierunku: od zdekodowanych bajtów próbek do wartości składowych
Kalkulatory PostScript typu Type 4 były ostatnim brakującym elementem
Do wersji v2.375.0 degradowały się one bezawaryjnie do neutralnego symbolu zastępczego; od wersji v2.375.0 funkcja HPDFEvalPostScriptCalculator wykonuje pełny zestaw operatorów standardu ISO 32000-1 §7.10.5 na ograniczonym stosie operandów: operacje arytmetyczne, porównania, operatory logiczne i bitowe, operacje na stosie (w tym roll) oraz instrukcje warunkowe if/ifelse. Semantyka skrajnych przypadków jest bardziej rygorystyczna niż się wydaje. Operator PostScript round zaokrągla połówki w stronę większej wartości, więc delphiowska funkcja Round (stosująca zaokrąglanie bankierskie) nie może być użyta; operatory trygonometryczne działają w stopniach, przy czym atan zwraca wartości w zakresie [0, 360); z kolei exp to potęga o dwóch operandach, a nie eksponenta naturalna. Ten sam interpreter steruje również wypełnieniami gradientowymi opartymi na funkcjach, stąd w tym samym wydaniu renderowanie cieniowania osiowego i radialnego zyskało rampy sterowane kalkulatorem
// 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;
Uczciwość w kwestii precyzji: kalkulator programowy obliczany w podwójnej precyzji (double precision) nie będzie zgodny z procesorem rastrującym (RIP) bit w bit, a ograniczenia na granicach przedziału Range mogą różnić się o najmniej znaczący bit (LSB) od innej implementacji. W przypadku wyświetlania na ekranie i testów regresyjnych jest to bez znaczenia; jeśli budujesz system proofingu z zarządzaniem barwą, transformacja odcieni to dopiero pierwszy etap i nadal potrzebujesz silnika CMM na dalszym etapie
CalGray, CalRGB, Lab oraz ICCBased bez silnika ICC
Rodziny oparte na CIE przechodzą przez drugą gałąź tego samego mechanizmu. HotPDF konwertuje wartości Lab przez standardowy łańcuch Lab na XYZ na sRGB, uwzględniając punkt przegięcia 6/29 w odwrotnej funkcji przejścia, oraz obsługuje CalRGB z wartością gamma dla każdego kanału i liniową macierzą 3x3, a także CalGray z pojedynczą wartością gamma. Detalem istotnym dla wydajności jest obsługa punktu bieli: macierz konwersji XYZ na sRGB przechodzi adaptację Bradforda do punktu bieli zadeklarowanego w przestrzeni kolorów i jest buforowana w rekordzie THPDFColorSpace, więc operacja dla każdego piksela to pojedyncze mnożenie 3x3, niezależnie od tego, jak nietypowe jest zadeklarowane źródło światła
Przestrzenie ICCBased są traktowane w sposób celowo pragmatyczny. Specyfikacja PDF wymaga, aby każdy strumień ICCBased deklarował przestrzeń alternatywną /Alternate lub przestrzeń dorozumianą przez liczbę składowych /N, właśnie po to, by przeglądarki bez silnika zarządzania barwą mogły wyrenderować obraz poprawnie. HotPDF rozpoznaje przestrzeń ICCBased za pomocą tej alternatywy lub przyjmuje DeviceGray, DeviceRGB bądź DeviceCMYK na podstawie wartości /N równej odpowiednio 1, 3 lub 4, gdy wpis jest nieobecny, i nigdy nie analizuje samych bajtów profilu. Oznacza to brak zależności od lcms oraz brak kosztów wyszukiwania profili, kosztem dokładności kolorymetrycznej: przestrzeń ICCBased, której profil silnie odbiega od wariantu alternatywnego, wyświetli obraz w kolorach alternatywnych. W przypadku wyświetlania na ekranie i miniatur (thumbnails) jest to kompromis, na który decyduje się każda lekka przeglądarka i należy to jasno opisać we własnej dokumentacji
Błąd, który uniemożliwiał wyszukanie każdej nazwanej przestrzeni kolorów
Mechanizm operatorów z wersji v2.333.0 został wydany z defektem, który pozostawał niewidoczny przez czterdzieści dwa wydania: procedury obsługi cs oraz CS wyszukiwały swój operand z zachowaniem wiodącego ukośnika (/CS0) w słownikach zasobów, gdzie klucze były przechowywane bez ukośnika (CS0). Każde wyszukiwanie nazwanej przestrzeni kolorów kończyło się niepowodzeniem w stu procentach przypadków, a kod bezgłośnie wycofywał się do domyślnego DeviceGray. Widoczny objaw był subtelny w najgorszy możliwy sposób: wypełnienie Separation o wartości 1 scn stawało się wartością 1.0 w przestrzeni DeviceGray, co oznacza kolor biały — a biały atrament na białej stronie to nie jest błąd, który ktokolwiek uwieczniłby na zrzucie ekranu. Poprawka z wersji v2.375.0 wprowadziła wspólną funkcję normalizacji nazw przy każdym wyszukiwaniu zasobu na podstawie operandu
Dwa powiązane błędy wykryto w trakcie tego samego badania. Po pierwsze, referencje pośrednie do obiektów będących tablicami były zwracane bez ich rozwiązania: dostęp do dokumentu w rendererze posiadał dedykowane translatory tylko dla strumieni i słowników, więc wpis /CS0 5 0 R wskazujący na samodzielną tablicę [/Separation ...] zwracał surową referencję i przestrzeń była oznaczana jako nieobsługiwana. Po drugie, funkcja HPDFReadNumericArray wymuszała ścisłą długość, wymagając, aby tablica PDF była co najmniej tak długa, jak dostarczony bufor. Odczyt tablic /C0 oraz /C1 funkcji typu Type 2 do czteroelementowego bufora kończył się więc niepowodzeniem dla wariantów alternatywnych o jednej i trzech składowych, pozostawiając obie tablice wyzerowane, przez co każdy wykładniczy odcień inny niż CMYK renderował się na czarno do czasu, gdy w wersji v2.376.0 wprowadzono elastyczny czytnik HPDFReadNumericArrayUpTo. Wniosek: każdy klucz PDF dokumentowany jako zawierający tablicę numeryczną o zmiennej długości musi być odczytywany za pomocą czytnika typu fill-what-exists (uzupełnij to, co istnieje), jako że sztywny rozmiar bufora ze ścisłym dopasowaniem zamienia poprawne pliki w zerowe dane
Jak testować renderowanie kolorów spotowych bez wprowadzania się w błąd?
Niewygodnym pytaniem pozostaje to, dlaczego zestaw testów pozostawał zielony przez cały ten czas. Oryginalny test regresyjny, SeparationRendersDistinguishable, sprawdzał jedynie, czy wyrenderowana mapa bitowa nie jest całkowicie czarna. Potok, który degradował każdy kolor spotowy do DeviceGray, generował szary i biały obraz, co nie jest czernią, więc asercja przechodziła pomyślnie, podczas gdy cała funkcja była martwa. Słabe asercje typu „nie puste”, „nie całkowicie czarne” czy „skrót jest niezerowy” nie pozwalają odróżnić działającego renderera od uszkodzonego, ponieważ niemal każdy stan awarii nadal generuje jakieś piksele
Styl asercji, który faktycznie wykrywa te awarie, blokuje oczekiwany kolor: należy wyrenderować ręcznie przygotowany minimalny plik PDF, w którym atrament Separation przyjmuje znaną barwę, a następnie zliczyć piksele, w których ta barwa dominuję. Renderowanie do mapy bitowej w celu weryfikacji używa tego samego punktu wejścia RenderLoadedPageToBitmap opisanego w przewodniku renderowania stron do mapy bitowej
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;
// Blokuj oczekiwany atrament: wymagaj rzeczywistego obszaru z dominacją
// koloru czerwonego, nigdy nie zgadzaj się na „nie całkiem czarne”.
Assert(RedHits > 500);
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Makieta (fixture) ma tak samo duże znaczenie jak asercja. Ręcznie przygotowany plik PDF o rozmiarze kilkuset bajtów, zawierający jedno wypełnienie Separation i nic poza tym, nie pozostawia wątpliwości co do oczekiwanego wyniku; plik z prawdziwego zdarzenia angażuje więcej kodu, lecz nie wskaże, który etap zawiódł. Zliczanie pikseli o oczekiwanym kolorze traktujemy obecnie jako minimalny wymóg dla każdego testu dymnego (smoke test) renderowania, ponieważ był to jedyny styl asercji, który ujawnił te defekty
Spot-color and CIE color-space rendering is part of the loaded-document pipeline in the HotPDF Component for Delphi and C++Builder, alongside the function engine, shading rendering, and the bitmap export paths shown above