La prima fattura che quasi ogni team renderizza con una libreria PDF esce sbagliata nello stesso modo: il testo dell'intestazione si trova lungo il bordo inferiore della pagina, e ogni riga successiva sale verso l'alto. Non è rotto nulla. Lo user space PDF, secondo ISO 32000-1 §8.3, pone l'origine nell'angolo inferiore sinistro con Y che aumenta verso l'alto, l'esatto opposto del canvas GDI su cui uno sviluppatore VCL ha disegnato per anni. HotPDF, libreria di generazione PDF losLab per Delphi e C++Builder, espone direttamente quel modello di coordinate, quindi i cinque minuti spesi ora per interiorizzarlo risparmiano una riscrittura del layout più tardi. Questo articolo percorre le primitive di output di cui un generatore di report ha davvero bisogno: testo posizionato, font che sopravvivono al deployment, posizionamento di immagini e disegno vettoriale.
Posizionamento del testo e origine in basso a sinistra
La chiamata centrale dell'oggetto pagina è TextOut(X, Y, Angle, Text). X e Y posizionano il testo in punti dall'angolo inferiore sinistro, e Angle lo ruota in gradi, che è il modo per ottenere timbri diagonali DRAFT e COPY senza ulteriore meccanica. L'idioma che mantiene valida l'intuizione formata su VCL è calcolare Y come altezza pagina meno la distanza dall'alto:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice-0001.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE'); // 50pt from top of Letter
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY'); // rotated stamp
Pdf.AddPage; // CurrentPage now points here
Pdf.CurrentPage.SetFont('Arial', [], 10); // font state does not carry over
Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Due comportamenti stateful in quel listato causano la maggior parte dei bug di pagina due. AddPage ripunta CurrentPage alla nuova pagina, quindi ogni riferimento memorizzato al precedente oggetto pagina è ormai obsoleto per il disegno. Inoltre la selezione del font è per pagina: chiama di nuovo SetFont dopo ogni AddPage, altrimenti il primo TextOut sulla nuova pagina non userà il font che pensi. Un ciclo report dovrebbe trattare "nuova pagina" e "ristabilire lo stato testo" come una sola unità.
Font che esistono sul server, non solo sul tuo desktop
I fallimenti di font sono fallimenti di deployment. La macchina di sviluppo ha installato il font corporate; l'account del servizio Windows sull'host di produzione no, e l'output sostituisce in silenzio. Il pattern difensivo è caricare i font da file controllati dal tuo installer invece di fidarsi della directory font del sistema operativo, e la chiamata di registrazione Unicode di HotPDF fa esattamente questo:
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź — Ünïcode test ✓'));
Nota che TextOut accetta direttamente un WideString, quindi i dati cliente che contengono qualunque cosa oltre la code page locale — in pratica tutti i dati cliente — passano dalla stessa chiamata dell'arredo ASCII del report, purché il font selezionato copra i glifi. I font Unicode incorporati richiedono inoltre che il documento sia PDF 1.5 o successivo, quindi tieni presente questo floor di versione se qualche altro requisito ti blocca a una versione più vecchia. Per script che richiedono shaping invece di solo lookup dei glifi, in particolare arabo ed ebraico, la pipeline dedicata right-to-left è trattata nel nostro articolo sullo shaping del testo per script complessi con HotPDF.
Nel raro caso in cui nessun file font possa rappresentare ciò che ti serve, segni tipo MICR o simboli proprietari, HotPDF supporta i font Type 3 tramite RegisterType3Font e AddType3Glyph, dove ogni glifo è un piccolo content stream definito da te. È uno strumento di nicchia, ma è meglio che distribuire simboli come centinaia di piccole immagini.
Immagini: gli argomenti centrali sono larghezza e altezza, non un angolo
HotPDF separa la registrazione dell'immagine dal posizionamento. AddImage acquisisce una volta un TBitmap o TJPEGImage — decodifica prima la grafica PNG in bitmap — e restituisce un indice; ShowImage posiziona quell'indice tutte le volte necessarie. La firma è la parte da leggere due volte:
var
Png: TPngImage;
Logo: TBitmap;
LogoIdx: Integer;
begin
Png := TPngImage.Create;
Logo := TBitmap.Create;
try
Png.LoadFromFile('brand-logo.png');
Logo.Assign(Png); // decode PNG to a bitmap
LogoIdx := Pdf.AddImage(Logo, icFlate); // lossless for flat-color art
finally
Logo.Free;
Png.Free;
end;
// (Index, X, Y, Width, Height, Angle) — not (X1, Y1, X2, Y2)
Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;
I due numeri dopo la posizione sono una larghezza e un'altezza, non l'angolo opposto, e l'ultimo argomento è un angolo di rotazione. Codice scritto assumendo X1/Y1/X2/Y2 produce loghi stirati su gran parte della pagina, un bug ovvio nell'output e sconcertante nel sorgente. Correlato: KeepImageAspectRatio è True di default, quindi un box non proporzionato aggiunge bande invece di distorcere; impostalo a False solo quando lo stretching è davvero voluto.
Anche la distinzione registrazione-contro-posizionamento conta per prestazioni e dimensione: AddImage incorpora i dati bitmap una volta, e ogni ShowImage con lo stesso indice riusa quell'unico oggetto incorporato. Un run di estratti da 500 pagine che chiama AddImage per pagina per lo stesso logo incorpora il logo 500 volte; lo stesso run che registra una volta e riusa l'indice lo incorpora una volta sola. Metti gli indici in cache in un piccolo dizionario indicizzato per percorso asset e il problema non compare mai.
La dimensione file vive qui. Il contenuto fotografico dovrebbe passare per la codifica JPEG — passa icJpeg a AddImage e imposta JpegQuality attorno a 85, dato che la proprietà è 100 di default — che resta visivamente pulita per allegati scansionati e foto a una frazione della dimensione lossless. Mantieni PNG per grafica a colori piatti come loghi e grafici, dove gli artefatti di ringing JPEG sono visibili e la compressione Flate è già efficiente. Un run di estratti che incorpora una foto per pagina con impostazioni sbagliate spedisce gigabyte; lo stesso run a JPEG 85 spedisce un decimo senza lamentele da parte degli occhi.
Linee, riquadri e ombreggiature con primitive path
Linee tabella e box totali non hanno bisogno di immagini: le primitive vettoriali producono output più nitido a qualsiasi zoom e costano quasi nulla in dimensione file. Il modello è costruzione del path seguita da un operatore di painting:
// Horizontal rule under the table header
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;
// Shaded totals box: X, Y, width, height
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;
La disciplina d'ordine è la stessa dei content stream PDF grezzi: imposta lo stato di paint, costruisci il path, poi chiama Stroke o Fill. Un path mai dipinto semplicemente svanisce, che è la spiegazione usuale quando una riga "non appare". SetRGBFillColor accetta un singolo TColor, quindi le costanti VCL — clNavy, clBlack — funzionano direttamente, e Rectangle segue la stessa convenzione larghezza-e-altezza del posizionamento immagini. Le hairline meritano una cautela: spessori sotto circa mezzo punto sono eleganti a schermo e possono sparire del tutto su una stampante da ufficio a 600 dpi, quindi 0,75 pt è un floor sensato per linee tabella che devono sopravvivere alla carta.
Paginazione su dati reali, non su dati campione
Le colonne numeriche espongono un'altra abitudine da costruire presto: allinea gli importi sul bordo destro calcolando la posizione X dal limite destro della colonna e dalla larghezza renderizzata di ogni valore, invece di imbottire le stringhe con spazi. Il padding con spazi funziona solo in font monospaziati, e i report finanziari non sono mai in font monospaziati. Formatta i valori con routine Delphi consapevoli della locale, come FormatFloat, prima di misurarli, così il separatore delle migliaia atteso dalla locale del cliente è quello di cui hai misurato la larghezza.
Il dataset demo ha dieci righe brevi; la produzione ha un cliente con un nome aziendale di 140 caratteri e un estratto con 4.000 righe. Un ciclo report robusto tiene un cursore Y che scende, sottraendo l'altezza di ogni riga, e va a nuova pagina quando il cursore attraverserebbe il margine inferiore, ricordando che "scendere" in questo sistema di coordinate significa diminuire Y. Metti la gestione del page break in un punto solo, riemetti SetFont e ridisegna l'intestazione ricorrente al suo interno, e i bug off-by-one-page spariscono. Se i report devono anche soddisfare requisiti archivistici o di accessibilità, le scelte di generazione fatte qui, font incorporati, output tagged, spazi colore, sono esattamente ciò che gli standard vincolano; vedi la guida HotPDF a PDF/A, PDF/X e PDF/UA prima che il template si cristallizzi.
FAQ
Perché il mio testo viene renderizzato in fondo alla pagina?
L'origine PDF è l'angolo inferiore sinistro con Y che cresce verso l'alto. Converti le posizioni relative all'alto con PageHeight - Offset, oppure progetta da subito il codice layout attorno all'origine in basso a sinistra.
Perché il font è sbagliato a pagina 2 ma corretto a pagina 1?
La selezione del font non si trasferisce tra pagine, e AddPage sposta CurrentPage alla nuova pagina. Chiama SetFont dopo ogni AddPage prima del primo TextOut.
Come tengo sotto controllo la dimensione file con molte foto incorporate?
Passa icJpeg a AddImage e imposta JpegQuality vicino a 85 per contenuto fotografico; riserva icFlate lossless a loghi e line art a colori piatti. Registra ogni immagine distinta una volta con AddImage e riusa l'indice.
Riferimento prodotto
Ogni chiamata di questo articolo è inclusa in HotPDF Component per Delphi e C++Builder, che documenta l'intera API di testo, font, immagini e disegno insieme alle funzionalità di form, crittografia e firma trattate altrove in questo blog.