La maggior parte del codice Delphi che interagisce con i PDF tratta il formato come un contenitore per due elementi: blocchi di testo e alcune bitmap posizionate. Questa visione è corretta nei limiti del suo ambito, ma lascia inutilizzata la parte più potente del formato. Una pagina PDF è una tela 2D indipendente dalla risoluzione, basata sullo stesso modello grafico di PostScript. Può tracciare linee, curve, aree piene, sfumature e motivi ripetuti, il tutto sotto forma di vettori che mantengono la massima nitidezza a qualsiasi livello di zoom e stampano alla massima risoluzione del dispositivo. Se stai disegnando un logo, un diagramma, una filigrana o il bordo di un certificato, il tracciato vettoriale è quasi sempre la primitiva corretta, ed è più leggero e nitido rispetto all'immagine rasterizzata a cui molti programmi ricorrono.
Questo articolo analizza il modello vettoriale così come definito dallo standard ISO 32000-1 e illustra le relative chiamate in PDFlibPas. L'obiettivo è rendere concreta la specifica, poiché l'API vi si mappa strettamente, e comprendere l'una aiuta a comprendere l'altra.
La pagina è un motore di tracciati
Lo standard ISO 32000-1 §8.5 descrive la grafica in due fasi che non si sovrappongono mai. Innanzitutto si costruisce un tracciato, che è pura geometria senza alcun risultato visibile. Successivamente si colora quel tracciato in una singola operazione che ne traccia il contorno (stroke), ne riempie l'interno (fill) o esegue entrambe le cose. Nulla appare sulla pagina durante la costruzione. Il tracciato è una sequenza astratta di punti e segmenti conservata nello stato grafico finché un operatore di disegno non la consuma, momento in cui viene renderizzata e rimossa.
Un tracciato è composto da uno o più sotto-tracciati. Un sotto-tracciato inizia in un punto e cresce aggiungendo segmenti: linee rette, curve di Bezier cubiche e, su alcune piattaforme, interi rettangoli aggiunti come sotto-tracciati chiusi autonomi. In PDFlibPas si apre un tracciato con StartPath, che definisce il punto di partenza, quindi lo si estende con AddLineToPath and AddCurveToPath. Ogni chiamata sposta un punto corrente implicito, in modo che il segmento successivo continui da dove è terminato il precedente. ClosePath traccia un segmento rettilineo finale che torna all'inizio del sotto-tracciato, un aspetto importante per il tracciamento del contorno poiché produce una reale giunzione d'angolo sul vertice di chiusura anziché due terminali aperti.
// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);
PDF.StartPath(150, 100); // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath; // straight segment back to (150, 100)
PDF.DrawPath(2); // 2 = fill and stroke; path is consumed
Le curve utilizzano AddCurveToPath, che accetta due punti di controllo Bezier e un punto finale: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). La curva si sviluppa dal punto corrente fino a (EndX, EndY), attratta dai due punti di controllo lungo il percorso. Gli archi circolari sono disponibili tramite AddArcToPath(CenterX, CenterY, TotalAngle), dove il raggio è ricavato dalla distanza tra il punto corrente e il centro, e il motore emette l'arco come una catena di segmenti di Bezier. I rettangoli dispongono di una scorciatoia, AddBoxToPath(Left, Top, Width, Height), che aggiunge un rettangolo chiuso completo come sotto-tracciato indipendente senza necessità di chiamare prima StartPath.
Le due regole di riempimento, e perché differiscono
Quando si riempie un tracciato che si auto-interseca o contiene un anello interno, il motore di rendering ha bisogno di una regola per determinare quali regioni sono all'interno della forma e quali costituiscono dei fori. Lo standard ISO 32000-1 §8.5.3.3 definisce due, che possono colorare la stessa geometria in modi differenti. La regola del numero di avvolgimento diverso da zero (nonzero winding rule) conta gli attraversamenti con segno di una semiretta tracciata da un punto di test all'infinito, aggiungendo uno per ogni segmento che attraversa da sinistra a destra e sottraendo uno per ciascuno che attraversa nella direzione opposta; il punto si considera interno se il totale non è zero. La regola pari-dispari (even-odd rule) ignora la direzione e conta semplicemente gli attraversamenti, considerando il punto come interno se il conteggio è dispari.
Il caso classico in cui le regole divergono è una forma con un foro, come una ciambella o una rondella. Disegnando un confine esterno e un confine interno al suo interno: con la regola pari-dispari, l'anello interno crea sempre un foro, poiché ogni punto tra i due confini viene attraversato una volta e ogni punto all'interno dell'anello interno viene attraversato due volte. Con la regola del numero di avvolgimento diverso da zero, il foro appare solo se l'anello interno si avvolge nella direzione opposta rispetto a quello esterno; se si avvolgono nella stessa direzione, gli avvolgimenti si sommano anziché annullarsi, e la regione interna viene riempita completamente. Una stella a cinque punte disegnata come un unico contorno auto-intersecante mostra la stessa differenza: la regola pari-dispari lascia vuoto il pentagono centrale, mentre quella dell'avvolgimento diverso da zero lo riempie.
PDFlibPas seleziona la regola in base alla chiamata di disegno effettuata, non tramite un flag. DrawPath esegue il riempimento con la regola del numero di avvolgimento diverso da zero; DrawPathEvenOdd riempie con la regola pari-dispari. Entrambe accettano la stessa modalità intera: 0 traccia solo il contorno, 1 esegue solo il riempimento e 2 riempie e traccia il contorno. La regola pari-dispari è lo strumento più semplice per creare fori proprio perché non richiede di gestire la direzione del sotto-tracciato.
// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120); // outer
PDF.AddBoxToPath(140, 130, 120, 60); // inner
PDF.DrawPath(1); // 1 = fill, nonzero winding
// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120); // outer
PDF.AddBoxToPath(140, 330, 120, 60); // inner cut-out
PDF.DrawPathEvenOdd(1); // 1 = fill, even-odd
Le sfumature assiali variano il colore lungo una linea
Un colore di riempimento piatto ha un unico valore su tutta l'area. Una sfumatura varia il colore in modo continuo, e il tipo più semplice è la sfumatura assiale, o lineare. Lo standard ISO 32000-1 §8.7.4.5 la specifica come una sfumatura assiale di Tipo 2 (axial shading): si definiscono due punti che tracciano un asse, un colore iniziale al primo punto e uno finale al secondo, e il motore di rendering interpola il colore lungo tale asse. Ogni punto nella regione riempita assume il colore della sua proiezione perpendicolare sull'asse, in modo che la sfumatura si sviluppi in bande ad angolo retto rispetto alla linea che unisce i due punti.
In PDFlibPas una sfumatura è una risorsa del documento dotata di nome, che viene creata una volta e poi selezionata come colore attivo. NewRGBAxialShader la registra. La firma è NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): i due punti finali dell'asse, le terne RGB a ciascuna estremità come valori nell'intervallo da 0 a 1 e un flag Extend. Impostando Extend su 1, i colori alle estremità proseguono come riempimento a tinta unita oltre i limiti dell'asse, comportamento solitamente desiderato affinché gli angoli di un'area esterni all'asse non rimangano non colorati; 0 li lascia intatti. Una volta creato lo shader, lo si associa con SetFillShader per le aree piene, SetLineShader per i contorni tracciati o SetTextShader per il testo. L'associazione rimane attiva per le successive chiamate di disegno, perciò il tracciato disegnato successivamente assumerà la sfumatura invece di un colore piatto.
// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
0, 100, 0.10, 0.25, 0.55, // start point and start RGB
0, 260, 1.00, 1.00, 1.00, // end point and end RGB
1); // 1 = extend ends as solid color
// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1); // 1 = fill, now filled by the shader
In questo caso l'asse è verticale, da y=100 a y=260 con una coordinata x fissa, perciò le bande di colore si sviluppano in orizzontale e il rettangolo sfuma dal blu alla base al bianco in cima. Poiché lo shader è identificato da un nome, una sola definizione può riempire qualsiasi numero di forme sulla pagina, e per tornare a un colore piatto è sufficiente effettuare un'altra chiamata a SetFillColor prima del tracciato successivo.
I motivi ripetuti (tiling patterns) replicano una cella
Mentre una sfumatura varia un singolo colore in modo fluido, un motivo ripetuto (tiling pattern) replica un piccolo elemento grafico su un'intera area. Lo standard ISO 32000-1 §8.7.3.1 definisce un tiling pattern come una cella del motivo, ovvero un blocco di contenuto indipendente, che il motore di rendering replica su una griglia fissa per coprire l'area colorata. In questo modo si creano tratteggi per disegni tecnici, motivi ripetuti di un marchio dietro un'intestazione o sfondi strutturati che rimangono nitidi a livello vettoriale e non appesantiscono il file, indipendentemente dalla grandezza dell'area, poiché la cella viene memorizzata una sola volta e referenziata ovunque.
PDFlibPas crea la cella del motivo catturando contenuto già presente sulla pagina. Si acquisisce una pagina o una regione con CapturePage, si trasforma l'acquisizione in un motivo con nome mediante NewTilingPatternFromCapturedPage(PatternName, CaptureID), e si seleziona tale motivo come riempimento corrente con SetFillTilingPattern(PatternName). Da quel momento in poi, qualsiasi tracciato riempito verrà dipinto con la cella ripetuta invece di un colore piatto, in modo analogo al funzionamento del riempimento tramite shader, ma utilizzando la cella come sorgente. La sequenza è più complessa rispetto a una singola chiamata, perciò, se il passaggio di acquisizione non è chiaro, si può considerare il motivo come un'operazione in due fasi: prima si genera la cella acquisita, poi la si associa come riempimento per nome prima di disegnare la regione desiderata.
Combinare le primitive
I vari elementi si compongono direttamente. Una forma di Bezier riempita è un tracciato di curve disegnato con DrawPath. Lo stesso contorno disegnato con DrawPathEvenOdd, dopo aver aggiunto un anello interno, mostra un foro che il riempimento basato sull'avvolgimento avrebbe chiuso. Un rettangolo con riempimento sfumato è un box associato a uno shader. L'esempio seguente disegna tutti e tre in sequenza, in modo che la differenza tra le due regole di riempimento sia visibile su un'unica pagina, e infine posiziona sotto di essi un pannello sfumato.
// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480); // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480); // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1); // 1 = fill
// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300); // new subpath: the hole
PDF.AddArcToPath(200, 300, 360); // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1); // hole is punched out
// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
60, 100, 0.95, 0.55, 0.10,
60, 200, 0.20, 0.10, 0.40,
1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);
Due dettagli sono particolarmente importanti. È la chiamata di disegno a determinare la regola di riempimento, quindi la scelta tra DrawPath e DrawPathEvenOdd corrisponde alla scelta tra avvolgimento diverso da zero e pari-dispari, e per le forme con fori la regola pari-dispari evita di dover analizzare la direzione dei sotto-tracciati. Inoltre, lo stato grafico viene campionato al momento del disegno: imposta colori, spessore delle linee e associazione dello shader prima della chiamata di disegno, poiché questo è lo stato letto dal motore. Costruisci prima, configura lo stato poi, e disegna alla fine; in questo modo il modello vettoriale si comporterà in modo prevedibile ogni volta.
Da qui, i passaggi successivi naturali consistono nella lettura di vettori e testo da un documento esistente, argomento trattato nel nostro articolo sull'estrazione di testo, immagini e font, e nel rendering dello stesso modello di disegno su un device context di Windows per l'anteprima a schermo e la stampa, descritto nella guida a stampa e anteprima. Le chiamate per tracciati, shader e motivi qui descritte sono distribuite come parte della libreria PDF per Delphi insieme alle API per testo, immagini, moduli e firme trattate in altre sezioni di questo blog.