Artykuł techniczny

Funkcje PDF typu 2/3/4 w Delphi: wykładnicze, PostScript

HotPDF, natywny komponent PDF VCL dla Delphi i C++Buildera, oblicza trzy typy funkcji PDF budowane ze wzorów, a nie z siatek próbek: interpolację wykładniczą typu 2, zszywanie typu 3 oraz funkcje kalkulatora PostScript typu 4, odpowiadające ISO 32000-1 §7.10.3, §7.10.4 i §7.10.5. Typ 2 miesza między dwoma wektorami wyjściowymi wzdłuż krzywej, typ 3 łączy w łańcuch kilka podfunkcji na jednej dziedzinie wejściowej, a typ 4 uruchamia ograniczony program PostScript, który potrafi rozgałęziać się, porównywać i obliczać niemal wszystko, czego strumień treści potrzebuje ze swoich wejść. Pomyl choć trochę którykolwiek z tych trzech, a błąd nigdy się nie ogłosi jako usterka — objawi się jako gradient z martwym płaskim pasmem, kolor dodatkowy renderujący się jako czysta czerń albo funkcja kalkulatora przesunięta dokładnie o jeden przy wejściach, których zestaw testów akurat nie sprawdził

Te trzy sąsiadują z czwartym, typem 0, który przechowuje próbkowaną siatkę zamiast wzoru i jest opisany osobno w towarzyszącym artykule o tablicach LUT kolorów typu 0. Obie rodziny rozwiązują ten sam problem — mapowanie wejścia na wyjście — ale typ 0 to dane obliczone raz i wypieczone w pliku, podczas gdy typy 2, 3 i 4 to kod oceniany przez czytnik przy każdym wywołaniu. Wszystkie cztery dzielą jeden punkt dyspozycji w rendererze HotPDF, kluczowany wpisem /FunctionType słownika funkcji, więc cieniowanie, transformacja odcienia albo funkcja punktu rastra nigdy nie musi wiedzieć, który z czterech typów otrzymała, zanim będzie mogła poprosić o kolor

Jak działa funkcja wykładnicza PDF typu 2?

Funkcja PDF typu 2 oblicza jeden wzór — y = C0 + x^N × (C1 − C0), stosowany składowa po składowej — gdzie x jest jedynym wejściem funkcji, znormalizowanym względem jej /Domain, zanim wzór się uruchomi (ISO 32000-1 §7.10.3). /C0 i /C1 to wektory wyjściowe na dwóch końcach tego zakresu, po jednej liczbie na składową wyjścia, a /N to wykładnik kształtujący krzywą między nimi: N = 1 daje prostą rampę liniową stojącą za większością przystanków gradientu i konwersji duotonowych, N powyżej 1 ciągnie krzywą w stronę C0, a N między 0 a 1 popycha ją w stronę C1. RegisterExponentialFunction buduje ten słownik z pięciu argumentów i zwraca obiekt funkcji gotowy do podłączenia do cieniowania, funkcji punktu rastra albo gdziekolwiek indziej, gdzie specyfikacja akceptuje klucz /Function

Zależność liczby składowych między C0 i C1 ma znaczenie dwukrotnie: raz przy tworzeniu funkcji typu 2, i drugi raz, ilekroć HotPDF musi wyrenderować taką, której sam nie utworzył. Po stronie tworzenia RegisterExponentialFunction sprawdza C0 i C1 względem siebie i zgłasza wyjątek w razie niezgodności, więc wywołanie, które dociera do BeginDoc, jest już samospójnym obiektem funkcji. Po stronie renderowania jednak ewaluator musi zaufać jakimkolwiek tablicom /C0 i /C1, które faktycznie deklaruje plik źródłowy — na przykład plik z drukarni otwarty do podglądu, albo podpisany dokument wyświetlany z powrotem użytkownikowi — a wersje przed 2.376.0 wczytywały te tablice do bufora o rozmiarze dla czterech składowych, przypadku CMYK. Odcień wykładniczy DeviceGray lub DeviceRGB, z jedno- albo trzyelementowym /C0 i /C1, zawodził przy tym odczycie po cichu i pozostawiał obie tablice wyzerowane, więc odcień malował się jako płaska czerń zamiast zamierzonego koloru. Wersja 2.376.0 zmieniła rozmiar czytnika na faktyczną zadeklarowaną liczbę wyjść funkcji zamiast stałego bufora — dokładnie ten rodzaj błędu, który ujawnia tylko przypadek testowy spoza CMYK, ponieważ istniejący zestaw testów działał wyłącznie na CMYK, gdzie cztery-w-cztery zawsze pasowało

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;

Zszywanie typu 3: łączenie podfunkcji przez tablicę Bounds

Funkcja PDF typu 3 zszywa k podfunkcji w jedno odcinkowe mapowanie na /Domain pojedynczego wejścia, a dwie tablice, które to umożliwiają, to /Bounds i /Encode (ISO 32000-1 §7.10.4). /Bounds zawiera k − 1 wewnętrznych punktów podziału, które kroją /Domain na k kolejnych przedziałów; ewaluator wybiera pierwszy przedział, którego górna granica przekracza wejście, albo ostatni przedział, gdy wejście osiąga ostatnią granicę, i przekazuje sterowanie do podfunkcji tego przedziału. /Encode następnie przemapowuje wejście z jego pozycji wewnątrz tego przedziału na dowolny zakres wejściowy, jakiego oczekuje sama wybrana podfunkcja — zwykle [0, 1], jeśli podfunkcja to kolejny odcinek wykładniczy — zanim ocena kontynuuje, o jedno wywołanie głębiej, we własnej /Domain i /Range tej podfunkcji

Ewaluator zszywania HotPDF dawniej obsługiwał wyłącznie dokładnie dwie podfunkcje, a jego czytnik /Bounds wymagał pełnej ośmioelementowej tablicy, więc pojedynczy punkt podziału, jakiego faktycznie potrzebuje dwuodcinkowy gradient — jedna liczba w /Bounds — zawsze zawodził przy parsowaniu i funkcja nie zwracała niczego. /Encode w ogóle nie było stosowane. Wersja 2.376.0 przepisała wybór jako ogólne przeszukiwanie k podfunkcji opisane w specyfikacji i zaczęła odczytywać /Bounds względem jego rzeczywistej zadeklarowanej długości, więc trzy-, cztero- albo pięcioprzystankowy gradient zszyty z tylu odcinków wykładniczych rozwiązuje się teraz tak samo, jak zawsze deklarował, że robi to dwuodcinkowy. Poniższy przykład buduje dwuodcinkową rampę czerń-przez-czerwień-do-bieli, kształt, po który sięga cieniowanie osiowe lub promieniste, ilekroć jedna krzywa wykładnicza nie może udźwignąć wszystkich przystanków kolorów wymaganych przez projekt

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;

Co potrafi funkcja kalkulatora PostScript typu 4, czego nie potrafią typy 2 i 3?

Funkcja PDF typu 4 uruchamia prawdziwy, choć celowo ograniczony, program: kalkulator PostScript, który wrzuca swoje wejścia na stos operandów, wykonuje operatory arytmetyczne, porównania, manipulacji stosem i logiczne oraz warunki if/ifelse, a po zakończeniu zostawia wyjścia na stosie (ISO 32000-1 §7.10.5, tabela 42). Nie ma konstrukcji pętli ani przechowywania w nazwanych zmiennych, jest tylko stos, co ułatwia rozumowanie o zgodnym programie — ale w ramach tego ograniczonego zestawu operatorów typ 4 potrafi wyrazić rzeczy, których nie potrafią typy 2 i 3, na przykład prawdziwy wzór mieszania wieloatramentowego dla separacji DeviceN albo funkcję punktu rastra z warunkowym progiem. Ewaluator HotPDF, HPDFEvalPostScriptCalculator, tokenizuje program raz — liczby, operatory i bloki procedur { } — a następnie przechodzi po stustopniowym stosie operandów, głębokości wymaganej przez ISO 32000-1 §7.10.5, za twardym pułapem 50 000 ocenionych operatorów jako defensywnym zabezpieczeniem przed patologicznymi lub ręcznie pisanymi programami

Operator roll: kierunek łatwo pomylić

roll to operator najbardziej narażony na odwrócenie przy pierwszej próbie, ponieważ zarówno kolejność jego argumentów, jak i kierunek rotacji są przeciwne do tego, jak opisałby je język naturalny. n j roll zdejmuje liczbę n i wielkość rotacji j, a następnie cyklicznie przesuwa górne n wpisów stosu o j pozycji, zawijając elementy wypadające z jednego końca z powrotem na drugi; kanoniczny przykład, wprost ze specyfikacji, to a b c 3 1 roll dające c a b — górny element przesuwa się na dół grupy, nie odwrotnie, a każdy inny element przesuwa się o jeden, żeby zrobić miejsce. Ewaluator HotPDF oblicza nową pozycję wpisu stosu i jako (i + j) mod n, co dokładnie zgadza się z tym przykładem, ale to dwuliniowa pętla, którą równie łatwo napisać z odwróconą rotacją, a odbita rolka nadal daje wiarygodnie wyglądający kolor — po prostu nie ten kolor, o który prosił autor pliku

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 to nie Round z Delphi: zaokrąglanie w górę kontra zaokrąglanie bankierskie

Operator round w PostScripcie za każdym razem rozstrzyga remis .5 na korzyść większej liczby całkowitej, a wbudowana funkcja Round w Delphi tak nie robi: zaokrągla do parzystej (banker's rounding), konwencję na przemian zmieniającą stronę, w którą pada remis .5, żeby powtarzane zaokrąglanie nie kumulowało obciążenia. Obie metody zgadzają się niemal wszędzie i różnią się dokładnie na granicy, która tu ma znaczenie — Round(0.5) w Delphi zwraca 0, a Round(2.5) zwraca 2, podczas gdy round ze specyfikacji PDF chce 1 i 3 dla tych samych wejść — więc niezgodność ukrywa się przy pobieżnym testowaniu, a potem odtwarza się jako spójne przesunięcie o jeden wszędzie tam, gdzie pośrednia arytmetyka programu kalkulatora ląduje dokładnie na wartości połówkowej. ISO 32000-1 §7.10.5, tabela 42, wprost mówi, że round popycha ułamek .5 w stronę większej liczby całkowitej, więc HotPDF implementuje ten operator jako Floor(x + 0.5) zamiast wywoływać Round z Delphi, i każdy kod, który reimplementuje albo ręcznie weryfikuje arytmetykę programu typu 4, potrzebuje tego samego podstawienia

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;

Walidacja w chwili rejestracji łapie wadliwy program kalkulatora wcześnie

Wadliwie sformowany program typu 4 jest tani do wyłapania w chwili tworzenia i kosztowny do wyłapania gdziekolwiek indziej, więc RegisterPostScriptFunction nie tylko przechowuje tekst źródłowy: ocenia program próbnie raz, w punkcie środkowym zadeklarowanej /Domain, zanim obiekt funkcji zostanie w ogóle zapisany do dokumentu. Niezbalansowane bloki { }, nierozpoznany operator, niedobór na stosie albo liczba wyjść niezgodna z /Range — wszystko to zawodzi przy tym próbnym przebiegu i natychmiast zgłasza wyjątek, ze stosem wywołań wskazującym na wywołanie RegisterPostScriptFunction, a nie na artefakt renderowania odkryty podczas QA na pliku, który już trafił do klienta. Próba w punkcie środkowym nie dowodzi, że program jest poprawny w całej swojej /Domain — rozgałęzienie warunkowe, które psuje się tylko blisko jednej krawędzi zakresu wejściowego, wciąż może prześlizgnąć się obok pojedynczego punktu próbki — ale zamyka całą klasę programów, które są strukturalnie zepsute, a nie tylko błędne w jednym rogu

Gdzie gradienty i kolory dodatkowe wykorzystują te funkcje w praktyce

Typy 2, 3 i 4 rzadko pojawiają się w prawdziwym PDF-ie w izolacji; występują wszędzie tam, gdzie specyfikacja akceptuje klucz /Function, a dwoma najczęstszymi odbiorcami są cieniowania i transformacje odcienia kolorów dodatkowych. Operator sh gradientu osiowego lub promienistego (ISO 32000-1 §8.7.4.5) ocenia swoją /Function raz na pozycję wzdłuż osi gradientu, co jest dokładnie przypadkiem wielu przystanków, dla którego istnieje zszywanie typu 3. Transformacja odcienia przestrzeni kolorów Separation lub DeviceN to drugi częsty dom dla tych trzech typów, i to tam typ 4 zarabia na swoje utrzymanie: pojedynczy atrament dodatkowy zwykle sprowadza się do krzywej typu 2 lub typu 0, ale mieszanka DeviceN kilku atramentów z prawdziwym trapowaniem i zachowaniem nadruku często potrzebuje logiki warunkowej, jaką potrafi wyrazić tylko kalkulator PostScript — przypadek opisany w artykule o renderowaniu kolorów dodatkowych Separation i DeviceN. RegisterSeparationFunc to wywołanie parujące po stronie tworzenia: przyjmuje nazwę koloru dodatkowego, alternatywną przestrzeń kolorów i dowolny obiekt zwracany przez rodzinę Register*Function, i podłącza tę transformację odcienia do zasobu przestrzeni kolorów Separation, który reszta strony może wybrać przez scn/SCN

Razem próbkowane siatki typu 0 i te trzy typy oparte na wzorach pokrywają każdą /Function, jaką może zadeklarować PDF, a wybór właściwego to głównie kwestia tego, co już się posiada: tablica przeglądowa obliczona gdzie indziej staje się typem 0, mieszanka dwupunktowa staje się typem 2, kilka mieszanek połączonych łańcuchem na dziedzinie staje się typem 3, a cokolwiek z prawdziwą logiką warunkową staje się typem 4. RegisterExponentialFunction, RegisterStitchingFunction i RegisterPostScriptFunction są częścią standardowego komponentu HotPDF dla Delphi i C++Buildera, obok reszty jego API funkcji i cieniowań zgodnego z ISO 32000-1