HotPDF, il componente PDF VCL nativo per Delphi e C++Builder, valuta i tre tipi di funzione PDF costruiti a partire da formule anziché da griglie di campioni: l'interpolazione esponenziale di Tipo 2, la giunzione (stitching) di Tipo 3 e le funzioni calcolatore PostScript di Tipo 4, corrispondenti a ISO 32000-1 §7.10.3, §7.10.4 e §7.10.5. Il Tipo 2 sfuma tra due vettori di output lungo una curva, il Tipo 3 concatena più sotto-funzioni su un unico dominio di input, e il Tipo 4 esegue un programma PostScript limitato capace di diramarsi, confrontare e calcolare quasi qualsiasi cosa un content stream richieda dai propri input. Sbagliare anche di poco uno di questi tre non si annuncia mai come un bug — si manifesta come un gradiente con una banda piatta morta, un colore spot che viene reso nero puro, oppure una funzione calcolatore sbagliata esattamente di uno negli input che una suite di test non ha per caso provato
Questi tre si affiancano a un quarto tipo, il Tipo 0, che memorizza una griglia campionata invece di una formula ed è trattato separatamente nell'articolo di approfondimento sulle tabelle di lookup colore Tipo 0. Le due famiglie risolvono lo stesso problema, mappare un input su un output, ma il Tipo 0 è dato calcolato una volta e incorporato nel file, mentre Tipo 2, 3 e 4 sono codice che il lettore valuta a ogni chiamata. Tutti e quattro condividono un unico punto di dispatch nel renderer di HotPDF, indicizzato sulla voce /FunctionType del dizionario della funzione, cosicché uno shading, una trasformazione di tinta o una funzione spot di retinatura non debbano mai sapere quale dei quattro hanno ricevuto prima di poter richiedere un colore
Come funziona una funzione esponenziale PDF di Tipo 2?
Una funzione PDF di Tipo 2 calcola un'unica formula — y = C0 + x^N × (C1 − C0), applicata componente per componente — dove x è l'unico input della funzione, normalizzato rispetto al proprio /Domain prima che la formula venga eseguita (ISO 32000-1 §7.10.3). /C0 e /C1 sono i vettori di output ai due estremi di quell'intervallo, un numero per ogni componente di output, e /N è l'esponente che modella la curva tra di essi: N = 1 dà la rampa lineare rettilinea dietro la maggior parte degli stop di gradiente e delle conversioni duotono, N maggiore di 1 attrae la curva verso C0, e N tra 0 e 1 la spinge verso C1. RegisterExponentialFunction costruisce quel dizionario a partire da cinque argomenti e restituisce un oggetto funzione pronto da collegare a uno shading, a una funzione spot di retinatura, o ovunque altro la specifica accetti una chiave /Function
La relazione sul numero di componenti tra C0 e C1 conta due volte: una volta quando si crea una funzione di Tipo 2, e di nuovo ogni volta che HotPDF deve renderizzarne una che non ha creato lui stesso. Sul lato della creazione, RegisterExponentialFunction verifica C0 e C1 l'uno contro l'altro e solleva un'eccezione se sono in disaccordo, cosicché una chiamata che raggiunge BeginDoc sia già un oggetto funzione internamente coerente. Sul lato del rendering, tuttavia, il valutatore deve fidarsi di qualsiasi array /C0 e /C1 che un file sorgente dichiari realmente — un file da tipografia aperto per l'anteprima, ad esempio, o un documento firmato mostrato a un utente — e le versioni precedenti alla 2.376.0 leggevano quegli array in un buffer dimensionato per quattro componenti, il caso CMYK. Una tinta esponenziale DeviceGray o DeviceRGB, con un /C0 e /C1 a uno o tre elementi, falliva silenziosamente quella lettura e lasciava entrambi gli array a zero, per cui la tinta veniva dipinta nero piatto invece del colore previsto. La versione 2.376.0 ha ridimensionato il lettore in base al numero di output effettivamente dichiarato dalla funzione invece che a un buffer fisso — esattamente il tipo di bug che solo un caso di test non-CMYK espone, poiché la suite esistente eseguiva CMYK ovunque, dove quattro-in-quattro entrava sempre correttamente
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;
Giunzione di Tipo 3: concatenare sotto-funzioni tramite un array Bounds
Una funzione PDF di Tipo 3 cuce insieme k sotto-funzioni in un'unica mappatura a tratti sopra il /Domain di un singolo input, e i due array che rendono possibile ciò sono /Bounds ed /Encode (ISO 32000-1 §7.10.4). /Bounds contiene k − 1 punti di divisione interni che ritagliano /Domain in k intervalli consecutivi; il valutatore sceglie il primo intervallo il cui limite superiore eccede l'input, oppure l'ultimo intervallo una volta che l'input raggiunge il limite finale, e passa il controllo alla sotto-funzione di quell'intervallo. /Encode poi rimappa l'input dalla sua posizione all'interno di quell'intervallo sull'intervallo di input che la sotto-funzione scelta si aspetta — tipicamente [0, 1] se la sotto-funzione è un ulteriore segmento esponenziale — prima che la valutazione prosegua, un livello di chiamata più in profondità, nel /Domain e /Range propri di quella sotto-funzione
Il valutatore di giunzione di HotPDF gestiva un tempo solo esattamente due sotto-funzioni, e il suo lettore di /Bounds richiedeva un array completo a otto elementi, per cui l'unico punto di divisione di cui un gradiente a due segmenti ha effettivamente bisogno — un numero in /Bounds — falliva sempre l'analisi e la funzione non restituiva nulla. /Encode non veniva applicato affatto. La versione 2.376.0 ha riscritto la selezione come la ricerca generale a k sotto-funzioni descritta dalla specifica e ha iniziato a leggere /Bounds in base alla sua reale lunghezza dichiarata, cosicché un gradiente a tre, quattro o cinque stop cucito da altrettanti segmenti esponenziali si risolva ora nello stesso modo in cui uno a due segmenti aveva sempre preteso di fare. L'esempio seguente costruisce una rampa a due segmenti nero-rosso-bianco, la forma a cui uno shading assiale o radiale ricorre ogni volta che una singola curva esponenziale non riesce a portare tutti gli stop di colore richiesti da un design
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;
Cosa può fare una funzione calcolatore PostScript di Tipo 4 che Tipo 2 e 3 non possono?
Una funzione PDF di Tipo 4 esegue un programma vero, per quanto deliberatamente limitato: un calcolatore PostScript che accumula i propri input su uno stack di operandi, esegue operatori aritmetici, di confronto, di manipolazione dello stack e booleani, oltre a condizionali if/ifelse, e lascia i propri output sullo stack al termine (ISO 32000-1 §7.10.5, Tabella 42). Non esiste alcun costrutto di ciclo né alcuna memorizzazione tramite variabili con nome, solo lo stack, il che mantiene facile da seguire il ragionamento su un programma conforme — ma entro quel set di operatori limitato, il Tipo 4 può esprimere cose che il Tipo 2 e il Tipo 3 non possono, come una vera formula di miscelazione multi-inchiostro per una separazione DeviceN o una funzione spot di retinatura con una soglia condizionale. Il valutatore di HotPDF, HPDFEvalPostScriptCalculator, tokenizza il programma una volta — numeri, operatori e blocchi di procedura { } — quindi percorre uno stack di operandi a 100 voci, la profondità richiesta da ISO 32000-1 §7.10.5, dietro un tetto rigido di 50.000 operatori valutati come barriera difensiva contro programmi patologici o scritti a mano
L'operatore roll: la direzione è facile da sbagliare al contrario
roll è l'operatore più probabile da ottenere invertito al primo tentativo, perché sia l'ordine dei suoi argomenti sia la sua direzione di rotazione vanno entrambi in senso opposto a come li descriverebbe l'italiano. n j roll estrae un conteggio n e un ammontare di rotazione j, quindi ruota ciclicamente le prime n voci dello stack di j posizioni, riportando all'altro capo gli elementi che escono da un'estremità; l'esempio canonico, direttamente dalla specifica, è a b c 3 1 roll che produce c a b — l'elemento in cima si sposta in fondo al gruppo, non viceversa, e ogni altro elemento si sposta in su di uno per fare spazio. Il valutatore di HotPDF calcola la nuova posizione della voce di stack i come (i + j) mod n, il che corrisponde esattamente a quell'esempio, ma è un ciclo di due righe altrettanto facile da scrivere con la rotazione invertita, e un roll speculare produce comunque un colore dall'aspetto plausibile — semplicemente non è il colore che l'autore del file ha richiesto
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 non è il Round di Delphi: arrotondamento verso l'alto contro arrotondamento bancario
L'operatore round di PostScript risolve un pareggio a .5 sempre verso l'intero più grande, mentre la funzione Round integrata di Delphi non lo fa: arrotonda al pari più vicino, la convenzione dell'arrotondamento bancario che alterna la direzione in cui cade un pareggio a .5 cosicché l'arrotondamento ripetuto non accumuli distorsione. I due concordano quasi ovunque e sono in disaccordo esattamente sul confine che qui conta — Round(0.5) di Delphi restituisce 0 e Round(2.5) restituisce 2, mentre il round della specifica PDF vuole 1 e 3 per quegli stessi input — quindi la discrepanza si nasconde durante test superficiali e poi si riproduce come uno scarto costante di uno ovunque la matematica intermedia di un programma calcolatore cada esattamente su un mezzo intero. La Tabella 42 di ISO 32000-1 §7.10.5 è esplicita nel dire che round spinge un valore frazionario a .5 verso l'intero maggiore, quindi HotPDF implementa l'operatore come Floor(x + 0.5) invece di chiamare il Round di Delphi, e qualsiasi codice che reimplementi o verifichi a campione a mano l'aritmetica di un programma di Tipo 4 ha bisogno della stessa sostituzione
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;
La validazione al momento della registrazione intercetta presto un programma calcolatore difettoso
Un programma di Tipo 4 malformato è economico da intercettare al momento della creazione e costoso da intercettare altrove, quindi RegisterPostScriptFunction non si limita a memorizzare il testo sorgente: valuta il programma a titolo di prova una volta, nel punto medio del /Domain dichiarato, prima ancora che l'oggetto funzione venga scritto nel documento. Blocchi { } sbilanciati, un operatore non riconosciuto, uno svuotamento dello stack (underflow) o un numero di output che non corrisponde a /Range falliscono tutti quella prova ed sollevano immediatamente un'eccezione, con lo stack di chiamata che punta alla chiamata RegisterPostScriptFunction invece che a un artefatto di rendering scoperto durante il QA su un file già distribuito. La prova al punto medio non dimostra che il programma sia corretto sull'intero /Domain — un ramo condizionale che si comporta male solo vicino a un'estremità dell'intervallo di input può ancora sfuggire a un singolo punto di campionamento — ma esclude l'intera classe di programmi che sono strutturalmente rotti anziché semplicemente sbagliati in un angolo
Dove gradienti e colori spot mettono al lavoro queste funzioni
Tipo 2, 3 e 4 raramente compaiono isolati in un PDF reale; emergono ovunque la specifica accetti una chiave /Function, e i due consumatori più comuni sono gli shading e le trasformazioni di tinta dei colori spot. L'operatore sh di un gradiente assiale o radiale (ISO 32000-1 §8.7.4.5) valuta la propria /Function una volta per ogni posizione lungo l'asse del gradiente, che è esattamente il caso multi-stop per cui esiste la giunzione di Tipo 3. La trasformazione di tinta di uno spazio colore Separation o DeviceN è l'altra sede frequente per questi tre tipi, ed è dove il Tipo 4 si guadagna il proprio ruolo: un singolo inchiostro spot di solito si riduce a una curva di Tipo 2 o Tipo 0, ma una miscela DeviceN di più inchiostri con un comportamento reale di trapping e sovrastampa spesso richiede la logica condizionale che solo un calcolatore PostScript può esprimere, il caso trattato nell'articolo sul rendering dei colori spot Separation e DeviceN. RegisterSeparationFunc è la chiamata di accoppiamento sul lato della creazione: accetta un nome di colorante, uno spazio colore alternativo e qualsiasi oggetto restituito dalla famiglia Register*Function, e collega quella trasformazione di tinta a una risorsa di spazio colore Separation che il resto della pagina può selezionare con scn/SCN
Insieme, le griglie campionate del Tipo 0 e questi tre tipi guidati da formule coprono ogni /Function che un PDF possa dichiarare, e scegliere quello giusto è per lo più una questione di cosa si ha già a disposizione: una tabella di lookup calcolata altrove diventa Tipo 0, una sfumatura a due estremi diventa Tipo 2, più sfumature concatenate su un dominio diventano Tipo 3, e qualsiasi cosa con vera logica condizionale diventa Tipo 4. RegisterExponentialFunction, RegisterStitchingFunction e RegisterPostScriptFunction fanno parte del componente HotPDF standard per Delphi e C++Builder, insieme al resto della sua API di funzioni e shading ISO 32000-1