La chiamata che inserisce il testo in una pagina PDF è lineare. Fornisci ad AddText una stringa, un font, una dimensione e una posizione, e i glifi appaiono. Quello che non fa è dirti quanto sarà larga quella stringa una volta disegnata e non spezza una stringa lunga su più righe. Una singola chiamata dipinge un'esecuzione (run) di testo in una posizione. Se la sequenza è più larga della colonna in cui doveva adattarsi, supera semplicemente il bordo e nulla nella chiamata di disegno ti avvisa. Nel momento in cui desideri un paragrafo piuttosto che una singola etichetta, il pezzo mancante è la larghezza di una stringa nel font e nella dimensione scelti, misurata prima di confermarla sulla pagina
Questo è il classico problema di layout. Per mandare a capo un paragrafo in una colonna, devi sapere, parola per parola, quanto spazio orizzontale occuperà ogni riga candidata e devi saperlo prima di disegnare qualsiasi cosa. Il word wrap (a capo automatico) è un ciclo di misurazione avvolto attorno a una chiamata di disegno e un binding che si limita a disegnare ti fornisce la seconda metà. Il supporto per la misurazione del testo nel componente PDFium colma tale lacuna con due funzioni, MeasureText e MeasureTextWidth, che riportano l'estensione renderizzata di una stringa senza apporre alcun segno su nessuna pagina
Perché la misurazione è un class helper, non un nuovo metodo su TPdf
Il supporto per la misurazione arriva come class helper Delphi per TPdf, risiedendo in una propria unità, piuttosto che come nuovi metodi integrati nella classe TPdf. Un class helper è una caratteristica del linguaggio che consente di associare metodi a un tipo esistente dall'esterno della sua dichiarazione. Una volta che l'unità è in scope, i nuovi metodi vengono chiamati esattamente come se appartenessero alla classe, quindi un metodo helper si legge come Pdf.MeasureTextWidth(...) senza che vi sia alcun oggetto separato da costruire o passare
Il motivo per strutturarlo in questo modo è la separazione. Il tipo TPdf principale rimane com'è, nessun campo viene aggiunto e nessuna firma esistente viene toccata, cosicché un progetto che non ha mai bisogno di layout non porta mai con sé il codice di misurazione. Un progetto che ne ha bisogno aggiunge un'unità a una clausola uses e i metodi si accendono. La funzionalità diventa facoltativa (opt-in) con la granularità di una singola unità, che è il modo più pulito per estendere un tipo che non possiedi o che non vuoi disturbare
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // the helper unit; brings MeasureText into scope on TPdf
// With the unit in scope the methods read as members of TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W and H are now the rendered width and height in PDF user units
end;
Misurare senza toccare la pagina
La misurazione deve essere priva di effetti collaterali. Deve riportare una larghezza senza lasciare nulla dietro di sé, perché la chiami molte volte mentre decidi un layout e la pagina deve apparire esattamente come se non avessi mai misurato nulla. La tecnica che lo rende possibile consiste nel costruire un oggetto di testo, chiedergli le sue dimensioni e buttarlo via prima che venga mai collegato a una pagina
La sequenza si compone di quattro chiamate PDFium. FPDFPageObj_NewTextObj crea un oggetto di testo nel documento, fornendo il nome del font e la dimensione. FPDFText_SetText imposta la stringa che l'oggetto trasporta. FPDFPageObj_GetBounds rilegge il riquadro di delimitazione (bounding box) dell'oggetto. FPDFPageObj_Destroy libera l'oggetto. Fatto cruciale, nulla in tale sequenza chiama l'API di inserimento della pagina. L'oggetto viene creato, interrogato e distrutto in modo isolato, così il documento è invariato quando la funzione ritorna. È una sonda usa e getta il cui unico output sono i quattro numeri del suo bounding box
Questo è il modo robusto per farlo perché PDFium non espone una comoda larghezza di avanzamento per glifo che potresti sommare da solo. Le metriche dei glifi dipendono dal programma del font, dalla codifica e da come PDFium carica il font face, e non esiste alcuna chiamata pubblica che ti consegni l'avanzamento di ogni carattere in una stringa. Il riquadro di delimitazione di un vero oggetto di testo, d'altra parte, è calcolato dagli stessi meccanismi che impaginerebbero i glifi per il disegno, quindi riflette l'effettiva estensione renderizzata piuttosto che un'approssimazione. Costruire un singolo oggetto monouso e leggerne i limiti è la misurazione più affidabile che la libreria possa fornire
// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
FontSize: Single; out Width, Height: Double);
var
TextObject: FPDF_PAGEOBJECT;
L, B, R, T: Single;
begin
Width := 0;
Height := 0;
if Self.Document = nil then
Exit;
TextObject := FPDFPageObj_NewTextObj(Self.Document,
FPDF_BYTESTRING(AnsiString(Font)), FontSize);
if TextObject = nil then
Exit;
try
if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
Exit;
if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
begin
Width := R - L;
Height := T - B;
end;
finally
FPDFPageObj_Destroy(TextObject); // probe discarded, page untouched
end;
end;
Coordinate e unità del risultato
Il bounding box restituisce quattro bordi, sinistro, inferiore, destro e superiore (left, bottom, right, top), e le due dimensioni derivano per sottrazione. La larghezza è la destra meno la sinistra e l'altezza è la parte superiore meno la parte inferiore. Entrambe sono espresse in unità utente (user units) PDF, dove un'unità è un settantaduesimo di pollice, lo stesso spazio di coordinate in cui posizioni il testo sulla pagina. Non vi è alcuna unità di dispositivo nascosta né alcun pixel coinvolto in questa fase. Una larghezza di 36 significa mezzo pollice di pagina, qualunque sia l'eventuale risoluzione di rendering
L'asse verticale corre nel modo definito dal PDF, con Y che aumenta verso l'alto, motivo per cui l'altezza è la parte superiore meno la parte inferiore piuttosto che il contrario. Tale dettaglio conta quando si avanza un cursore lungo una colonna. Si misura l'altezza di una linea, quindi la si sottrae dalla linea di base (baseline) corrente per trovare la successiva, perché spostarsi lungo la pagina significa spostarsi verso una Y più piccola. Se la destinazione è uno schermo piuttosto che la carta, si convertono le unità utente in pixel del dispositivo con la risoluzione dello schermo: un valore in unità utente moltiplicato per i DPI e diviso per 72 fornisce i pixel, quindi una larghezza di colonna impostata in punti (points) può essere confrontata con una sequenza misurata prima di decidere dove va l'interruzione
Cosa succede con input degenerato
Le funzioni sono scritte per fallire silenziosamente (fail quietly). Se non c'è alcun documento aperto o se l'oggetto di testo non può essere creato, il risultato è un'estensione zero anziché un'eccezione sollevata. Larghezza e altezza vengono inizializzate a zero in cima e sovrascritte solo una volta che un riquadro di delimitazione è stato riletto con successo. Una stringa vuota, un documento mancante, un font che la libreria non riesce a risolvere in un oggetto: ciascuno di questi restituisce zero invece di lanciare un'eccezione
Tale scelta mantiene semplice un ciclo di misurazione, perché un ciclo che corre su migliaia di parole non è il posto per la gestione delle eccezioni a ogni iterazione. Il costo è che il chiamante porta il peso del controllo. Una larghezza pari a zero è una sentinella, non un fatto riguardante il testo, perciò il codice che divide per una larghezza misurata o assume un valore positivo deve difendersi dallo zero prima di fidarsi. Tratta lo zero come "impossibile misurare" e il contratto è chiaro; ignoralo e un input degenerato diventerà silenziosamente un layout con una colonna di glifi sovrapposti
Un greedy word wrap basato sulla misurazione
Avendo a disposizione una funzione di larghezza, l'andata a capo automatica è un breve ciclo "greedy" (avido). Si divide il paragrafo in parole, si mantiene una riga corrente e per ogni parola si misura come sarebbe la riga se vi si aggiungesse tale parola. Finché la riga di prova si adatta ancora alla larghezza della colonna si continua ad aggiungere; quando trabocca, si invia alla pagina la riga corrente con AddText e se ne avvia una nuova con la parola che non ci stava. L'accumulo viene eseguito interamente con MeasureTextWidth, e l'unica cosa che raggiunge la pagina è una riga per cui hai già confermato l'adattamento
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<WideString>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := WideString(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// Measure the candidate line before drawing anything.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the line that fit
Y := Y - LineHeight; // Y decreases going down
Line := Words[I]; // overflowing word starts next line
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the final line
end;
Il ciclo misura la riga di prova piuttosto che misurare ogni parola e sommare, perché la larghezza di una riga non è la somma delle larghezze delle sue parole. Gli spazi tra le parole contribuiscono e una sequenza misurata lo cattura direttamente. La regola greedy, adattare quante più parole lo consente la colonna e interrompersi all'ultima che si adatta, è la stessa regola che colma il divario tra un nudo AddText e un vero paragrafo. La chiamata di disegno non è mai stata la parte difficile. La misurazione che deve precederla lo è, ed è esattamente ciò che fornisce l'helper
Dove si inserisce
La misurazione è lo strato tra la generazione del contenuto e il suo rendering, quindi si sposa naturalmente con il resto di un flusso di lavoro documentale da zero (from-scratch). Se stai assemblando pagine e posizionando testo fin dall'inizio, il lavoro di base si trova in creare documenti PDF da zero con il componente PDFium in Delphi, dove AddText e l'impostazione della pagina sono trattati per intero. Quando il font che stai misurando conta tanto quanto la stringa, perché le metriche dipendono dal face, analizzare le proprietà dei font PDF con il componente PDFium in Delphi mostra come la libreria riporti le informazioni sul font che guidano quei bounding box. Entrambi si basano sullo stesso binding, il PDFium Component per Delphi e Lazarus, dove l'helper di misurazione viene fornito insieme alle API di documento, pagina e testo descritte in tutto questo blog