Technisch artikel

PDF Type 2/3/4-functies in Delphi: exponentieel, PostScript

HotPDF, de native VCL-PDF-component voor Delphi en C++Builder, evalueert de drie PDF-functietypen die uit formules zijn opgebouwd in plaats van uit steekproefroosters: Type 2 exponentiële interpolatie, Type 3 stiksel (stitching), en Type 4 PostScript-rekenmachinefuncties, overeenkomend met ISO 32000-1 §7.10.3, §7.10.4, en §7.10.5. Type 2 mengt tussen twee uitvoervectoren langs een curve, Type 3 schakelt meerdere subfuncties in serie over één invoerdomein, en Type 4 voert een beperkt PostScript-programma uit dat kan vertakken, vergelijken, en vrijwel alles berekenen wat een inhoudsstroom uit zijn invoer nodig heeft. Krijg een van de drie een klein beetje verkeerd en de fout kondigt zich nooit aan als bug — het verschijnt als een verloop met een dood vlakke band, een steunkleur die zuiver zwart rendert, of een rekenmachinefunctie die precies één afwijkt bij de invoer die een testsuite toevallig niet heeft geprobeerd

Deze drie staan naast een vierde, Type 0, die een bemonsterd raster opslaat in plaats van een formule en apart wordt behandeld in het bijbehorende artikel over Type 0-kleuropzoektabellen. De twee families lossen hetzelfde probleem op, een invoer naar een uitvoer mappen, maar Type 0 is data die eenmalig berekend en in het bestand gebakken wordt, terwijl Type 2, 3, en 4 code zijn die de lezer bij elke aanroep evalueert. Alle vier delen één verzendpunt in HotPDF's renderer, gesleuteld op de /FunctionType-vermelding van het functiedictionary, zodat een shading, een tintomzetting, of een halftoon-steunkleurfunctie nooit hoeft te weten welke van de vier het heeft ontvangen voordat het om een kleur kan vragen

Hoe werkt een PDF Type 2 exponentiële functie?

Een PDF Type 2-functie berekent één formule — y = C0 + x^N × (C1 − C0), toegepast component voor component — waarbij x de enkele invoer van de functie is, genormaliseerd tegen zijn /Domain voordat de formule wordt uitgevoerd (ISO 32000-1 §7.10.3). /C0 en /C1 zijn de uitvoervectoren aan de twee uiteinden van dat bereik, één getal per uitvoercomponent, en /N is de exponent die de curve ertussen vormgeeft: N = 1 geeft de rechte lineaire helling achter de meeste verloopstops en duotoonomzettingen, N boven 1 trekt de curve richting C0, en N tussen 0 en 1 duwt hem richting C1. RegisterExponentialFunction bouwt dat dictionary op uit vijf argumenten en geeft een functieobject terug dat klaar is om in te pluggen in een shading, een halftoon-steunkleurfunctie, of ergens anders waar de spec een /Function-sleutel accepteert

De componentaantalrelatie tussen C0 en C1 doet er tweemaal toe: eenmaal wanneer u een Type 2-functie opstelt, en opnieuw wanneer HotPDF er een moet renderen die het niet zelf heeft gemaakt. Aan de opstelkant controleert RegisterExponentialFunction C0 en C1 tegen elkaar en werpt een uitzondering op als ze het oneens zijn, dus een aanroep die BeginDoc bereikt, is al een zelfconsistent functieobject. Aan de renderkant echter moet de evaluator vertrouwen op welke /C0- en /C1-arrays een bronbestand ook daadwerkelijk declareert — een drukwerkbestand geopend voor voorbeeld, bijvoorbeeld, of een ondertekend document dat aan een gebruiker wordt teruggetoond — en versies vóór 2.376.0 lazen die arrays in een buffer die voor vier componenten was gedimensioneerd, het CMYK-geval. Een DeviceGray- of DeviceRGB-exponentiële tint, met een /C0 en /C1 van één of drie elementen, faalde die lezing stilzwijgend en liet beide arrays op nul staan, waardoor de tint vlak zwart schilderde in plaats van de bedoelde kleur. Versie 2.376.0 dimensioneerde de lezer opnieuw op het daadwerkelijke gedeclareerde uitvoeraantal van de functie in plaats van een vaste buffer — precies het soort bug dat alleen een niet-CMYK-testgeval blootlegt, aangezien de bestaande suite overal CMYK draaide, waar vier-in-vier altijd paste

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 stiksel: subfuncties in serie schakelen over een Bounds-array

Een PDF Type 3-functie stikt k subfuncties aaneen tot één stuksgewijze mapping over het /Domain van één invoer, en de twee arrays die dat mogelijk maken zijn /Bounds en /Encode (ISO 32000-1 §7.10.4). /Bounds bevat k − 1 interne splitspunten die /Domain in k opeenvolgende intervallen verdelen; de evaluator kiest het eerste interval waarvan de bovengrens de invoer overschrijdt, of het laatste interval zodra de invoer de laatste grens bereikt, en draagt over aan de subfunctie van dat interval. /Encode mapt de invoer vervolgens opnieuw vanuit zijn positie binnen dat interval naar welk invoerbereik de gekozen subfunctie zelf ook verwacht — doorgaans [0, 1] als de subfunctie nog een exponentieel segment is — voordat de evaluatie doorgaat, één aanroep dieper, in het eigen /Domain en /Range van die subfunctie

HotPDF's stikselevaluator behandelde vroeger precies twee subfuncties, en de /Bounds-lezer vereiste een volledige array van acht elementen, dus het ene splitspunt dat een verloop van twee segmenten daadwerkelijk nodig heeft — één getal in /Bounds — faalde altijd bij het parseren en de functie gaf niets terug. /Encode werd helemaal niet toegepast. Versie 2.376.0 herschreef de selectie als de algemene k-subfunctie-zoekopdracht die de spec beschrijft en begon /Bounds tegen zijn werkelijke gedeclareerde lengte te lezen, dus een verloop met drie, vier, of vijf stops gestikt uit evenzoveel exponentiële segmenten lost nu op dezelfde manier op als een verloop met twee segmenten altijd beweerde te doen. Het voorbeeld hieronder bouwt een helling van twee segmenten zwart-via-rood-naar-wit, de vorm waar een axiale of radiale shading naar grijpt wanneer één exponentiële curve niet elke kleurstop kan dragen die een ontwerp vereist

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;

Wat kan een Type 4 PostScript-rekenmachinefunctie dat Type 2 en 3 niet kunnen?

Een PDF Type 4-functie voert een echt, zij het bewust beperkt, programma uit: een PostScript-rekenmachine die zijn invoer op een operandenstapel duwt, rekenkundige, vergelijkende, stapelmanipulatie- en booleaanse operatoren uitvoert plus if/ifelse-conditionals, en zijn uitvoer op de stapel laat staan wanneer het klaar is (ISO 32000-1 §7.10.5, Tabel 42). Er is geen lusconstructie en geen opslag van benoemde variabelen, alleen de stapel, wat het gemakkelijk maakt om over een conform programma te redeneren — maar binnen die beperkte operatorset kan Type 4 dingen uitdrukken die Type 2 en Type 3 niet kunnen, zoals een echte multi-inkt-mengformule voor een DeviceN-separatie of een halftoon-steunkleurfunctie met een conditionele drempel. HotPDF's evaluator, HPDFEvalPostScriptCalculator, tokeniseert het programma eenmalig — getallen, operatoren, en { }-procedureblokken — en doorloopt vervolgens een operandenstapel van 100 vermeldingen, de diepte die ISO 32000-1 §7.10.5 vereist, achter een harde plafond van 50.000 geëvalueerde operatoren als defensieve stopgrens tegen pathologische of handgeschreven programma's

De roll-operator: de richting is gemakkelijk verkeerd om te krijgen

roll is de operator die bij een eerste poging het meest waarschijnlijk verkeerd om uitkomt, omdat zowel de argumentvolgorde als de rotatierichting tegengesteld lopen aan hoe het Engels ze beschrijft. n j roll haalt een aantal n en een rotatiehoeveelheid j van de stapel, en verschuift vervolgens cyclisch de bovenste n stapelvermeldingen met j posities, waarbij items die van het ene uiteinde afvallen terugkeren aan het andere — het klassieke voorbeeld, rechtstreeks uit de spec, is a b c 3 1 roll dat c a b oplevert — het bovenste item verplaatst naar de onderkant van de groep, niet andersom, en elk ander item schuift één plek omhoog om ruimte te maken. HotPDF's evaluator berekent de nieuwe positie van stapelvermelding i als (i + j) mod n, wat exact overeenkomt met dat voorbeeld, maar het is een lus van twee regels die net zo gemakkelijk geschreven wordt met de rotatie omgekeerd, en een gespiegelde roll produceert nog steeds een aannemelijk ogende kleur — het is alleen niet de kleur die de bestandsauteur vroeg

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 is niet Delphi's Round: naar boven afronden bij .5 versus bankiersafronding

PostScript's round-operator lost een gelijkspel op .5 elke keer op richting het grotere gehele getal, en Delphi's ingebouwde Round-functie doet dat niet: het rondt half-naar-even af, de bankiersafrondingsconventie die afwisselt welke kant een gelijkspel op .5 opvalt, zodat herhaald afronden geen vertekening opbouwt. De twee zijn het bijna overal met elkaar eens en oneens precies op de grens die hier ertoe doet — Delphi's Round(0.5) geeft 0 terug en Round(2.5) geeft 2 terug, terwijl de round van de PDF-spec 1 en 3 wil voor diezelfde invoer — dus de mismatch verstopt zich door achteloos testen en reproduceert zich vervolgens als een consistente afwijking van precies één, waar de tussentijdse rekenkunde van een rekenmachineprogramma ook maar exact op een half geheel getal terechtkomt. ISO 32000-1 §7.10.5 Tabel 42 is expliciet dat round een fractie van .5 richting het grotere gehele getal duwt, dus HotPDF implementeert de operator als Floor(x + 0.5) in plaats van Delphi's Round aan te roepen, en elke code die de rekenkunde van een Type 4-programma handmatig herimplementeert of steekproefsgewijs controleert, heeft dezelfde vervanging nodig

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;

Validatie ten tijde van registratie vangt een slecht rekenmachineprogramma vroeg op

Een misvormd Type 4-programma is goedkoop op te vangen op het moment van opstellen en duur om ergens anders op te vangen, dus RegisterPostScriptFunction slaat niet alleen de brontekst op: het evalueert het programma eenmalig proefondervindelijk, op het middelpunt van het gedeclareerde /Domain, voordat het functieobject ooit in het document wordt geschreven. Onevenwichtige { }-blokken, een niet-herkende operator, een stapelonderloop, of een uitvoeraantal dat niet overeenkomt met /Range falen allemaal die proefrun en werpen onmiddellijk een uitzondering op, waarbij de call stack naar de RegisterPostScriptFunction-aanroep wijst in plaats van naar een renderartefact dat tijdens QA wordt ontdekt op een bestand dat al is verzonden. De proef op het middelpunt bewijst niet dat het programma correct is over zijn hele /Domain — een conditionele vertakking die zich alleen dicht bij één rand van het invoerbereik misdraagt, kan nog steeds langs één steekproefpunt glippen — maar het sluit de hele klasse van programma's uit die structureel kapot zijn in plaats van slechts in één hoek fout

Waar verlopen en steunkleuren deze functies aan het werk zetten

Type 2, 3, en 4 komen zelden geïsoleerd voor in een echte PDF; ze duiken op waar de spec ook een /Function-sleutel accepteert, en de twee meest voorkomende gebruikers zijn shadings en tintomzettingen van steunkleuren. De sh-operator van een axiaal of radiaal verloop (ISO 32000-1 §8.7.4.5) evalueert zijn /Function eenmaal per positie langs de verloopas, precies het meervoudige-stop-geval waarvoor Type 3-stiksel bestaat. De tintomzetting van een Separation- of DeviceN-kleurruimte is de andere frequente thuisbasis voor deze drie typen, en het is waar Type 4 zijn nut bewijst: een enkele steunkleur-inkt herleidt zich meestal tot een Type 2- of Type 0-curve, maar een DeviceN-mengsel van meerdere inkten met echt trapping- en overdrukgedrag heeft vaak de conditionele logica nodig die alleen een PostScript-rekenmachine kan uitdrukken, het geval dat wordt behandeld in het artikel over het renderen van Separation- en DeviceN-steunkleuren. RegisterSeparationFunc is de bijbehorende aanroep aan de opstelkant: het neemt een kleurstofnaam, een alternatieve kleurruimte, en elk object dat de Register*Function-familie teruggeeft, en verbindt die tintomzetting met een Separation-kleurruimteresource die de rest van de pagina kan selecteren met scn/SCN

Samen dekken Type 0's bemonsterde roosters en deze drie formuledreven typen elke /Function die een PDF kan declareren, en de juiste kiezen is vooral een kwestie van wat u al heeft: een opzoektabel elders berekend wordt Type 0, een tweepunts-menging wordt Type 2, meerdere mengingen in serie geschakeld over een domein worden Type 3, en alles met echte conditionele logica wordt Type 4. RegisterExponentialFunction, RegisterStitchingFunction, en RegisterPostScriptFunction maken deel uit van de standaard HotPDF-component voor Delphi en C++Builder, naast de rest van de ISO 32000-1-functie- en shading-API