Teknisk artikel

PDF Typ 2/3/4-funktioner i Delphi: exponentiell, PostScript

HotPDF, den native VCL-PDF-komponenten för Delphi och C++Builder, utvärderar de tre PDF-funktionstyper som byggs från formler snarare än sampelrutnät: Typ 2 exponentiell interpolation, Typ 3 hopfogning (stitching), och Typ 4 PostScript-kalkylatorfunktioner, motsvarande ISO 32000-1 §7.10.3, §7.10.4, och §7.10.5. Typ 2 blandar mellan två utdatavektorer längs en kurva, Typ 3 kedjar samman flera delfunktioner över en enda indatadomän, och Typ 4 kör ett begränsat PostScript-program som kan förgrena sig, jämföra och beräkna nästan vad som helst en innehållsström behöver från sin indata. Få någon av de tre lite fel och felet aviserar sig aldrig som en bugg — det visar sig som en gradient med ett dött platt band, en dekorfärg som renderas ren svart, eller en kalkylatorfunktion som är exakt ett fel vid de indata en testsvit råkade inte prova

Dessa tre står bredvid en fjärde, Typ 0, som lagrar ett samplat rutnät i stället för en formel och täcks separat i följdartikeln om Typ 0-färguppslagstabeller. De två familjerna löser samma problem, att mappa en indata till en utdata, men Typ 0 är data beräknad en gång och inbakad i filen, medan Typ 2, 3, och 4 är kod läsaren utvärderar vid varje anrop. Alla fyra delar en enda dispatchpunkt i HotPDF:s renderare, nyckad på funktionsordbokens /FunctionType-post, så en skuggning, en tonöverföring, eller en rasterpunkts spot-funktion aldrig behöver veta vilken av de fyra den fick innan den kan begära en färg

Hur fungerar en PDF Typ 2-exponentialfunktion?

En PDF Typ 2-funktion beräknar en formel — y = C0 + x^N × (C1 − C0), applicerad komponent för komponent — där x är funktionens enda indata, normaliserad mot dess /Domain innan formeln körs (ISO 32000-1 §7.10.3). /C0 och /C1 är utdatavektorerna vid de två ändarna av det intervallet, ett tal per utdatakomponent, och /N är exponenten som formar kurvan mellan dem: N = 1 ger den raka linjära rampen bakom de flesta gradientstopp och duoton-konverteringar, N över 1 drar kurvan mot C0, och N mellan 0 och 1 skjuter den mot C1. RegisterExponentialFunction bygger den ordboken från fem argument och lämnar tillbaka ett funktionsobjekt redo att kopplas in i en skuggning, en rasterpunkts spot-funktion, eller var som helst annars specifikationen accepterar en /Function-nyckel

Komponenträkningsförhållandet mellan C0 och C1 spelar roll två gånger: en gång när du författar en Typ 2-funktion, och igen närhelst HotPDF måste rendera en den inte skapade. På författarsidan kontrollerar RegisterExponentialFunction C0 och C1 mot varandra och kastar om de är oense, så ett anrop som når BeginDoc redan är ett självkonsekvent funktionsobjekt. På renderingssidan måste dock utvärderaren lita på vilka /C0- och /C1-arrayer en källfil faktiskt deklarerar — en tryckerifil öppnad för förhandsvisning, säg, eller ett signerat dokument visat tillbaka till en användare — och versioner före 2.376.0 läste de arrayerna in i en buffert dimensionerad för fyra komponenter, CMYK-fallet. En DeviceGray- eller DeviceRGB-exponentiell ton, med en en- eller treelements /C0 och /C1, misslyckades med den läsningen tyst och lämnade båda arrayerna på noll, så tonen målades platt svart i stället för sin avsedda färg. Version 2.376.0 ändrade om läsaren till funktionens faktiska deklarerade utdataantal i stället för en fast buffert — exakt den typ av bugg som bara ett icke-CMYK-testfall avslöjar, eftersom den befintliga sviten körde CMYK genomgående, där fyra-in-i-fyra alltid passade

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;

Typ 3-hopfogning: kedjar delfunktioner via en Bounds-array

En PDF Typ 3-funktion fogar samman k delfunktioner till en styckvis mappning över en enda indatas /Domain, och de två arrayerna som får det att fungera är /Bounds och /Encode (ISO 32000-1 §7.10.4). /Bounds innehåller k − 1 inre delpunkter som skär /Domain i k på varandra följande intervall; utvärderaren väljer det första intervallet vars övre gräns överstiger indatan, eller det sista intervallet så fort indatan når den slutliga gränsen, och lämnar över till det intervallets delfunktion. /Encode mappar sedan om indatan från dess position inuti det intervallet till vilket indataintervall den valda delfunktionen själv förväntar sig — typiskt [0, 1] om delfunktionen är ännu ett exponentiellt segment — innan utvärderingen fortsätter, ett anrop djupare, in i den delfunktionens egen /Domain och /Range

HotPDF:s hopfogningsutvärderare hanterade tidigare bara exakt två delfunktioner, och dess /Bounds-läsare krävde en fullständig åttaelementsarray, så den enda delpunkt en tvåsegmentsgradient faktiskt behöver — ett tal i /Bounds — misslyckades alltid att tolkas och funktionen returnerade ingenting. /Encode applicerades inte alls. Version 2.376.0 skrev om urvalet som den generella k-delfunktionssökning specifikationen beskriver och började läsa /Bounds mot dess verkliga deklarerade längd, så en tre-, fyra-, eller femstopps-gradient hopfogad från så många exponentiella segment löser sig nu på samma sätt som en tvåsegments alltid påstod sig göra. Exemplet nedan bygger en tvåsegments svart-genom-röd-till-vit-ramp, formen en axial- eller radialskuggning tar till närhelst en exponentiell kurva inte kan bära varje färgstopp en design kräver

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;

Vad kan en Typ 4 PostScript-kalkylatorfunktion göra som Typ 2 och 3 inte kan?

En PDF Typ 4-funktion kör ett genuint, om än medvetet begränsat, program: en PostScript-kalkylator som skjuter sin indata på en operandstack, kör aritmetik-, jämförelse-, stackmanipulations- och booleoperatorer plus if/ifelse-villkor, och lämnar sin utdata på stacken när den är klar (ISO 32000-1 §7.10.5, Tabell 42). Det finns ingen loop-konstruktion och ingen namngiven variabellagring, bara stacken, vilket håller ett regelrätt program enkelt att resonera kring — men inom den begränsade operatoruppsättningen kan Typ 4 uttrycka saker Typ 2 och Typ 3 inte kan, som en riktig flerfärgs-blandningsformel för en DeviceN-separation eller en rasterpunkts spot-funktion med ett villkorat tröskelvärde. HotPDF:s utvärderare, HPDFEvalPostScriptCalculator, tokeniserar programmet en gång — tal, operatorer, och { }-procedurblock — och går sedan igenom en stack med 100 poster, djupet ISO 32000-1 §7.10.5 kräver, bakom ett hårt tak på 50 000 utvärderade operatorer som ett defensivt skyddsnät mot patologiska eller handskrivna program

roll-operatorn: riktningen är lätt att få bakvänd

roll är operatorn som mest sannolikt blir bakvänd vid ett första försök, eftersom både dess argumentordning och dess rotationsriktning går motsatt hur svenskan skulle beskriva dem. n j roll poppar ett antal n och ett rotationsbelopp j, och skiftar sedan cykliskt de översta n stackposterna med j positioner, och slår om poster som faller av ena änden tillbaka på den andra; det kanoniska exemplet, direkt från specifikationen, är a b c 3 1 roll som ger c a b — den översta posten flyttar till botten av gruppen, inte tvärtom, och varje annan post skiftar upp ett steg för att göra plats. HotPDF:s utvärderare beräknar den nya positionen för stackpost i som (i + j) mod n, vilket matchar det exemplet exakt, men det är en tvåradsloop som är precis lika lätt att skriva med rotationen spegelvänd, och en spegelvänd roll ger fortfarande en trovärdig färg — den är bara inte färgen filens författare bad 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 är inte Delphis Round: uppåtavrundning kontra bankavrundning

PostScripts round-operator löser en .5-oavgjord situation mot det större heltalet varje gång, och Delphis inbyggda Round-funktion gör inte det: den avrundar half-to-even, bankavrundningskonventionen som växlar vilket håll en .5-oavgjord situation faller åt så att upprepad avrundning inte ackumulerar bias. De två är överens nästan överallt och oense exakt på gränsen som spelar roll här — Delphis Round(0.5) returnerar 0 och Round(2.5) returnerar 2, medan PDF-specifikationens round vill ha 1 och 3 för samma indata — så avvikelsen döljer sig genom oreflekterad testning och reproducerar sig sedan som ett konsekvent ett-fel-fel var en kalkylatorprograms mellanberäkning hamnar exakt på ett halvt heltal. ISO 32000-1 §7.10.5 Tabell 42 är uttrycklig om att round skjuter en bråkdel .5 mot det större heltalet, så HotPDF implementerar operatorn som Floor(x + 0.5) i stället för att anropa Delphis Round, och all kod som implementerar om eller stickprovskontrollerar ett Typ 4-programs aritmetik för hand behöver samma substitution

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;

Registreringstidsvalidering fångar ett trasigt kalkylatorprogram tidigt

Ett felformat Typ 4-program är billigt att fånga vid författartillfället och dyrt att fånga någon annanstans, så RegisterPostScriptFunction lagrar inte bara källtexten: den provkör programmet en gång, vid mittpunkten av den deklarerade /Domain, innan funktionsobjektet någonsin skrivs in i dokumentet. Obalanserade { }-block, en okänd operator, ett stack-underflöde, eller ett utdataantal som inte matchar /Range misslyckas alla den provkörningen och kastar ett undantag omedelbart, med anropsstacken pekande mot RegisterPostScriptFunction-anropet i stället för mot en renderingsartefakt upptäckt under QA på en fil som redan skickats. Mittpunktsprovet bevisar inte att programmet är korrekt över hela sin /Domain — en villkorlig gren som bara beter sig fel nära en kant av indataintervallet kan fortfarande smita förbi en enda samplingspunkt — men det stänger av hela klassen av program som är strukturellt trasiga snarare än bara fel i ett hörn

Var gradienter och dekorfärger sätter dessa funktioner i arbete

Typ 2, 3, och 4 dyker sällan upp isolerat i en riktig PDF; de dyker upp var specifikationen än accepterar en /Function-nyckel, och de två vanligaste konsumenterna är skuggningar och tonöverföringar för dekorfärger. En axial- eller radialgradients sh-operator (ISO 32000-1 §8.7.4.5) utvärderar sin /Function en gång per position längs gradientaxeln, vilket är exakt det flerstoppsfall Typ 3-hopfogning finns för. En Separation- eller DeviceN-färgrymds tonöverföring är det andra vanliga hemmet för dessa tre typer, och det är där Typ 4 gör sig förtjänt: en enda dekorfärg reduceras vanligtvis till en Typ 2- eller Typ 0-kurva, men en DeviceN-blandning av flera färger med riktigt fångst- och överfärgningsbeteende behöver ofta den villkorslogik bara en PostScript-kalkylator kan uttrycka, fallet som täcks i artikeln om att rendera Separation- och DeviceN-dekorfärger. RegisterSeparationFunc är det ihopparande anropet på författarsidan: det tar ett färgnamn, en alternativ färgrymd, och vilket objekt som helst Register*Function-familjen returnerar, och kopplar in den tonöverföringen i en Separation-färgrymdsresurs resten av sidan kan välja med scn/SCN

Tillsammans täcker Typ 0:s samplade rutnät och dessa tre formelstyrda typer varje /Function en PDF kan deklarera, och att välja rätt en handlar mest om vad du redan har: en uppslagstabell beräknad någon annanstans blir Typ 0, en tvåändpunktsblandning blir Typ 2, flera blandningar hopfogade över en domän blir Typ 3, och allt med riktig villkorslogik blir Typ 4. RegisterExponentialFunction, RegisterStitchingFunction, och RegisterPostScriptFunction är en del av standardversionen av HotPDF-komponenten för Delphi och C++Builder, tillsammans med resten av dess ISO 32000-1-funktions- och skuggnings-API