HotPDF, natívny PDF komponent VCL pre Delphi a C++Builder, vyhodnocuje tri typy funkcií PDF postavené na vzorcoch namiesto mriežok vzoriek: exponenciálnu interpoláciu typu 2, zošívanie typu 3 a kalkulačné funkcie PostScript typu 4, ktoré zodpovedajú ISO 32000-1 §7.10.3, §7.10.4 a §7.10.5. Typ 2 prelína medzi dvomi výstupnými vektormi pozdĺž krivky, typ 3 reťazí niekoľko čiastkových funkcií naprieč jednou vstupnou doménou a typ 4 spúšťa obmedzený program PostScript, ktorý dokáže vetviť, porovnávať a vypočítať takmer čokoľvek, čo obsahový prúd potrebuje zo svojich vstupov. Ak sa čo i len jeden z týchto troch typov trochu pomýli, chyba sa nikdy neohlási ako bug — prejaví sa ako prechod s mŕtvym plochým pásmom, priama farba, ktorá sa vykreslí ako čistá čierna, alebo kalkulačná funkcia, ktorá je presne o jednotku vedľa pri vstupoch, ktoré testovacia sada náhodou nevyskúšala
Tieto tri stoja vedľa štvrtého typu, typu 0, ktorý ukladá vzorkovanú mriežku namiesto vzorca a je opísaný samostatne v sprievodnom článku o vyhľadávacích tabuľkách farieb typu 0. Obe rodiny riešia rovnaký problém, mapovanie vstupu na výstup, no typ 0 sú dáta vypočítané raz a zapečené do súboru, zatiaľ čo typy 2, 3 a 4 sú kód, ktorý čítačka vyhodnocuje pri každom volaní. Všetky štyri zdieľajú jeden bod rozoslania vo vykresľovači HotPDF, kľúčovaný podľa položky /FunctionType slovníka funkcie, takže tieňovanie, prevod odtieňa alebo priama funkcia rastra nikdy nemusí vedieť, ktorý zo štyroch typov dostala, skôr než si vypýta farbu
Ako funguje exponenciálna funkcia PDF typu 2?
Funkcia PDF typu 2 počíta jeden vzorec — y = C0 + x^N × (C1 − C0), aplikovaný zložka po zložke — kde x je jediný vstup funkcie, normalizovaný voči jej /Domain ešte pred spustením vzorca (ISO 32000-1 §7.10.3). /C0 a /C1 sú výstupné vektory na dvoch koncoch tohto rozsahu, jedno číslo na výstupnú zložku, a /N je exponent, ktorý formuje krivku medzi nimi: N = 1 dáva priamu lineárnu rampu za väčšinou zastávok prechodu a konverzií duotónov, N nad 1 ťahá krivku smerom k C0, a N medzi 0 a 1 ju tlačí smerom k C1. RegisterExponentialFunction zostaví tento slovník z piatich argumentov a vráti objekt funkcie pripravený zapojiť sa do tieňovania, priamej funkcie rastra alebo kdekoľvek inde, kde špecifikácia prijíma kľúč /Function
Vzťah počtu zložiek medzi C0 a C1 je dôležitý dvakrát: raz, keď funkciu typu 2 vytvárate, a znova vždy, keď HotPDF musí vykresliť takú, ktorú nevytvoril sám. Na strane tvorby RegisterExponentialFunction porovná C0 a C1 navzájom a vyvolá výnimku, ak sa nezhodujú, takže volanie, ktoré sa dostane až k BeginDoc, je už objektom funkcie, ktorý je vnútorne konzistentný. Na strane vykresľovania však musí evaluátor dôverovať akýmkoľvek poliam /C0 a /C1, ktoré zdrojový súbor v skutočnosti deklaruje — povedzme súbor z tlačiarne otvorený na náhľad, alebo podpísaný dokument zobrazený späť používateľovi — a verzie pred 2.376.0 čítali tieto polia do bufferu s veľkosťou pre štyri zložky, prípad CMYK. Exponenciálny odtieň DeviceGray alebo DeviceRGB, s jednoprvkovým alebo trojprvkovým /C0 a /C1, pri tomto čítaní ticho zlyhal a nechal obe polia na nule, takže sa odtieň vykreslil ploché čierne namiesto zamýšľanej farby. Verzia 2.376.0 zmenila veľkosť čítača na skutočný deklarovaný počet výstupov funkcie namiesto pevného bufferu — presne ten druh chyby, ktorú odhalí iba testovací prípad mimo CMYK, keďže existujúca testovacia sada bežala výhradne v CMYK, kde štyri do štyroch vždy sadli
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;
Zošívanie typu 3: reťazenie čiastkových funkcií cez pole Bounds
Funkcia PDF typu 3 zošíva k čiastkových funkcií do jedného po častiach definovaného mapovania nad /Domain jediného vstupu, a dve polia, ktoré to umožňujú, sú /Bounds a /Encode (ISO 32000-1 §7.10.4). /Bounds drží k − 1 vnútorných deliacich bodov, ktoré rozdelia /Domain na k po sebe idúcich intervalov; evaluátor vyberie prvý interval, ktorého horná hranica prevyšuje vstup, alebo posledný interval, keď vstup dosiahne poslednú hranicu, a odovzdá riadenie čiastkovej funkcii tohto intervalu. /Encode potom prepočíta vstup z jeho pozície vnútri tohto intervalu na akýkoľvek vstupný rozsah, ktorý samotná vybraná čiastková funkcia očakáva — typicky [0, 1], ak je čiastková funkcia ešte jeden exponenciálny segment — ešte predtým, než vyhodnotenie pokračuje o jedno volanie hlbšie, do vlastnej /Domain a /Range tejto čiastkovej funkcie
Evaluátor zošívania v HotPDF kedysi zvládal iba presne dve čiastkové funkcie a jeho čítač /Bounds vyžadoval plné osemprvkové pole, takže jediný deliaci bod, ktorý dvojsegmentový prechod v skutočnosti potrebuje — jedno číslo v /Bounds — sa vždy nepodarilo naparsovať a funkcia nevrátila nič. /Encode sa vôbec neaplikoval. Verzia 2.376.0 prepísala výber na všeobecné vyhľadávanie s k čiastkovými funkciami, ako to opisuje špecifikácia, a začala čítať /Bounds podľa jej skutočnej deklarovanej dĺžky, takže trojbodový, štvorbodový alebo päťbodový prechod zošitý z takéhoto počtu exponenciálnych segmentov sa teraz vyrieši rovnako, ako to dvojsegmentový vždy tvrdil, že robí. Príklad nižšie zostavuje dvojsegmentovú rampu čierna-cez-červenú-po-bielu, tvar, po ktorom axiálne alebo radiálne tieňovanie siaha vždy, keď jedna exponenciálna krivka neunesie všetky farebné zastávky, ktoré dizajn 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;
Čo dokáže kalkulačná funkcia PostScript typu 4, čo typy 2 a 3 nedokážu?
Funkcia PDF typu 4 spúšťa skutočný, hoci zámerne obmedzený, program: kalkulačku PostScript, ktorá vloží svoje vstupy na operandový zásobník, vykoná aritmetické, porovnávacie, zásobníkové a booleovské operátory plus podmienky if/ifelse, a po skončení nechá svoje výstupy na zásobníku (ISO 32000-1 §7.10.5, tabuľka 42). Neexistuje žiadna slučková konštrukcia ani úložisko pomenovaných premenných, iba zásobník, čo drží program rešpektujúci normu ľahko pochopiteľným — no vnútri tejto obmedzenej množiny operátorov dokáže typ 4 vyjadriť veci, ktoré typy 2 a 3 nedokážu, ako skutočný vzorec miešania viacerých farieb pre separáciu DeviceN alebo priamu funkciu rastra s podmienečným prahom. Evaluátor HotPDF, HPDFEvalPostScriptCalculator, program raz tokenizuje — čísla, operátory a bloky procedúr { } — a potom prechádza 100-položkový operandový zásobník, hĺbku, akú vyžaduje ISO 32000-1 §7.10.5, za pevným stropom 50 000 vyhodnotených operátorov ako obrannou poistkou proti patologickým alebo ručne písaným programom
Operátor roll: smer sa dá ľahko pomýliť
roll je operátor, ktorý na prvý pokus s najväčšou pravdepodobnosťou vyjde obrátene, pretože jeho poradie argumentov aj smer rotácie oba idú opačne, než ako to opisuje bežná reč. n j roll vyberie zo zásobníka počet n a hodnotu rotácie j, potom cyklicky posunie vrchných n položiek zásobníka o j pozícií, pričom položky, ktoré vypadnú na jednom konci, sa zabalia späť na druhý; kanonický príklad priamo zo špecifikácie je a b c 3 1 roll produkujúce c a b — vrchná položka sa presunie na spodok skupiny, nie naopak, a každá ostatná položka sa posunie o jednu hore, aby uvoľnila miesto. Evaluátor HotPDF počíta novú pozíciu položky zásobníka i ako (i + j) mod n, čo presne zodpovedá tomuto príkladu, no ide o dvojriadkovú slučku, ktorú je rovnako ľahké napísať s obrátenou rotáciou, a zrkadlovo obrátený roll stále vyprodukuje farbu, ktorá vyzerá vierohodne — len to nie je farba, o ktorú autor súboru žiadal
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 nie je Round v Delphi: zaokrúhľovanie nahor oproti bankárskemu zaokrúhľovaniu
Operátor round v PostScripte vyrieši remízu na .5 vždy smerom k väčšiemu celému číslu, zatiaľ čo vstavaná funkcia Round v Delphi to nerobí: zaokrúhľuje na najbližšie párne, čo je konvencia bankárskeho zaokrúhľovania, ktorá strieda, ktorým smerom remíza na .5 padne, aby sa pri opakovanom zaokrúhľovaní nehromadilo skreslenie. Oba súhlasia takmer všade a nesúhlasia práve na hranici, ktorá je tu dôležitá — Round(0.5) v Delphi vráti 0 a Round(2.5) vráti 2, zatiaľ čo round podľa špecifikácie PDF chce pre tie isté vstupy 1 a 3 — takže nesúlad sa pri bežnom testovaní skryje a potom sa reprodukuje ako konzistentná chyba o jednotku kdekoľvek, kde medzivýpočet kalkulačného programu skončí presne na polovičnom celom čísle. ISO 32000-1 §7.10.5 tabuľka 42 je v tomto explicitná: round tlačí zlomok .5 smerom k väčšiemu celému číslu, takže HotPDF implementuje tento operátor ako Floor(x + 0.5) namiesto volania Round z Delphi, a akýkoľvek kód, ktorý ručne znova implementuje alebo namátkovo kontroluje aritmetiku programu typu 4, potrebuje rovnakú 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;
Validácia pri registrácii odhalí zlý kalkulačný program včas
Poškodený program typu 4 je lacné odhaliť v čase tvorby a drahé odhaliť kdekoľvek inde, takže RegisterPostScriptFunction neurobí len to, že uloží zdrojový text: raz skúšobne vyhodnotí program, v strede deklarovanej /Domain, ešte predtým, než sa objekt funkcie vôbec zapíše do dokumentu. Nevyvážené bloky { }, nerozpoznaný operátor, podtečenie zásobníka, alebo počet výstupov, ktorý sa nezhoduje s /Range, to všetko spôsobí zlyhanie tohto skúšobného behu a okamžite vyvolá výnimku, so zásobníkom volaní ukazujúcim na volanie RegisterPostScriptFunction namiesto na artefakt vykresľovania objavený počas QA na súbore, ktorý sa už dostal von. Skúška v strednom bode nedokazuje, že je program správny cez celú svoju /Domain — podmienená vetva, ktorá sa nesprávne správa iba blízko jedného okraja vstupného rozsahu, môže stále prekĺznuť popri jedinom vzorkovacom bode — no uzatvára celú triedu programov, ktoré sú štrukturálne pokazené, nie len chybné v jednom kúte
Kde tieto funkcie prichádzajú k slovu pri prechodoch a priamych farbách
Typy 2, 3 a 4 sa v skutočnom PDF málokedy objavujú izolovane; objavujú sa všade, kde špecifikácia prijíma kľúč /Function, a dvaja najčastejší konzumenti sú tieňovania a prevody odtieňa priamej farby. Operátor sh axiálneho alebo radiálneho prechodu (ISO 32000-1 §8.7.4.5) vyhodnocuje svoju /Function raz za pozíciu pozdĺž osi prechodu, čo je presne prípad viacerých zastávok, pre ktorý existuje zošívanie typu 3. Prevod odtieňa farebného priestoru Separation alebo DeviceN je ďalším častým domovom týchto troch typov, a práve tam si typ 4 zaslúži svoje miesto: jediná priama farba sa zvyčajne zredukuje na krivku typu 2 alebo typu 0, no zmes DeviceN z niekoľkých farieb so skutočným trappingom a správaním pretlače často potrebuje podmienenú logiku, akú dokáže vyjadriť iba kalkulačka PostScript, prípad opísaný v článku o vykresľovaní priamych farieb Separation a DeviceN. RegisterSeparationFunc je párové volanie na strane tvorby: prijíma názov farby, alternatívny farebný priestor a akýkoľvek objekt, ktorý vráti rodina Register*Function, a zapojí tento prevod odtieňa do zdroja farebného priestoru Separation, ktorý zvyšok stránky môže vybrať pomocou scn/SCN
Spolu vzorkované mriežky typu 0 a tieto tri typy poháňané vzorcami pokrývajú každú /Function, akú PDF dokáže deklarovať, a výber toho správneho je väčšinou otázkou toho, čo už máte: vyhľadávacia tabuľka vypočítaná inde sa stáva typom 0, prelínanie medzi dvomi koncovými bodmi sa stáva typom 2, niekoľko prelínaní zreťazených cez doménu sa stáva typom 3 a čokoľvek so skutočnou podmienenou logikou sa stáva typom 4. RegisterExponentialFunction, RegisterStitchingFunction a RegisterPostScriptFunction sú súčasťou štandardného komponentu HotPDF pre Delphi a C++Builder, spolu so zvyškom jeho API pre funkcie a tieňovania podľa ISO 32000-1