Technický článek

Funkce PDF typu 2/3/4 v Delphi: exponenciální, sešívané, PostScript

HotPDF, nativní komponenta PDF pro VCL pro Delphi a C++Builder, vyhodnocuje tři typy funkcí PDF postavené na vzorcích, ne na mřížkách vzorků: exponenciální interpolaci typu 2, sešívání typu 3 a kalkulátorové funkce PostScript typu 4, odpovídající ISO 32000-1 §7.10.3, §7.10.4 a §7.10.5. Typ 2 mísí mezi dvěma výstupními vektory podél křivky, typ 3 řetězí několik dílčích funkcí přes jednu vstupní doménu a typ 4 spouští omezený program PostScript, který umí větvit, porovnávat a spočítat téměř cokoli, co proud obsahu potřebuje ze svých vstupů. Když se u kteréhokoli z těchto tří něco pokazí jen trochu, selhání se nikdy neohlásí jako chyba — projeví se jako gradient s mrtvým plochým pásem, přímá barva, která se vykreslí jako čistě černá, nebo kalkulátorová funkce, která je přesně o jedna vedle na vstupech, které testovací sada náhodou nezkusila

Tyto tři typy sedí vedle čtvrtého, typu 0, který ukládá vzorkovanou mřížku místo vzorce a je popsán samostatně v doprovodném článku o vyhledávacích tabulkách barev typu 0. Obě rodiny řeší stejný problém, mapování vstupu na výstup, ale typ 0 jsou data spočítaná jednou a zapečená do souboru, zatímco typ 2, 3 a 4 je kód, který čtenář vyhodnocuje při každém volání. Všechny čtyři sdílejí jeden bod dispatche ve vykreslovači HotPDF, klíčovaný podle položky /FunctionType slovníku funkce, takže stínování, převod odstínu nebo funkce polotónové přímé barvy nikdy nemusí vědět, který ze čtyř typů dostaly, dřív než mohou požádat o barvu

Jak funguje exponenciální funkce PDF typu 2?

Funkce PDF typu 2 počítá jeden vzorec — y = C0 + x^N × (C1 − C0), aplikovaný po jednotlivých složkách — kde x je jediný vstup funkce, normalizovaný podle své /Domain ještě předtím, než se vzorec vyhodnotí (ISO 32000-1 §7.10.3). /C0 a /C1 jsou výstupní vektory na obou koncích tohoto rozsahu, jedno číslo na výstupní složku, a /N je exponent, který tvaruje křivku mezi nimi: N = 1 dává rovný lineární přechod za většinou zarážek gradientu a duotónových převodů, N nad 1 táhne křivku směrem k C0 a N mezi 0 a 1 ji tlačí směrem k C1. RegisterExponentialFunction sestaví tento slovník z pěti argumentů a vrátí objekt funkce připravený zapojit do stínování, funkce polotónové přímé barvy nebo kamkoli jinam, kde specifikace přijímá klíč /Function

Vztah počtu složek mezi C0 a C1 je důležitý dvakrát: jednou, když funkci typu 2 vytváříte, a znovu, kdykoli musí HotPDF vykreslit takovou, kterou nevytvořil sám. Na straně vytváření RegisterExponentialFunction porovná C0 a C1 navzájem a vyvolá výjimku, pokud si neodpovídají, takže volání, které se dostane až k BeginDoc, má už samo o sobě konzistentní objekt funkce. Na straně vykreslování ale evaluátor musí věřit tomu, jaká pole /C0 a /C1 zdrojový soubor skutečně deklaruje — třeba soubor z tiskárny otevřený pro náhled, nebo podepsaný dokument zobrazený zpět uživateli — a verze před 2.376.0 četly tato pole do bufferu o velikosti pro čtyři složky, případ CMYK. Exponenciální odstín DeviceGray nebo DeviceRGB, s jednoprvkovým nebo tříprvkovým /C0 a /C1, toto čtení tiše selhal a ponechal obě pole na nule, takže se odstín vykreslil jako plochá černá místo zamýšlené barvy. Verze 2.376.0 změnila velikost čtečky na skutečný deklarovaný počet výstupů funkce místo pevného bufferu — přesně ten druh chyby, který odhalí jen testovací případ mimo CMYK, protože stávající sada testů běžela celá v CMYK, kde se čtyři do čtyř vždy vešly

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;

Sešívání typu 3: řetězení dílčích funkcí přes pole Bounds

Funkce PDF typu 3 sešívá k dílčích funkcí do jednoho mapování po částech přes /Domain jediného vstupu, a dvě pole, díky kterým to funguje, jsou /Bounds a /Encode (ISO 32000-1 §7.10.4). /Bounds drží k − 1 vnitřních dělicích bodů, které rozřežou /Domain na k po sobě jdoucích intervalů; evaluátor vybere první interval, jehož horní mez převyšuje vstup, nebo poslední interval, jakmile vstup dosáhne poslední meze, a předá řízení dílčí funkci tohoto intervalu. /Encode pak přemapuje vstup z jeho pozice uvnitř tohoto intervalu na jakýkoli vstupní rozsah, který vybraná dílčí funkce sama očekává — typicky [0, 1], pokud je dílčí funkcí další exponenciální segment — dřív, než vyhodnocení pokračuje o volání hlouběji, do vlastní /Domain a /Range této dílčí funkce

Evaluátor sešívání HotPDF dřív zvládal jen přesně dvě dílčí funkce a jeho čtečka /Bounds vyžadovala plné osmiprvkové pole, takže jediný dělicí bod, který dvousegmentový gradient skutečně potřebuje — jedno číslo v /Bounds — se vždy nepodařilo naparsovat a funkce nevrátila nic. /Encode se vůbec neaplikovalo. Verze 2.376.0 přepsala výběr na obecné hledání mezi k dílčími funkcemi, jak jej popisuje specifikace, a začala číst /Bounds podle jeho skutečné deklarované délky, takže tří-, čtyř- nebo pětibodový gradient sešitý z odpovídajícího počtu exponenciálních segmentů se teď vyřeší stejně, jako to dvousegmentový vždy tvrdil. Příklad níže sestaví dvousegmentový přechod černá-přes-červenou-do-bílé, tvar, po kterém axiální nebo radiální stínování sáhne pokaždé, kdy jedna exponenciální křivka neunese všechny barevné zarážky, které design vyžaduje

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 dokáže kalkulátorová funkce PostScript typu 4, co typ 2 a 3 nezvládnou?

Funkce PDF typu 4 spouští skutečný, byť záměrně omezený program: kalkulátor PostScript, který vloží své vstupy na zásobník operandů, vykoná aritmetické, porovnávací operátory, operátory pro manipulaci se zásobníkem a booleovské operátory plus podmínky if/ifelse, a po dokončení nechá své výstupy na zásobníku (ISO 32000-1 §7.10.5, tabulka 42). Neexistuje žádná konstrukce smyčky ani úložiště pojmenovaných proměnných, jen zásobník, což udržuje konformní program snadno pochopitelným — ale v rámci této omezené sady operátorů dokáže typ 4 vyjádřit věci, které typ 2 a typ 3 nedokážou, jako skutečný vzorec míchání více inkoustů pro separaci DeviceN nebo polotónovou přímou funkci s podmíněným prahem. Evaluátor HotPDF, HPDFEvalPostScriptCalculator, program jednou tokenizuje — čísla, operátory a bloky procedur { } — a pak prochází zásobník operandů se 100 položkami, hloubka, kterou požaduje ISO 32000-1 §7.10.5, za pevným stropem 50 000 vyhodnocených operátorů jako obranným zádrhelem proti patologickým nebo ručně psaným programům

Operátor roll: směr se snadno spletě obráceně

roll je operátor, u kterého je při prvním pokusu nejsnadnější dostat opačný výsledek, protože jak pořadí jeho argumentů, tak směr jeho rotace běží opačně, než jak by je popsala čeština nebo angličtina. n j roll vyjme počet n a míru rotace j, pak cyklicky posune horních n položek zásobníku o j pozic, přičemž položky vypadávající na jednom konci obtočí zpět na druhý; kanonický příklad přímo ze specifikace je a b c 3 1 roll, které vyprodukuje c a b — horní položka se přesune na spodek skupiny, ne naopak, a každá další položka se posune o jednu nahoru, aby se uvolnilo místo. Evaluátor HotPDF počítá novou pozici položky zásobníku i jako (i + j) mod n, což přesně odpovídá tomuto příkladu, ale je to dvouřádková smyčka, kterou je stejně snadné napsat s obrácenou rotací, a zrcadlově obrácený roll pořád vyprodukuje barvu, která vypadá věrohodně — jen to není barva, o kterou autor souboru žádal

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 není Delphi Round: zaokrouhlování nahoru versus bankéřské zaokrouhlování

Operátor round v PostScriptu vždy rozhodne remízu na .5 směrem k většímu celému číslu, zatímco vestavěná funkce Delphi Round to tak nedělá: zaokrouhluje half-to-even, konvenci bankéřského zaokrouhlování, která střídá, kterým směrem remíza na .5 padne, aby se opakovaným zaokrouhlováním nehromadila systematická odchylka. Obě metody se shodují téměř všude a liší se přesně na hranici, na které tu záleží — Delphi Round(0.5) vrátí 0 a Round(2.5) vrátí 2, zatímco round podle specifikace PDF chce pro stejné vstupy 1 a 3 — takže se nesoulad při běžném testování schová a pak se projeví jako konzistentní chyba o jedna kdekoli, kde mezivýpočet kalkulátorového programu skončí přesně na polovině celého čísla. ISO 32000-1 §7.10.5 tabulka 42 je v tomto explicitní: round tlačí zlomek .5 směrem k většímu celému číslu, takže HotPDF implementuje tento operátor jako Floor(x + 0.5) místo volání Delphi Round, a jakýkoli kód, který přeimplementuje nebo ručně kontroluje aritmetiku programu typu 4, potřebuje stejnou náhradu

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;

Validace při registraci odchytí špatný kalkulátorový program včas

Poškozený program typu 4 je levné odchytit ve chvíli vytváření a drahé odchytit kdekoli jinde, takže RegisterPostScriptFunction nejen ukládá zdrojový text: jednou zkusmo vyhodnotí program ve středovém bodě deklarované /Domain, ještě předtím, než se objekt funkce vůbec zapíše do dokumentu. Nevyvážené bloky { }, nerozpoznaný operátor, podtečení zásobníku nebo počet výstupů, který neodpovídá /Range, to všechno tento zkušební běh nechá selhat a okamžitě vyvolá výjimku, přičemž zásobník volání ukazuje na volání RegisterPostScriptFunction místo na artefakt vykreslení objevený při QA na souboru, který už byl odeslán. Zkušební běh ve středovém bodě nedokazuje, že je program správný přes celou svou /Domain — podmíněná větev, která se chová špatně jen blízko jednoho okraje vstupního rozsahu, může jediným vzorkovým bodem přesto proklouznout — ale uzavírá celou třídu programů, které jsou strukturálně rozbité, ne jen chybné v jednom rohu

Kde tyto funkce nasazují do práce gradienty a přímé barvy

Typ 2, 3 a 4 se ve skutečném PDF málokdy objevují izolovaně; objevují se všude, kde specifikace přijímá klíč /Function, a dva nejběžnější konzumenti jsou stínování a převody odstínu přímých barev. Operátor sh axiálního nebo radiálního gradientu (ISO 32000-1 §8.7.4.5) vyhodnocuje svou /Function jednou na pozici podél osy gradientu, což je přesně vícebodový případ, pro který existuje sešívání typu 3. Převod odstínu barevného prostoru Separation nebo DeviceN je druhým častým domovem těchto tří typů, a je to místo, kde si typ 4 vydělá na své místo: jeden přímý inkoust se obvykle zredukuje na křivku typu 2 nebo typu 0, ale mísení DeviceN několika inkoustů se skutečným chováním trappingu a přetisku často potřebuje podmíněnou logiku, kterou dokáže vyjádřit jen kalkulátor PostScript, případ popsaný v článku o vykreslování přímých barev Separation a DeviceN. RegisterSeparationFunc je párovací volání na straně vytváření: bere jméno kolorantu, alternativní barevný prostor a jakýkoli objekt, který vrátí rodina Register*Function, a zapojí tento převod odstínu do zdroje barevného prostoru Separation, který může zbytek stránky vybrat přes scn/SCN

Vzorkované mřížky typu 0 a tyto tři typy poháněné vzorcem společně pokrývají každou /Function, kterou PDF dokáže deklarovat, a výběr toho správného je hlavně otázka toho, co už máte: vyhledávací tabulka spočítaná jinde se stane typem 0, přechod mezi dvěma koncovými body se stane typem 2, několik přechodů zřetězených přes doménu se stane typem 3 a cokoli se skutečnou podmíněnou logikou se stane typem 4. RegisterExponentialFunction, RegisterStitchingFunction a RegisterPostScriptFunction jsou součástí standardní komponenty HotPDF pro Delphi a C++Builder, spolu se zbytkem jejího API pro funkce a stínování podle ISO 32000-1