Teknisk artikkel

PDF Type 2/3/4-funksjoner i Delphi: eksponentiell, PostScript

HotPDF, den native VCL PDF-komponenten for Delphi og C++Builder, evaluerer de tre PDF-funksjonstypene bygget fra formler i stedet for prøvegitter: Type 2 eksponentiell interpolasjon, Type 3 sammensying (stitching), og Type 4 PostScript-kalkulatorfunksjoner, tilsvarende ISO 32000-1 §7.10.3, §7.10.4, og §7.10.5. Type 2 blander mellom to utdatavektorer langs en kurve, Type 3 kjeder flere underfunksjoner over ett inndatadomene, og Type 4 kjører et begrenset PostScript-program som kan forgrene, sammenligne, og beregne nesten hva som helst en innholdsstrøm trenger fra sine inndata. Få noen av de tre bare litt feil, og feilen kunngjør seg aldri som en bug — den viser seg som en gradient med et dødt flatt bånd, en spotfarge som gjengis rent svart, eller en kalkulatorfunksjon som er nøyaktig én av ved de inndataene en testsuite tilfeldigvis ikke prøvde

Disse tre sitter ved siden av en fjerde, Type 0, som lagrer et samplet gitter i stedet for en formel og dekkes separat i følgeartikkelen om Type 0-fargeoppslagstabeller. De to familiene løser det samme problemet, å kartlegge en inndata til en utdata, men Type 0 er data beregnet én gang og bakt inn i filen, mens Type 2, 3, og 4 er kode leseren evaluerer ved hvert kall. Alle fire deler ett utsendingspunkt i HotPDFs fremviser, nøkkelsatt på funksjonsordbokens /FunctionType-oppføring, slik at en skyggelegging, en tint-transformasjon, eller en halvtone-spotfunksjon aldri trenger å vite hvilken av de fire den mottok før den kan spørre etter en farge

Hvordan fungerer en PDF Type 2 eksponentiell funksjon?

En PDF Type 2-funksjon beregner én formel — y = C0 + x^N × (C1 − C0), anvendt komponent for komponent — der x er funksjonens ene inndata, normalisert mot dens /Domain før formelen kjører (ISO 32000-1 §7.10.3). /C0 og /C1 er utdatavektorene ved de to endene av det området, ett tall per utdatakomponent, og /N er eksponenten som former kurven mellom dem: N = 1 gir den rette lineære rampen bak de fleste gradientstopp og duotone-konverteringer, N over 1 trekker kurven mot C0, og N mellom 0 og 1 skyver den mot C1. RegisterExponentialFunction bygger den ordboken fra fem argumenter og overleverer et funksjonsobjekt klart til å kobles inn i en skyggelegging, en halvtone-spotfunksjon, eller hvor som helst ellers spesifikasjonen aksepterer en /Function-nøkkel

Komponentantall-forholdet mellom C0 og C1 betyr noe to ganger: én gang når man forfatter en Type 2-funksjon, og igjen hver gang HotPDF må gjengi en den ikke selv opprettet. På forfatter-siden sjekker RegisterExponentialFunction C0 og C1 mot hverandre og kaster et unntak hvis de er uenige, slik at et kall som når BeginDoc, allerede har et selvkonsistent funksjonsobjekt. På gjengivelsessiden må imidlertid evaluatoren stole på hvilke som helst /C0- og /C1-arrayer en kildefil faktisk deklarerer — en trykkeri-fil åpnet for forhåndsvisning, for eksempel, eller et signert dokument vist tilbake til en bruker — og versjoner før 2.376.0 leste de arrayene inn i en buffer dimensjonert for fire komponenter, CMYK-tilfellet. En DeviceGray- eller DeviceRGB-eksponentiell tint, med en én- eller tre-elements /C0 og /C1, feilet den lesingen stille og lot begge arrayene stå på null, slik at tinten malte flatt svart i stedet for sin tiltenkte farge. Versjon 2.376.0 endret leseren til å dimensjoneres etter funksjonens faktisk deklarerte utdataantall i stedet for en fast buffer — nøyaktig den typen bug bare en ikke-CMYK-testsak avslører, ettersom den eksisterende suiten kjørte CMYK gjennomgående, der fire-inn-i-fire alltid passet

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 sammensying: å kjede underfunksjoner over et Bounds-array

En PDF Type 3-funksjon syr sammen k underfunksjoner til én stykkevis kartlegging over ett enkelt inndatas /Domain, og de to arrayene som får det til å fungere, er /Bounds og /Encode (ISO 32000-1 §7.10.4). /Bounds holder k − 1 indre delingspunkter som skjærer /Domain inn i k påfølgende intervaller; evaluatoren velger det første intervallet hvis øvre grense overstiger inndataen, eller det siste intervallet når inndataen når den siste grensen, og overleverer til det intervallets underfunksjon. /Encode kartlegger deretter inndataen fra dens posisjon inne i det intervallet om til hvilket som helst inndataområde den valgte underfunksjonen selv forventer — typisk [0, 1] hvis underfunksjonen er nok et eksponentielt segment — før evalueringen fortsetter, ett kall dypere, inn i den underfunksjonens egen /Domain og /Range

HotPDFs sammensyings-evaluator pleide å håndtere kun nøyaktig to underfunksjoner, og /Bounds-leseren dens krevde et fullt åtte-elements array, slik at det ene delingspunktet en to-segments gradient faktisk trenger — ett tall i /Bounds — alltid feilet å parse, og funksjonen returnerte ingenting. /Encode ble ikke anvendt i det hele tatt. Versjon 2.376.0 skrev om utvalget til det generelle k-underfunksjons-søket spesifikasjonen beskriver, og begynte å lese /Bounds mot dens faktiske deklarerte lengde, slik at en tre-, fire-, eller fem-stopps gradient sydd sammen fra like mange eksponentielle segmenter nå løses på samme måte som et to-segments alltid hevdet å gjøre. Eksempelet nedenfor bygger en to-segments svart-gjennom-rød-til-hvit-rampe, formen en aksial eller radial skyggelegging griper til hver gang én eksponentiell kurve ikke kan bære alle fargestoppene et design krever

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;

Hva kan en Type 4 PostScript-kalkulatorfunksjon gjøre som Type 2 og 3 ikke kan?

En PDF Type 4-funksjon kjører et ekte, om enn bevisst begrenset, program: en PostScript-kalkulator som legger inndataene sine på en operandstakk, utfører aritmetiske, sammenlignings-, stakk-manipulasjons-, og boolske operatorer pluss if/ifelse-betingelser, og etterlater utdataene sine på stakken når den er ferdig (ISO 32000-1 §7.10.5, Tabell 42). Det finnes ingen løkkekonstruksjon og ingen navngitt-variabel-lagring, bare stakken, noe som holder et konformt program lett å resonnere om — men innenfor det begrensede operatorsettet kan Type 4 uttrykke ting Type 2 og Type 3 ikke kan, slik som en ekte multi-blekk-blandingsformel for en DeviceN-separasjon eller en halvtone-spotfunksjon med en betinget terskel. HotPDFs evaluator, HPDFEvalPostScriptCalculator, tokeniserer programmet én gang — tall, operatorer, og { }-prosedyreblokker — og går deretter gjennom en 100-elements operandstakk, dybden ISO 32000-1 §7.10.5 krever, bak et hardt tak på 50 000 evaluerte operatorer som en defensiv sikkerhetsmekanisme mot patologiske eller håndskrevne programmer

roll-operatoren: retningen er lett å få baklengs

roll er operatoren som mest sannsynlig kommer ut baklengs ved første forsøk, fordi både argumentrekkefølgen og rotasjonsretningen løper motsatt av hvordan norsk (og engelsk) beskriver dem. n j roll popper et antall n og en rotasjonsmengde j, og forskyver deretter de øverste n stakkoppføringene syklisk med j posisjoner, og pakker elementer som faller av den ene enden, tilbake på den andre; det kanoniske eksempelet, rett fra spesifikasjonen, er a b c 3 1 roll som produserer c a b — det øverste elementet flytter seg til bunnen av gruppen, ikke omvendt, og hvert annet element forskyver seg opp én for å gi plass. HotPDFs evaluator beregner den nye posisjonen til stakkoppføring i som (i + j) mod n, som matcher det eksempelet nøyaktig, men det er en to-linjers løkke som er like lett å skrive med rotasjonen speilvendt, og en speilvendt roll produserer fortsatt en fargen som ser plausibel ut — den er bare ikke fargen filforfatteren ba om

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 er ikke Delphis Round: halv-opp versus bankers avrunding

PostScripts round-operator løser en .5-uavgjort mot det større heltallet hver gang, og Delphis innebygde Round-funksjon gjør ikke det: den runder halv-til-jevn, bankers-avrundingskonvensjonen som veksler hvilken vei en .5-uavgjort faller, slik at gjentatt avrunding ikke akkumulerer skjevhet. De to er enige nesten overalt og uenige nøyaktig på grensen som betyr noe her — Delphis Round(0.5) returnerer 0 og Round(2.5) returnerer 2, mens PDF-spesifikasjonens round vil ha 1 og 3 for de samme inndataene — så avviket gjemmer seg gjennom uformell testing og reproduserer seg deretter som en konsekvent av-med-én-feil hvor enn et kalkulatorprograms mellomregning havner nøyaktig på et halvt heltall. ISO 32000-1 §7.10.5 Tabell 42 er eksplisitt på at round skyver en brøkdel-.5 mot det større heltallet, så HotPDF implementerer operatoren som Floor(x + 0.5) i stedet for å kalle Delphis Round, og all kode som reimplementerer eller stikkprøve-sjekker et Type 4-programs aritmetikk for hånd, trenger den samme substitusjonen

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;

Validering ved registreringstidspunktet fanger et dårlig kalkulatorprogram tidlig

Et feilformet Type 4-program er billig å fange ved forfattertidspunktet og dyrt å fange hvor som helst ellers, så RegisterPostScriptFunction lagrer ikke bare kildeteksten: den prøve-evaluerer programmet én gang, ved midtpunktet av det deklarerte /Domain, før funksjonsobjektet noensinne skrives inn i dokumentet. Ubalanserte { }-blokker, en ukjent operator, en stakk-underflyt, eller et utdataantall som ikke matcher /Range, feiler alle den prøvekjøringen og kaster et unntak umiddelbart, med kallstakken pekende på RegisterPostScriptFunction-kallet i stedet for på et gjengivelsesartefakt oppdaget under QA på en fil som allerede ble sendt ut. Midtpunkt-prøven beviser ikke at programmet er korrekt over hele sin /Domain — en betinget gren som bare oppfører seg galt nær én kant av inndataområdet, kan fortsatt smette forbi ett enkelt prøvepunkt — men den lukker hele klassen av programmer som er strukturelt ødelagte snarere enn bare feil i ett hjørne

Hvor gradienter og spotfarger setter disse funksjonene i arbeid

Type 2, 3, og 4 dukker sjelden opp isolert i en ekte PDF; de dukker opp hvor som helst spesifikasjonen aksepterer en /Function-nøkkel, og de to vanligste konsumentene er skyggelegginger og spotfarge-tint-transformasjoner. En aksial eller radial gradients sh-operator (ISO 32000-1 §8.7.4.5) evaluerer sin /Function én gang per posisjon langs gradientaksen, noe som er nøyaktig det multi-stopps-tilfellet Type 3-sammensying finnes for. En Separation- eller DeviceN-fargeroms tint-transformasjon er det andre hyppige hjemmet for disse tre typene, og det er der Type 4 gjør sin nytte: et enkelt spotblekk reduseres vanligvis til en Type 2- eller Type 0-kurve, men en DeviceN-blanding av flere blekk med ekte trapping- og overprint-oppførsel trenger ofte den betingede logikken bare en PostScript-kalkulator kan uttrykke, tilfellet dekket i artikkelen om å gjengi Separation- og DeviceN-spotfarger. RegisterSeparationFunc er koblings-kallet på forfatter-siden: den tar et fargestoffnavn, et alternativt fargerom, og hvilket som helst objekt Register*Function-familien returnerer, og kobler den tint-transformasjonen inn i en Separation-fargeroms-ressurs resten av siden kan velge med scn/SCN

Sammen dekker Type 0s samplede gitter og disse tre formeldrevne typene hver eneste /Function en PDF kan deklarere, og å velge riktig type handler for det meste om hva man allerede har: en oppslagstabell beregnet et annet sted blir Type 0, en to-endepunkts-blanding blir Type 2, flere blandinger kjedet over et domene blir Type 3, og alt med ekte betinget logikk blir Type 4. RegisterExponentialFunction, RegisterStitchingFunction, og RegisterPostScriptFunction er en del av den standard HotPDF-komponenten for Delphi og C++Builder, sammen med resten av dens ISO 32000-1 funksjons- og skyggeleggings-API