Tehnički članak

PDF funkcije Type 2/3/4 u Delphiju: eksponencijalne i PostScript

HotPDF, nativna VCL PDF komponenta za Delphi i C++Builder, izračunava tri PDF tipa funkcija zasnovana na formulama umesto na uzorkovanim mrežama: eksponencijalnu interpolaciju Type 2, povezivanje Type 3 i PostScript kalkulatorske funkcije Type 4, koje odgovaraju odeljcima ISO 32000-1 §7.10.3, §7.10.4 i §7.10.5. Type 2 interpolira između dva izlazna vektora duž krive, Type 3 povezuje više podfunkcija kroz jedan ulazni domen, a Type 4 izvršava ograničeni PostScript program koji iz ulaza može da grana, upoređuje i izračunava gotovo sve što je toku sadržaja potrebno. Ako se bilo koji od ta tri tipa i malo pogrešno obradi, greška se nikada ne predstavi kao očigledan kvar — pojavi se kao gradijent sa mrtvom ravnom zonom, spot boja koja se iscrtava potpuno crno ili kalkulatorska funkcija koja na ulazima koje testni skup nije isprobao odstupa tačno za jedan

Ova tri tipa stoje uz četvrti, Type 0, koji umesto formule čuva uzorkovanu mrežu i obrađen je zasebno u pratećem članku o tabelama za pretraživanje boja Type 0. Obe porodice rešavaju isti problem, preslikavaju ulaz u izlaz, ali Type 0 je podatak izračunat jednom i ugrađen u datoteku, dok su Type 2, 3 i 4 kôd koji čitač izvršava pri svakom pozivu. Sva četiri tipa dele jednu tačku za izbor u HotPDF iscrtavaču, zasnovanu na stavci /FunctionType u rečniku funkcije, pa senčenje, transformacija nijanse ili spot funkcija polutona ne moraju da znaju koji su od četiri tipa dobili pre nego što zatraže boju

Kako funkcioniše PDF eksponencijalna funkcija Type 2?

PDF funkcija Type 2 izračunava jednu formulu — y = C0 + x^N × (C1 − C0), primenjenu komponentu po komponentu — pri čemu je x jedini ulaz funkcije, normalizovan prema njenom /Domain pre primene formule (ISO 32000-1 §7.10.3). /C0 i /C1 su izlazni vektori na dva kraja tog opsega, sa jednim brojem po izlaznoj komponenti, dok je /N eksponent koji oblikuje krivu između njih: N = 1 daje pravolinijski prelaz iza većine gradijentnih zaustavnih tačaka i duotonskih konverzija, N veće od 1 privlači krivu ka C0, a N između 0 i 1 gura je ka C1. RegisterExponentialFunction gradi taj rečnik iz pet argumenata i vraća objekat funkcije spreman za povezivanje sa senčenjem, spot funkcijom polutona ili bilo kojim drugim mestom gde specifikacija prihvata ključ /Function

Odnos broja komponenti u C0 i C1 važan je dvaput: kada pravite funkciju Type 2 i svaki put kada HotPDF mora da iscrta funkciju koju nije samo napravio. Na strani za izradu, RegisterExponentialFunction proverava C0 i C1 u odnosu jedan prema drugom i podiže izuzetak ako se ne slažu, pa poziv koji stigne do BeginDoc već ima međusobno usklađen objekat funkcije. Na strani iscrtavanja, međutim, evaluator mora da veruje nizovima /C0 i /C1 koje izvorna datoteka zaista deklariše — na primer datoteci iz štamparije otvorenoj za pregled ili potpisanom dokumentu prikazanom korisniku — a verzije pre 2.376.0 učitavale su te nizove u bafer dimenzionisan za četiri komponente, slučaj CMYK. Eksponencijalna nijansa DeviceGray ili DeviceRGB sa jednom odnosno tri stavke u /C0 i /C1 tiho je padala na tom čitanju i ostavljala oba niza na nuli, pa je nijansa bila potpuno crna umesto predviđene boje. Verzija 2.376.0 promenila je čitač tako da koristi stvarni deklarisani broj izlaznih komponenti funkcije umesto fiksnog bafera — upravo ona vrsta greške koju otkrije samo testni slučaj koji nije CMYK, jer je postojeći skup testova sve vreme koristio CMYK, gde se četiri stavke uvek uklapaju u četiri mesta

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;

Type 3 povezivanje: ulančavanje podfunkcija kroz niz Bounds

PDF funkcija Type 3 povezuje k podfunkcija u jedno komadno preslikavanje preko /Domain-a jednog ulaza, a dva niza koja to omogućavaju jesu /Bounds i /Encode (ISO 32000-1 §7.10.4). /Bounds sadrži k − 1 unutrašnjih tačaka podele koje dele /Domain na k uzastopnih intervala; evaluator bira prvi interval čija gornja granica prelazi ulaz ili poslednji interval kada ulaz dostigne konačnu granicu, a zatim prosleđuje obradu podfunkciji tog intervala. /Encode dalje preslikava ulaz iz njegovog položaja unutar intervala u opseg ulaza koji očekuje izabrana podfunkcija — obično [0, 1] ako je podfunkcija još jedan eksponencijalni segment — pre nego što se procena nastavi jedan poziv dublje, u sopstvenom /Domain-u i /Range-u te podfunkcije

HotPDF evaluator za povezivanje ranije je obrađivao tačno dve podfunkcije, a njegov čitač /Bounds zahtevao je ceo niz od osam elemenata, pa pojedinačna tačka podele koja je zaista potrebna gradijentu sa dva segmenta — jedan broj u /Bounds — nikada nije uspevala da se raščlani i funkcija nije vraćala ništa. /Encode se uopšte nije primenjivao. Verzija 2.376.0 prepisala je izbor u opštu pretragu k podfunkcija opisanu specifikacijom i počela da čita /Bounds prema njegovoj stvarnoj deklarisanoj dužini, pa gradijent sa tri, četiri ili pet zaustavnih tačaka, sastavljen od isto toliko eksponencijalnih segmenata, sada dobija isti rezultat kao gradijent sa dva segmenta za koji se to oduvek očekivalo. Primer u nastavku pravi prelaz sa dva segmenta, od crne preko crvene do bele, kakav aksijalno ili radijalno senčenje koristi kada jedna eksponencijalna kriva ne može da obuhvati sve zaustavne tačke boje koje dizajn zahteva

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;

Šta funkcija Type 4 PostScript kalkulatora može, a Type 2 i Type 3 ne mogu?

PDF funkcija Type 4 izvršava pravi, iako namerno ograničeni program: PostScript kalkulator koji svoje ulaze postavlja na stek operanada, izvršava aritmetičke, uporedne, stekovne i logičke operatore, kao i uslovne konstrukcije if/ifelse, a po završetku ostavlja izlaze na steku (ISO 32000-1 §7.10.5, tabela 42). Ne postoje petlje ni čuvanje imenovanih promenljivih, već samo stek, što usklađeni program čini lakim za razumevanje — ali u okviru tog ograničenog skupa operatora Type 4 može da izrazi ono što Type 2 i Type 3 ne mogu, kao što su stvarna formula mešanja više mastila za razdvajanje DeviceN ili spot funkcija polutona sa uslovnim pragom. HotPDF evaluator, HPDFEvalPostScriptCalculator, tokenizuje program jednom — brojeve, operatore i proceduralne blokove { } — a zatim prolazi kroz stek operanada sa 100 mesta, dubinu koju zahteva ISO 32000-1 §7.10.5, uz čvrstu granicu od 50.000 izračunatih operatora kao zaštitu od patoloških ili ručno napisanih programa

Operator roll: smer se lako pogrešno odredi

roll je operator koji najčešće ispadne obrnut pri prvom pokušaju, jer se i redosled njegovih argumenata i smer rotacije razlikuju od načina na koji ih obično opisujemo. n j roll skida broj n i iznos rotacije j, zatim ciklično pomera gornjih n stavki steka za j mesta i stavke koje otpadnu na jednom kraju vraća na drugi; kanonski primer iz specifikacije jeste da a b c 3 1 roll daje c a b — gornja stavka prelazi na dno grupe, a ne u suprotnom smeru, dok se svaka druga pomera za jedno mesto da bi napravila prostor. HotPDF evaluator računa novu poziciju stavke steka i kao (i + j) mod n, što se tačno poklapa sa tim primerom, ali ta petlja od dve linije jednako se lako napiše sa obrnutom rotacijom, a zrcalno obrnut roll i dalje daje boju koja izgleda uverljivo — samo nije ona koju je autor datoteke zatražio

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;

Operator round nije Delphi funkcija Round: zaokruživanje naviše i bankarsko zaokruživanje

PostScript operator round svaki put rešava izjednačenje na .5 pomeranjem ka većem celom broju, dok ugrađena Delphi funkcija Round to ne radi: ona zaokružuje na najbliži paran broj, po konvenciji bankarskog zaokruživanja koja naizmenično bira smer za .5 kako se ponovljenim zaokruživanjem ne bi gomilala greška. Uglavnom daju isti rezultat i razlikuju se upravo na granici koja je ovde važna — Delphi funkcija Round(0.5) vraća 0, a Round(2.5) vraća 2, dok PDF specifikacija za round traži 1 i 3 za iste ulaze — pa se razlika ne vidi u površnom testiranju, a zatim se ponavlja kao dosledno odstupanje za jedan svuda gde međurezultat kalkulatorskog programa tačno padne na polovinu celog broja. ISO 32000-1 §7.10.5 tabela 42 izričito navodi da round gura razlomljeni .5 ka većem celom broju, zato HotPDF ovaj operator primenjuje kao Floor(x + 0.5) umesto poziva Delphi funkcije Round, a svaki kôd koji ponovo implementira ili ručno proverava aritmetiku programa Type 4 mora da koristi istu zamenu

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;

Validacija prilikom registracije rano otkriva neispravan kalkulatorski program

Neispravan Type 4 program lako je otkriti tokom izrade, a skupo ga je otkrivati bilo gde drugde, zato RegisterPostScriptFunction ne čuva samo izvorni tekst: jednom probno izvršava program, u sredini deklarisanog /Domain-a, pre nego što se objekat funkcije uopšte upiše u dokument. Neuravnoteženi blokovi { }, nepoznat operator, nedostatak operanada na steku ili broj izlaza koji se ne podudara sa /Range-om obaraju tu probu i odmah izazivaju izuzetak, pri čemu stek poziva pokazuje na poziv RegisterPostScriptFunction, a ne na artefakt iscrtavanja otkriven tokom QA provere datoteke koja je već objavljena. Probna tačka u sredini ne dokazuje da je program ispravan kroz ceo /Domain — uslovna grana koja greši samo blizu jedne ivice ulaznog opsega i dalje može da prođe pored jedne tačke uzorkovanja — ali uklanja celu klasu programa koji su strukturno neispravni, a ne samo pogrešni u jednom uglu

Gde gradijenti i spot boje koriste ove funkcije

Type 2, 3 i 4 se u stvarnom PDF-u retko pojavljuju samostalno; pojavljuju se svuda gde specifikacija prihvata ključ /Function, a dva najčešća korisnika jesu senčenja i transformacije nijanse spot boja. Operator sh za aksijalni ili radijalni gradijent (ISO 32000-1 §8.7.4.5) izračunava svoj /Function jednom za svaku poziciju duž ose gradijenta, što je upravo slučaj sa više zaustavnih tačaka za koji postoji povezivanje Type 3. Transformacija nijanse prostora boja Separation ili DeviceN drugo je često mesto za ova tri tipa i tu Type 4 pokazuje svoju vrednost: jedno spot mastilo obično se svodi na krivu Type 2 ili Type 0, ali DeviceN mešavina više mastila sa stvarnim ponašanjem trappinga i preklapanja često zahteva uslovnu logiku koju može da izrazi samo PostScript kalkulator, što je obrađeno u članku o iscrtavanju spot boja Separation i DeviceN. RegisterSeparationFunc je odgovarajući poziv na strani za izradu: prima naziv koloranta, alternativni prostor boja i bilo koji objekat koji vraća porodica Register*Function, a zatim povezuje tu transformaciju nijanse sa resursom prostora boja Separation koji ostatak stranice može da izabere pomoću scn/SCN

Zajedno, uzorkovane mreže Type 0 i ova tri tipa zasnovana na formulama pokrivaju svaku funkciju /Function koju PDF može da deklariše, a izbor odgovarajućeg tipa uglavnom zavisi od onoga što već imate: tabela za pretraživanje izračunata negde drugde postaje Type 0, mešavina između dve krajnje tačke postaje Type 2, više mešavina ulančanih kroz domen postaju Type 3, a sve što ima stvarnu uslovnu logiku postaje Type 4. RegisterExponentialFunction, RegisterStitchingFunction i RegisterPostScriptFunction deo su standardne HotPDF Component za Delphi i C++Builder, uz ostatak njenog ISO 32000-1 API-ja za funkcije i senčenja