Articolo tecnico

Disegno vettoriale su canvas PDF in Delphi con HotPDF

HotPDF disegna grafica vettoriale costruendo un percorso sulla pagina corrente e chiedendo poi che venga dipinto. Non c'è alcun passaggio bitmap nel mezzo. Una linea tracciata con MoveTo e LineTo finisce come operatore di percorso PDF nel content stream, quindi resta un vero vettore: nitida al 50% di zoom, nitida al 1600% e con un ingombro pari a una frazione di quello che costerebbe una versione rasterizzata. Per diagrammi, filetti di tabella, assi di grafici e decorazioni dei moduli è esattamente ciò che serve, e l'API che c'è dietro è abbastanza piccola da imparare in una sola sessione

L'intera superficie di disegno vive su THotPDF.CurrentPage. Fra BeginDoc ed EndDoc impostate colore e spessore della linea su quell'oggetto pagina, posate la geometria e chiamate un operatore di disegno per confermarla. Le quattro primitive che userete di più sono MoveTo e LineTo per i percorsi arbitrari, Rectangle per i riquadri, Circle per i dischi, e i due operatori di disegno Stroke e Fill

Il sistema di coordinate parte in basso a sinistra

Questa è la cosa che fa inciampare chiunque arrivi dalla VCL. Il TCanvas con cui dipingete i controlli mette l'origine nell'angolo in alto a sinistra con la Y che cresce verso il basso. Il PDF fa l'opposto. HotPDF misura dall'angolo in basso a sinistra della pagina in punti (1/72 di pollice), con la Y che cresce salendo. Un punto a Y := 720 si trova vicino alla sommità di una pagina US Letter, alta 792 punti, mentre Y := 50 sta vicino al fondo. Se il vostro primo disegno esce specchiato in verticale, il motivo è questo: codice portato dalla grafica a schermo presuppone la direzione sbagliata ed esce dal bordo inferiore

La stessa convenzione governa TextOut, quindi testo e forme condividono un unico modello mentale una volta che lo avete interiorizzato. Progettate un layout decidendo dove sta il fondo di ciascun elemento, non la sommità, e il resto segue

Confronto fra il canvas a schermo con origine in alto a sinistra e il PDF con origine in basso a sinistra: lo stesso punto vicino alla sommità di una pagina US Letter vale Y = 72 nel codice TCanvas ma Y = 720 nei punti di HotPDF, quindi il codice portato senza invertire la Y disegna specchiato
HotPDF misura dall'angolo in basso a sinistra in punti, così un punto vicino alla sommità della pagina da 612 per 792 porta Y = 720, la stessa posizione fisica che il codice TCanvas indirizza con una piccola Y verso il basso

Percorsi: MoveTo, LineTo, Stroke

Un percorso tracciato è una penna sollevata, appoggiata e trascinata. MoveTo solleva la penna e fissa il punto di partenza senza segnare nulla. Ogni LineTo estende il percorso corrente fino a un nuovo punto. Sulla pagina non compare nulla finché non chiamate Stroke, che disegna il percorso accumulato usando il colore di tratto e lo spessore correnti, poi azzera il percorso così che il MoveTo successivo riparta da capo

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // Lo spessore è in punti e vale finché non lo cambiate.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // Una riga orizzontale in alto nella pagina (Y misurata dal basso).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // conferma il percorso; prima non si vedeva nulla

    // Una polilinea più spessa: tre segmenti in un solo percorso.
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 640);
    Pdf.CurrentPage.LineTo(172, 690);
    Pdf.CurrentPage.LineTo(272, 620);
    Pdf.CurrentPage.LineTo(372, 680);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Due dettagli fanno risparmiare parecchio tempo di debug. Lo spessore della linea è stato, non argomento: SetLineWidth lo imposta una volta e ogni Stroke successivo usa quel valore finché non lo cambiate di nuovo, ed è per questo che la polilinea qui sopra è più spessa del filetto. E il percorso si azzera dopo ogni Stroke, quindi uno Stroke dimenticato significa che la geometria posata con tanta cura non viene disegnata affatto. Se una forma manca dall'uscita, la chiamata di disegno è il primo posto in cui guardare

Le coordinate sono in punti, e i punti sono frazionari. MoveTo e LineTo accettano valori Single, quindi un filo di 0.5 punti o una posizione a 72.25 sono legittimi e significativi, non arrotondati via all'unità intera più vicina. Quella precisione conta in due direzioni opposte. Uno spessore inferiore a circa 0.5 può essere disegnato come la linea più sottile possibile dipendente dal dispositivo, che sparisce a schermo e riappare in stampa, perciò un filetto visibile vuole uno spessore scelto di proposito invece del valore predefinito. All'estremo opposto, agganciare filetti di tabella e linee di griglia a coordinate intere evita che una griglia fitta sembri leggermente irregolare dove linee adiacenti arrotondano in modo diverso. Decidete in anticipo il passo della griglia in punti e il resto dell'impaginazione lo eredita

Forme riempite e colore

Le primitive chiuse possono essere riempite invece che contornate. Rectangle riceve una posizione e una dimensione, Circle riceve un centro e un raggio, e l'una o l'altra si confermano con Fill, che dipinge l'interno nel colore di riempimento corrente, oppure con Stroke per il solo contorno. Colore di riempimento e colore di tratto sono due pezzi di stato distinti, impostati con SetRGBFillColor e SetRGBStrokeColor, che ricevono entrambi un singolo TColor. Ciò significa che potete riusare direttamente le costanti di colore di Delphi e la funzione RGB

Modello dei percorsi di HotPDF: MoveTo, LineTo, Rectangle e Circle costruiscono in memoria un percorso corrente invisibile, e solo la conferma con Stroke, Fill o FillAndStroke lo dipinge usando lo stato grafico persistente di colore di tratto, colore di riempimento e spessore prima di svuotare il buffer
La geometria si accumula in silenzio nel percorso corrente finché un operatore di disegno non la conferma con il colore di tratto, il colore di riempimento e lo spessore memorizzati: una chiamata di disegno dimenticata lascia la forma invisibile
// Rectangle(X, Y, Width, Height): X e Y sono l'angolo in basso a sinistra.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X e Y sono il centro.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Solo contorno: impostate colore e spessore del tratto, poi Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Attenzione alla forma degli argomenti di Rectangle. È posizione più dimensione, X, Y, Width, Height, non due angoli opposti. Il TCanvas.Rectangle che gli sviluppatori Delphi conoscono riceve (Left, Top, Right, Bottom), quindi la memoria muscolare passerà a HotPDF un secondo angolo dove esso si aspetta una larghezza e un'altezza, e il riquadro esce della misura sbagliata. La coppia (X, Y) è l'angolo in basso a sinistra, coerente con l'origine della pagina. Per un cerchio, (X, Y) è il centro e il terzo argomento è il raggio in punti

Una scelta di colore che l'esempio originale sbagliava

Una versione più vecchia di questo esempio inizializzava i colori con Random($FFFFFF) su ogni forma. Sembra vivace, ed è l'istinto sbagliato per i documenti generati. Un PDF costruito da codice è di solito qualcosa che volete anche testare, e i colori di riempimento casuali rendono impossibile confrontare l'uscita fra una esecuzione e l'altra: un diff byte per byte contro un file di riferimento fallisce ogni volta, senza motivo reale. Scegliete colori espliciti. Quando volete varietà lungo una serie di forme, pilotatela dai vostri dati o da un array di palette fissa, così lo stesso ingresso produce sempre lo stesso file. Il determinismo vale più della novità quando l'artefatto attraversa una catena di rilascio

Mettere insieme le primitive: un riquadro di richiamo

Ogni primitiva è semplice presa da sola; il guadagno si vede quando una manciata di esse compone qualcosa di cui un rapporto ha davvero bisogno. Un richiamo, il riquadro annotato che punta a una figura e la spiega, usa tutto ciò che abbiamo visto finora: un rettangolo riempito con bordo, una linea di puntamento tracciata, un punto che ancora il puntatore e del testo disposto dentro il riquadro con le stesse coordinate dal basso a sinistra usate dalle forme. FillAndStroke si guadagna qui il suo posto, dipingendo interno e contorno di un solo percorso in una sola conferma invece di costruire il rettangolo due volte

Anatomia di un riquadro di richiamo assemblato con le primitive HotPDF: un rettangolo confermato con FillAndStroke, una linea di puntamento tracciata con MoveTo e LineTo, un punto di ancoraggio riempito e barre di etichetta TextOut che condividono la stessa griglia in basso a sinistra delle forme
Quattro conferme costruiscono il richiamo: FillAndStroke dipinge insieme pannello e bordo, puntatore e punto riusano lo stato di tratto e riempimento, e ogni scostamento delle etichette è semplice aritmetica rispetto all'angolo del riquadro (90, 600)
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. Il riquadro: riempimento chiaro più bordo visibile, un percorso, una conferma.
    //    Rectangle vuole angolo in basso a sinistra più dimensioni, Y misurata dal basso
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // pannello ambra chiaro
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // bordo più scuro
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. Il puntatore: un segmento tracciato dal bordo del riquadro verso il basso
    //    in direzione della cosa che state annotando
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // bordo sinistro del riquadro
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. Un punto riempito ancora il puntatore al suo bersaglio
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. L'etichetta, posizionata rispetto all'angolo in basso a sinistra del riquadro.
    //    Testo e forme condividono un solo sistema di coordinate, quindi gli
    //    scostamenti sono semplice aritmetica rispetto a (90, 600)
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
    Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
    Pdf.CurrentPage.SetFont('Arial', [], 9);
    Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
    Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Notate quanta poca gestione di stato richieda il composito. Colore di riempimento, colore di tratto e spessore vengono impostati subito prima della forma che li usa, così ogni blocco del disegno si legge come una unità autonoma e può essere riordinato o estratto in una procedura di supporto senza trascinarsi dietro stato nascosto. Racchiudete tutto questo in una procedura che riceve il punto di ancoraggio e le stringhe, e avete una annotazione di diagramma riutilizzabile al costo di quaranta righe

Dove il disegno vettoriale rende, e dove no

Ricorrete a queste chiamate di percorso e forma quando la geometria è generata: linee di griglia e barre dei grafici, i filetti di una tabella di fattura, riquadri di richiamo su un diagramma, un marchio espresso come una manciata di percorsi. Tutto ciò scala senza sfocature e aggiunge quasi nulla alla dimensione del file, perché un rettangolo è qualche numero anziché migliaia di pixel. Anche il rovescio della medaglia è onesto. Se ciò che avete davvero è una fotografia o una schermata, disegnatela invece come immagine con AddImage e ShowImage; ricalcare una bitmap con chiamate vettoriali non vi porta nulla. I segmenti rettilinei, i rettangoli e i cerchi visti sopra reggono la larga maggioranza del lavoro reale di reportistica, e i tre affinamenti che gli sviluppatori chiedono subito dopo, curve, tratteggi e trasparenza, stanno sullo stesso oggetto pagina

Curve, tratteggi e trasparenza in breve

Le curve libere estendono la stessa macchina dei percorsi che avete già. CurveToC(X1, Y1, X2, Y2, X3, Y3) aggiunge un segmento di Bezier cubica dal punto corrente a (X3, Y3), piegando verso i due punti di controllo, e le varianti abbreviate CurveToV e CurveToY coprono i casi in cui un punto di controllo coincide con un estremo. Un percorso può mescolare liberamente segmenti LineTo e CurveToC prima che un solo Stroke o Fill lo confermi, ed è così che si costruiscono angoli arrotondati e linee di grafico morbide

I tratti tratteggiati sono stato, esattamente come lo spessore. SetDash([3, 3], 0) commuta ogni tratto successivo su uno schema di tre punti pieni e tre vuoti, con l'array che indica in punti le lunghezze dei tratti attivi e inattivi e il secondo argomento che sfasa il punto di inizio del ciclo; NoDash riporta la penna alla linea continua. Impostatelo, tracciate le linee di griglia che lo vogliono e riportatelo indietro prima del filetto continuo successivo, altrimenti il tratteggio contagia in silenzio tutto ciò che segue

La trasparenza passa per uno stato grafico con nome anziché per un argomento di colore, perché nel PDF l'alfa è una proprietà del dizionario di stato grafico. Registratene uno sul documento con RegisterExtGState, passando un alfa di riempimento e un alfa di tratto fra 0 e 1, poi applicate il nome restituito con CurrentPage.SetGraphicsState; da quel punto riempimenti e tratti dipingono con l'opacità registrata. È una cerimonia più pesante rispetto ai setter di colore, e ne vale la pena la prima volta che una barra di evidenziazione deve stare sopra il testo senza nasconderlo

L'abitudine restante che vale la pena tenere è la verifica. La geometria generata può passare sulla vostra macchina e fallire su quella di un cliente, di solito per una sostituzione di font in un testo mescolato al disegno o per una assunzione sul formato pagina che non regge. Aprite il file finito a qualche livello di zoom per confermare che i bordi restino puliti, e controllate che ogni forma cada dentro il riquadro dei margini che avevate previsto. Con uno schema di colori deterministico, quel controllo si può automatizzare contro un PDF di riferimento invece di farlo a occhio

Le chiamate MoveTo, LineTo, Stroke, Fill e quelle di colore mostrate qui fanno parte del HotPDF Delphi Component per Delphi e C++Builder