Artykuł techniczny

Renderowanie kolorów Separation i DeviceN w Delphi

HotPDF renderuje kolory dodatkowe Separation i DeviceN na wczytanych stronach PDF, rozwiązując przestrzeń barw przez HPDFResolveColorSpace, obliczając funkcję tint transform przez HPDFEvalTintTransform i przekształcając wynik przez przestrzeń alternatywną na RGB dla ekranu. Od wersji v2.375.0 ten potok oblicza wszystkie cztery typy funkcji PDF, w tym kalkulatory PostScript typu 4, więc plik gotowy do druku z farbami Pantone wyświetla swoje prawdziwe kolory zamiast zastępnika. Ten artykuł przechodzi przez to, jak ten potok działa, i, równie użytecznie, jak zawodził w trakcie budowania

Przypadek wyzwalający jest zawsze ten sam. Klient dostaje plik PDF z drukarni: nagłówek złożono nazwaną farbą dodatkową, grafika opakowania używa dwufarbowej mieszanki DeviceN, a tekst główny jest zwykłą czernią. W Acrobacie wygląda idealnie. W twojej aplikacji w Delphi nagłówek renderuje się na czarno albo, co gorsza, nie renderuje się wcale, a klient zgłasza błąd przeciwko twojemu oprogramowaniu, a nie przeciwko plikowi. Plik jest w porządku. Renderer po prostu nie mówi przestrzeniami barw, których ten plik używa

Dlaczego kolory dodatkowe renderują się na czarno w przeglądarce PDF?

Kolory dodatkowe renderują się na czarno albo znikają, gdy renderer implementuje wyłącznie operatory barw urządzenia (rg, g, k) i ignoruje te ogólne. ISO 32000-1 §8.6 definiuje trzy grupy przestrzeni barw: 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ń wybiera się operatorami ogólnymi: cs i CS wybierają przestrzeń po nazwie ze słownika zasobów strony, a potem sc, SC, scn i SCN dostarczają wartości składowych. Renderer pomijający te operatory zachowuje ostatnio ustawiony kolor, którym dla strony zaczynającej się od nagłówka w kolorze dodatkowym jest początkowa czerń DeviceGray

HotPDF dodał zestaw operatorów ogólnych do swojego renderera stron w v2.333.0, wraz z ujednoliconą ścieżką rozwiązywania: każdy wpis zasobu /ColorSpace, czy to goła nazwa, wbudowana tablica, czy odwołanie pośrednie, jest parsowany do jednego rekordu THPDFColorSpace, a każde żądanie koloru wypełnienia albo obrysu przechodzi przez pojedyncze wywołanie HPDFResolveColor. Wyliczenie rodzin pokazuje zasięg na pierwszy rzut oka

HotPDF: trzy rodziny przestrzeni barw PDF z sekcji 8.6 ISO 32000-1, gdzie przestrzenie urządzeń maluje się operatorami rg g k, a przestrzenie oparte na CIE i specjalne są osiągalne wyłącznie przez ogólne operatory cs CS scn SCN
Tylko rodzina urządzeń odpowiada na bezpośrednie operatory rg, g i k — każda przestrzeń oparta na CIE i każda specjalna zależy od ogólnego zestawu cs, CS, scn i SCN, a renderer, który je pomija, zachowuje poprzedni kolor
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 spłaca się wielokrotnie: csfUnsupported jest pełnoprawną rodziną, a nie błędem. Przestrzeń, której renderer nie potrafi zinterpretować, degraduje się do zdefiniowanego zachowania zapasowego zamiast przerywać stronę, co odpowiada zachowaniu głównych przeglądarek i nie pozwala jednemu egzotycznemu wypełnieniu wygasić skądinąd renderowalnego dokumentu

Jak tint transform zamienia jedną wartość farby w prawdziwy kolor?

Przestrzeń Separation niesie trzy informacje: nazwę farby, alternatywną przestrzeń barw i funkcję tint transform. Tablica [/Separation /PANTONE485 /DeviceCMYK f] mówi: gdy strumień treści zapisze 0.8 scn, podaj wartość krycia 0.8 do funkcji f i pomaluj wynikową czwórką CMYK. DeviceN uogólnia to na N farb z funkcją o N wejściach. Sama nazwa farby jest na ekranie wyłącznie informacyjna; całą semantyką renderowania jest tint transform, więc renderer, który parsuje przestrzeń, ale pomija funkcję, nie zrobił jeszcze nic użytecznego

HPDFEvalTintTransform to silnik funkcji stojący za tym krokiem. Wylądował w v2.334.0 i oblicza funkcje wykładnicze typu 2 (C0 + x^N * (C1 - C0) z ograniczaniem przez Domain i Range), funkcje zszywające typu 3 (rekurencja podfunkcji wybieranych przez Bounds z przemapowaniem Encode) oraz funkcje próbkowane typu 0 z próbkami 8-, 16- i 32-bitowymi. Typ 0 to ta sama maszyneria tablic przeglądowych, którą omówiliśmy od strony tworzenia w artykule o budowaniu tablic LUT typu 0; strona renderująca przechodzi identyczną strukturę w drugą stronę, od zdekodowanych bajtów próbek z powrotem do wartości składowych

Potok tint transform dla Separation w HotPDF, w którym wartość krycia z operatora scn przechodzi przez tablicę przestrzeni Separation, a HPDFEvalTintTransform oblicza funkcje typów 0, 2, 3 i 4 na składowe CMYK przestrzeni alternatywnej przekształcane na sRGB
Jedna wartość krycia z operatora scn wchodzi do przestrzeni Separation, a silnik tint transform zwraca pełną krotkę składowych przestrzeni alternatywnej, która potem przekształca się raz na potrzeby ekranu

Funkcje kalkulatora PostScript typu 4 broniły się najdłużej. Do wersji v2.375.0 degradowały się łagodnie do neutralnego zastępnika; od v2.375.0 HPDFEvalPostScriptCalculator wykonuje pełny zestaw operatorów z ISO 32000-1 §7.10.5 na ograniczonym stosie operandów: operatory arytmetyczne, porównania, logiczne i bitowe, manipulację stosem wraz z roll oraz warunki if/ifelse. Semantyka przypadków brzegowych jest ostrzejsza, niż wygląda. PostScriptowe round zaokrągla połówki w górę, więc nie da się użyć delphiowego Round z zaokrąglaniem bankowym; operatory trygonometryczne działają w stopniach, a atan zwraca wartości z przedziału [0, 360); exp zaś jest dwuargumentowym potęgowaniem, a nie eksponentą naturalną. Ten sam ewaluator napędza również wypełnienia gradientowe oparte na funkcjach, i dlatego renderowanie cieniowań osiowych i promienistych dostało rampy sterowane kalkulatorem w tym samym wydaniu

// Oblicz tint transform dla Separation/DeviceN (typ 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
  const Inputs: THPDFColorComps; InputCount: Integer;
  out AltComps: THPDFColorComps): Boolean;

// Kalkulator PostScript typu 4, zestaw operatorów ISO 32000-1 7.10.5.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
  const Inputs: THPDFColorComps; InputCount: Integer;
  var Outputs: THPDFColorComps; OutCount: Integer): Boolean;

Uczciwie o precyzji: kalkulator programowy liczony w podwójnej precyzji nie zgodzi się z RIP bit w bit, a ograniczanie na granicy Range może różnić się o najmniej znaczący bit od innej implementacji. Dla wyświetlania na ekranie i renderowania regresyjnego to bez znaczenia; jeśli budujesz proofing z zarządzaniem barwą, tint transform jest dopiero pierwszym etapem i dalej potrzebujesz prawdziwego CMM w dalszej części łańcucha

CalGray, CalRGB, Lab i ICCBased bez silnika ICC

Rodziny oparte na CIE idą drugą gałęzią tego samego resolwera. HotPDF przekształca wartości Lab standardowym łańcuchem Lab na XYZ na sRGB, wraz z sześcianem punktu przełamania 6/29 w odwrotnej funkcji przenoszenia, i obsługuje CalRGB z jego gammą na kanał plus macierzą liniową 3x3 oraz CalGray z jego pojedynczą gammą. Szczegółem istotnym dla wydajności jest obsługa punktu bieli: macierz konwersji z XYZ na sRGB jest adaptowana metodą Bradforda do punktu bieli zadeklarowanego w przestrzeni barw i buforowana w rozwiązanym rekordzie THPDFColorSpace, więc praca na piksel pozostaje jednym mnożeniem 3x3, niezależnie od tego, jak egzotyczny jest zadeklarowany iluminant

Przestrzenie ICCBased dostają celowo pragmatyczne potraktowanie. Specyfikacja PDF wymaga, by każdy strumień ICCBased deklarował przestrzeń /Alternate albo domyślną wynikającą z liczby składowych /N, właśnie po to, by przeglądarki bez silnika zarządzania barwą wciąż renderowały sensownie. HotPDF rozwiązuje ICCBased przez tę przestrzeń alternatywną albo zgaduje DeviceGray, DeviceRGB czy DeviceCMYK na podstawie /N równego 1, 3 lub 4, gdy wpisu brakuje, i nigdy nie parsuje bajtów profilu. Oznacza to brak zależności od lcms i brak kosztu wyszukiwania profilu, w zamian za dokładność kolorymetryczną: przestrzeń ICCBased, której profil mocno odbiega od jej alternatywy, wyświetli się według alternatywy. Dla oglądania na ekranie i miniatur to ta sama wymiana, na którą godzi się każda lekka przeglądarka, i jest to granica, którą warto jasno postawić we własnej dokumentacji

Błąd, przez który każde wyszukiwanie nazwanej przestrzeni barw chybiało

Instalacja operatorów z v2.333.0 wyszła z defektem, który pozostał niewidoczny przez czterdzieści dwa wydania: obsługa cs i CS szukała swojego operandu z zachowanym wiodącym ukośnikiem (/CS0) wśród kluczy słownika zasobów przechowywanych bez ukośnika (CS0). Każde wyszukiwanie nazwanej przestrzeni barw chybiało, w stu procentach przypadków, a kod po cichu schodził do domyślnej DeviceGray. Widoczny objaw był subtelny w najgorszy możliwy sposób: wypełnienie Separation przez 1 scn stawało się DeviceGray 1.0, co maluje biel, a biała farba na białej stronie nie jest błędem renderowania, który ktokolwiek zrzuca na ekranie. Poprawką w v2.375.0 był wspólny pomocnik normalizacji nazw stosowany przy każdym wyszukiwaniu zasobu po operandzie

Z tego samego śledztwa wypadły dwa bliźniacze defekty. Po pierwsze, odwołania pośrednie do obiektów będących tablicami wracały nierozwiązane: dostęp do dokumentu w rendererze miał typowane resolwery wyłącznie dla strumieni i słowników, więc /CS0 5 0 R wskazujące samodzielną tablicę [/Separation ...] wracało jako surowe odwołanie, a przestrzeń parsowała się jako nieobsługiwana. Po drugie, HPDFReadNumericArray wymuszało ścisłą semantykę długości, wymagając, by tablica PDF była co najmniej tak długa jak podany bufor. Odczyt /C0 i /C1 funkcji typu 2 do czteroelementowego bufora zawodził zatem dla alternatyw o jednej i trzech składowych, zostawiając obie tablice wyzerowane, i każde niebędące CMYK wykładnicze krycie renderowało się na czarno, dopóki v2.376.0 nie wprowadziło wyrozumiałego czytnika HPDFReadNumericArrayUpTo. Lekcja do przeniesienia dalej: każdy klucz PDF udokumentowany jako mieszczący tablicę liczbową o zmiennej długości trzeba czytać czytnikiem wypełniającym tyle, ile istnieje, ponieważ bufor stałego rozmiaru ze ścisłym dopasowaniem zamienia prawidłowe pliki w ciche zera

Jak testować renderowanie kolorów dodatkowych, nie oszukując samego siebie?

Niewygodne pytanie brzmi, dlaczego zestaw testów przez cały ten czas świecił na zielono. Pierwotna regresja, SeparationRendersDistinguishable, sprawdzała wyłącznie, czy wyrenderowana bitmapa nie jest całkowicie czarna. Potok, który zrzucał każdy kolor dodatkowy do DeviceGray, produkował wyjście szare i białe, a to nie jest czarne, więc asercja przechodziła, podczas gdy cała funkcja była martwa. Słabe asercje w postaci „nie puste”, „nie całe czarne” albo „skrót jest niezerowy” nie odróżniają działającego renderera od zepsutego, ponieważ niemal każdy tryb awarii i tak produkuje jakieś piksele

Styl asercji, który faktycznie wyłapuje takie awarie, blokuje oczekiwany kolor: wyrenderuj ręcznie zbudowany minimalny plik PDF, którego farba Separation rozwiązuje się do znanego odcienia, a potem policz piksele dominujące w tym odcieniu. Renderowanie do bitmapy na potrzeby inspekcji korzysta z tego samego punktu wejścia RenderLoadedPageToBitmap, opisanego w przewodniku po renderowaniu strony do bitmapy

HotPDF: słabe kontra mocne asercje renderowania kolorów dodatkowych, gdzie predykat nie-całe-czarne przechodzi na szarym zepsutym wyjściu, a liczenie pikseli z dominującą czerwienią z progiem RedHits powyżej 500 wyłapuje martwy potok
Predykat nie-całe-czarne przechodzi na szarym wyjściu, podczas gdy cały potok kolorów dodatkowych jest martwy, a to liczenie pikseli o oczekiwanym odcieniu wypycha defekt na światło dzienne
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;
        // Zablokuj oczekiwaną farbę: żądaj realnego obszaru pikseli
        // z dominującą czerwienią, nigdy nie poprzestawaj na "nie całe czarne".
        Assert(RedHits > 500);
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Materiał testowy znaczy tyle samo co asercja. Ręcznie zbudowany plik PDF o długości kilkuset bajtów, z jednym wypełnieniem Separation i niczym więcej, nie pozostawia wątpliwości co do oczekiwanego wyjścia; plik z prawdziwego świata ćwiczy więcej kodu, ale nie powie ci, który etap zawiódł. Traktujemy dziś liczenie pikseli o oczekiwanym kolorze jako minimalną poprzeczkę dla każdego testu dymnego renderowania, ponieważ jest to jedyny styl asercji, który wypchnął te defekty na światło dzienne

Renderowanie kolorów dodatkowych i przestrzeni barw CIE jest częścią potoku wczytanego dokumentu w komponencie HotPDF dla Delphi oraz C++Builder, obok silnika funkcji, renderowania cieniowań i pokazanych wyżej ścieżek eksportu do bitmapy