Articolo tecnico

Creazione di PDF da Zero con PDFium Component in Delphi

PDFium ha la reputazione di un motore di visualizzazione, il renderer dietro la scheda PDF di Chrome, quindi la prima cosa da chiarire è che PDFium Component può anche costruire un documento che non è mai esistito prima. Il lato authoring avvolge l'API dell'oggetto pagina di PDFium: si crea un documento vuoto, si aggiungono pagine con dimensioni esplicite e si inseriscono testo, percorsi vettoriali (vector paths) e immagini in ogni pagina alle coordinate scelte. Non c'è un linguaggio di descrizione della pagina da imparare e nessun driver di stampa nel ciclo. Tu chiami i metodi, la libreria assembla gli oggetti PDF e SaveAs serializza il risultato

Quello che non ottieni è un motore di layout. Questo è abbastanza importante da dirlo subito, perché modella ogni esempio di seguito. PDFium Component posiziona il contenuto dove gli dici tu, in coordinate assolute e in nessun altro luogo. Non manderà a capo un paragrafo, non farà scorrere il testo attraverso un'interruzione di pagina, né calcolerà una tabella da righe e colonne. Quelli sono il tuo lavoro. Se sei arrivato qui aspettandoti qualcosa che riaggiusti la prosa come fa un word processor, calibrati ora: questa è un'API di posizionamento precisa e di basso livello, più vicina al disegno su una tela (canvas) che all'impaginazione di un documento. Per le fatture generate, i certificati, le etichette e le pagine di report in cui sai già dove appartiene ogni elemento, quella precisione è esattamente ciò che desideri

Il minimo che produce un file

Tre chiamate si interpongono tra un TPdf vuoto e un PDF salvato: crea il documento, aggiungi una pagina, scrivilo. Tutto il resto è contenuto che stratifichi nel mezzo:

uses
  Vcl.Graphics,   // per clBlack e TColor
  PDFium;         // TPdf vive qui

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // documento in memoria vuoto
    Pdf.AddPage(0, 595, 842);           // A4 verticale, in punti
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serializza su disco
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Un dettaglio fa inciampare le persone che hanno visto snippet più vecchi: non si assegna Pdf.Active := True dopo CreateDocument. La proprietà Active segnala se esiste un handle di documento, e CreateDocument ne ha già creato uno, quindi la proprietà è True nel momento in cui quella chiamata ritorna. Impostarla di nuovo è un no-op nel migliore dei casi e fuorviante per il lettore successivo nel peggiore. Active si guadagna da vivere all'uscita: l'assegnazione di False rilascia il documento sottostante prima di Free, che è il giusto ordine di smontaggio (teardown). Tratta CreateDocument e un'apertura di caricamento file (open) come mutuamente esclusivi. La libreria si rifiuta di creare un nuovo documento su un TPdf che ne ha già uno aperto, quindi riutilizzo significa chiudere prima il documento corrente

Le coordinate iniziano in basso a sinistra

La seconda coppia di argomenti di AddText, e di ogni chiamata di posizionamento, è un punto nello spazio utente del PDF. L'origine si trova nell'angolo in basso a sinistra della pagina, X corre verso destra e Y corre verso l'alto. Un'unità è un punto, 1/72 di pollice, quindi una pagina A4 è 595 per 842 unità e l'US Letter è 612 per 792. Quella Y verso l'alto è la singola fonte più comune di confusione "il mio testo è fuori dalla pagina", perché le coordinate dello schermo e delle bitmap pongono l'origine in alto con Y che cresce verso il basso. In una pagina alta 842 punti, un'intestazione vicino alla parte superiore si trova intorno a Y 780, non Y 60. Quando una stampa atterra da qualche parte inaspettata, l'altezza della pagina meno la tua Y è quasi sempre il numero che intendevi effettivamente

AddPage accetta una posizione di inserimento come suo primo argomento, espressa in base uno, con 0 come comoda abbreviazione per "inizio del documento". Passa 0 o 1 per la prima pagina e la pagina viene inserita in primo piano; passa il valore corrispondente al conteggio a cui stai accodando per aggiungerla alla fine. La pagina appena aggiunta diventa anche la pagina corrente, quella a cui mirano le chiamate di disegno successive, quindi non c'è nessun passaggio separato "seleziona questa pagina" dopo averla aggiunta. Se aggiungi diverse pagine e in seguito devi tornare a disegnare su una precedente, imposta PageNumber per spostare il cursore; mentre riempi le pagine in ordine man mano che le crei, puoi lasciarlo stare

Scrittura di testo e la regola dei font che colpisce silenziosamente

La firma di AddText porta tutto ciò di cui una singola stringa ha bisogno: la stringa, il nome di un font, una dimensione in punti, l'ancora X e Y, quindi il colore opzionale, un byte alfa per la trasparenza e un angolo di rotazione in gradi:

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Titolo in nero, opacità predefinita, nessuna rotazione
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // Una firma (byline) più chiara 24 punti sotto di esso
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // Un debole timbro diagonale di bozza (draft) attraverso la pagina
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Il byte alfa va da $00 (invisibile) a $FF (opaco), che è ciò che rende il timbro di bozza una filigrana (watermark) piuttosto che un blocco solido: $30 è all'incirca il diciannove percento di opacità, sufficiente per leggere attraverso di esso. L'angolo ruota la stringa in senso antiorario attorno alla sua ancora, quindi 45 gradi danno il classico timbro da angolo ad angolo. Niente di tutto questo richiede una funzione filigrana separata. Una filigrana è solo una chiamata AddText grande, semi-trasparente e ruotata, e disegnarla prima o dopo il corpo decide se si trova dietro o sopra il contenuto

I font meritano una frase attenta, perché la modalità di guasto è silenziosa. Quando passi il nome di un font, PDFium Component chiede al sistema operativo i dati TrueType di quel font e li incorpora nel documento, motivo per cui un file creato sulla tua macchina viene renderizzato in modo identico su una che non ha mai installato il font. Il problema è cosa succede quando il nome non si risolve: un errore di battitura o un carattere che semplicemente non è presente sulla macchina di compilazione. Non ci sono eccezioni. La libreria ripiega sulla creazione di un oggetto di testo che porta il nome solo come etichetta, senza nulla di incorporato, e lascia al visualizzatore il compito di sostituire tutto ciò che considera simile. Il testo appare nei tuoi test, sembra plausibile e cambia metrica o glifi nel momento in cui il file si apre da qualche parte in cui sono installati font diversi. Usa nomi che sai essere presenti sulla macchina di generazione, tratta l'elenco dei font come una dipendenza di distribuzione e apri un campione in un visualizzatore su un sistema pulito prima di fidarti dell'output

Forme vettoriali: costruisci un percorso, poi applicalo

Linee, rettangoli e regioni riempite passano attraverso un percorso (path). Ne apri uno con CreatePath, che imposta il punto di inizio e tutto lo stile in una volta sola: modalità di riempimento, colori di riempimento e tratto (stroke) con i loro rispettivi byte alfa, larghezza del tratto, i terminali delle linee (line caps) e le giunzioni (joins). Quindi lo estendi con LineTo, BezierTo e ClosePath, e infine AddPath applica il percorso finito sulla pagina. Il passaggio di applicazione è facile da dimenticare e non produce nulla se lo salti:

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // Una riga orizzontale sottile. L'overload del rettangolo imposta un riquadro direttamente:
  // X, Y, Larghezza, Altezza, poi la modalità di riempimento e i colori.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Overload dei punti: inizia al primo vertice, linea verso il resto, chiudi.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nulla viene disegnato finché questo non viene eseguito
end;

Due overload coprono i casi comuni. La forma a quattro coordinate prende X, Y, larghezza e altezza e ti dà un rettangolo allineato agli assi in una singola chiamata, che è quello a cui ricorrere per disegnare un righello, il bordo di una cella o un pannello di sfondo riempito. La forma a due coordinate imposta solo un punto di inizio e tu tracci il resto del contorno stesso con LineTo e BezierTo. La modalità di riempimento (fill mode) controlla come vengono dipinte le regioni sovrapposte: fmWinding (nonzero winding) si adatta alla maggior parte delle forme solide, fmAlternate (even-odd) gestisce ritagli e contorni auto-intersecanti e fmNone lascia un percorso solo delineato senza riempimento, che è quello che usa il divisore sopra

Le tabelle sono percorsi e testo, assemblate a mano

Poiché non c'è una primitiva di tabella, una tabella è un ciclo. Decidi gli offset X delle colonne e l'altezza delle righe, scrivi ogni cella con AddText e disegni le regole con percorsi di rettangoli. L'aritmetica è tua, ma è semplice e, una volta scritta, si generalizza a qualsiasi griglia di cui hai bisogno:

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // offset colonna
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Riga intestazione
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Riga sotto l'intestazione
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Righe dati, con Y in calo ad ogni iterazione
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

Nota che la Y diminuisce dell'altezza della riga a ogni passaggio, di nuovo perché l'alto è positivo. Questo è anche il punto in cui si manifesta l'assenza della misurazione del testo: nulla impedisce a un nome di elemento lungo di debordare nella colonna successiva, perché la libreria non sa quanto in larghezza sia stata resa la tua stringa. Per output in formato fisso in cui controlli i dati, dimensioni le colonne generosamente e vai avanti. Per i contenuti veramente variabili, limiti gli input o misuri le larghezze dei glifi tu stesso prima di posizionarli, che è il punto in cui una libreria di composizione dedicata inizia a ripagarsi da sola

Immagini e pagine multiple

Il contenuto raster arriva tramite gli helper di immagine. AddPicture accetta una TPicture caricata e la posiziona in un punto, con una larghezza e un'altezza opzionali per scalarla; AddImage accetta un percorso di file o una TBitmap direttamente, e AddJpegImage fa lo stream dei byte JPEG senza passare attraverso una bitmap. Come con tutto il resto, le coordinate di posizionamento sono l'angolo in basso a sinistra dell'immagine nello spazio utente, e la larghezza e l'altezza sono la dimensione della pagina in punti, non le dimensioni in pixel dell'origine

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // accoda; la nuova pagina diventa corrente
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // piè di pagina (footer) vicino al bordo inferiore
      // ... disegna il corpo di questa pagina qui ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Un documento multipagina è il pattern a pagina singola in un ciclo. Ogni AddPage accoda una pagina e la rende corrente, quindi il corpo e il piè di pagina che disegni in seguito atterrano sulla pagina che hai appena aggiunto. Non si riassegna PageNumber all'interno di questo ciclo, perché l'aggiunta di una pagina ha già spostato il cursore lì; hai bisogno di PageNumber solo quando torni a una pagina fuori dall'ordine di creazione. Chiama SaveAs una volta alla fine, dopo che l'ultima pagina è stata riempita. Se hai bisogno di un profilo di archiviazione piuttosto che di un file semplice, lo stesso oggetto del documento espone SaveAsPdfA e le altre varianti di conformità, quindi la scelta dello standard di output è una chiamata di salvataggio diversa, non un percorso di compilazione diverso

Dove questo si adatta

L'inquadramento onesto è che l'API di authoring di PDFium Component è uno strato fedele e sottile sopra il modello a pagina-oggetto di PDFium: creazione di documenti reali, font incorporati reali, vero contenuto vettoriale e raster, serializzati in un file conforme agli standard. Non è, e non pretende di essere, un motore di documenti impaginabili (reflowing). La linea di demarcazione è il layout del testo. Se il tuo output è basato su template: fatture, certificati, etichette, dashboard resi in una griglia fissa, il modello a coordinate assolute è diretto e veloce e il codice rimane leggibile. Se il tuo output è una lunga prosa che deve andare a capo e impaginarsi da sola, dovrai ricostruire un motore di layout in cima a queste chiamate, e questo è lo strumento sbagliato per quel lavoro. Sapere da quale lato di quella linea ti trovi costituisce la maggior parte della decisione

I metodi di creazione descritti qui fanno parte di PDFium Component per Delphi, che abbina questo percorso di creazione alle funzioni di rendering e di estrazione del testo per cui PDFium è maggiormente conosciuto