Le coordinate PDF sono in punti, le coordinate della stampante sono in unità del dispositivo e le due non hanno nulla in comune fino a quando non le si converte volontariamente. Questa discrepanza è alla base della maggior parte degli output di stampa difettosi nelle applicazioni Delphi: il codice invia il file corretto, ma la pagina risulta tagliata, allungata o bianca. Il componente PDFium gestisce l'aspetto del rendering in modo pulito; il meccanismo di stampa è standard VCL. I due si integrano con una modesta quantità di codice una volta compreso ciò che ciascuna parte si aspetta
Come funziona la pipeline di rendering-stampa
Il PDFium Component non comunica direttamente con le stampanti. Il pattern è questo: renderizza una pagina su un oggetto TBitmap alla risoluzione desiderata, dopodiché trasferisci quella bitmap sul canvas della stampante con StretchDIBits. Il metodo TPdf.RenderPage restituisce una bitmap di proprietà del chiamante, dandoti così il controllo sulle dimensioni in pixel. Passando [rePrinting] nell'insieme di opzioni, PDFium modifica il proprio percorso di rendering rimuovendo effetti per il solo schermo, quali il subpixel hinting per gli LCD, e gestendo correttamente il MediaBox della pagina per l'output di stampa. Omettendo rePrinting, ciò che si manda in stampa sarà un rendering su schermo, che appare nitido su monitor ma produce solitamente un output meno definito sulle stampanti ad alti DPI in quanto le scelte di hinting operate per schermi a 96 DPI non si adattano affatto alla stampa a 300 o 600 DPI
La proprietà TPdf.Active costituisce il solo parametro di sicurezza da verificare prima di alterare alcuna delle proprietà della pagina. Il componente assorbe gli errori di caricamento senza segnalarli: assegnare Active := True in corrispondenza ad un file guasto o blindato da password non innescherà alcuna eccezione; manterrà piuttosto Active in stato False. Controllalo sempre a seguito dell'assegnazione. Leggere il conteggio PageCount o il valore di larghezza PageWidth su un documento inattivo restituisce zero, generando risultati vuoti del tutto invisibili e molto complessi da debuggare nel momento in cui raggiungono lo spooler
Un semplice ciclo di stampa minimale
Il caso funzionante più semplice carica un file, apre un processo di stampa, itera le pagine e si chiude. L'unico dettaglio insidioso è che Printer.NewPage non deve essere chiamato prima della prima pagina, da qui l'utilizzo del flag FirstPage. Il trasferimento StretchDIBits passa attraverso GetDIBSizes e GetDIB per estrarre i bit indipendenti dal dispositivo dall'handle della bitmap, per poi dipingerli sul canvas della stampante all'intera dimensione della pagina:
procedure PrintPdfFile(const FileName: string);
var
Pdf: TPdf;
I: Integer;
Bitmap: TBitmap;
InfoHeaderSize, ImageSize: DWORD;
InfoHeader: PBitmapInfo;
Image: Pointer;
FirstPage: Boolean;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
if not Pdf.Active then
Exit; // load failed silently; bail out
Printer.Title := Pdf.Title;
Printer.BeginDoc;
try
FirstPage := True;
for I := 1 to Pdf.PageCount do
begin
if FirstPage then
FirstPage := False
else
Printer.NewPage;
Pdf.PageNumber := I;
// Render at printer resolution; rePrinting adjusts the render path
Bitmap := Pdf.RenderPage(
0, 0,
Printer.PageWidth,
Printer.PageHeight,
ro0,
[rePrinting]
);
try
GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
InfoHeader := AllocMem(InfoHeaderSize);
try
Image := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
StretchDIBits(
Printer.Canvas.Handle,
0, 0, Printer.PageWidth, Printer.PageHeight,
0, 0, Bitmap.Width, Bitmap.Height,
Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
);
finally
FreeMem(Image);
end;
finally
FreeMem(InfoHeader);
end;
finally
Bitmap.Free;
end;
end;
finally
Printer.EndDoc;
end;
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Passare Printer.PageWidth e Printer.PageHeight come dimensioni della bitmap significa renderizzare alla dimensione nativa in pixel della stampante, che tiene già conto dei DPI del dispositivo. La chiamata StretchDIBits mappa quindi quei pixel in rapporto 1:1 sulla pagina. Questo offre la massima fedeltà ottenibile senza esplicita aritmetica dei DPI, ma funziona solo quando la pagina PDF e il foglio di carta fisico risultano essere della stessa dimensione. Quando differiscono, è necessario un ridimensionamento esplicito
Scalatura quando le dimensioni della pagina e della carta differiscono
Una pagina PDF in formato A4 verticale non si adatta automaticamente a una stampante US Letter, e una pagina orizzontale inviata a una stampante orientata verticalmente verrà tagliata. L'approccio standard consiste nel calcolare un fattore di scala uniforme basato sul rapporto tra i pixel della stampante e i punti del PDF, per poi applicarlo a entrambe le dimensioni in modo da preservare le proporzioni. Pdf.PageWidth e Pdf.PageHeight espongono le dimensioni correnti della pagina in punti, dove un punto corrisponde a 1/72 di pollice. Moltiplicando per il DPI di destinazione e dividendo per 72 si converte la misura in pixel a quella risoluzione. Prendi il valore minimo (Min) tra il rapporto X e Y per ottenere la scala massima che riesca comunque a rientrare nell'area stampabile:
// Fit PDF page to printable area, preserving aspect ratio
var
ScaleX, ScaleY, Scale: Double;
DestWidth, DestHeight: Integer;
Dpi: Integer;
begin
Dpi := 300; // target render resolution
Pdf.PageNumber := PageIndex;
ScaleX := Printer.PageWidth / (Pdf.PageWidth * Dpi / 72);
ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
Scale := Min(ScaleX, ScaleY);
// Clamp to 1.0 for shrink-to-fit only (no enlargement)
if Scale > 1.0 then Scale := 1.0;
DestWidth := Round(Pdf.PageWidth * Dpi / 72 * Scale);
DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);
Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
[rePrinting, reAnnotations]);
// ... transfer with StretchDIBits as above
end;
Il rendering a Dpi = 300 è adatto alla maggior parte delle stampanti da ufficio. A 600 DPI, la bitmap per una singola pagina A4 raggiunge all'incirca i 34 megapixel, il che equivale a circa 100 MB per una bitmap a 32 bit; il guadagno in qualità per i comuni documenti di testo è minimo mentre il costo in memoria per pagina diventa significativo. Mantieni i 600 DPI per i centri stampa o i disegni tecnici densi di grafica vettoriale dove fa veramente la differenza
Il flag reAnnotations nel secondo blocco di codice è indipendente da rePrinting. Includilo quando l'utente si aspetta che timbri, evidenziazioni e caselle di commento compaiano su carta. Omettilo per output di solo contenuto. Entrambi i flag possono essere combinati liberamente
Rotazione della pagina
PDFium memorizza la rotazione della pagina nel PDF sotto forma di voce /Rotate, accessibile tramite Pdf.PageRotation, la quale restituisce un valore TRotation (ro0, ro90, ro180, ro270). Il sistema di coordinate della stampante inverte le rotazioni di 90 e 270 gradi rispetto allo schermo. Se passi il valore grezzo di PageRotation direttamente a RenderPage senza alcun adattamento, le pagine orizzontali incorporate in un documento verticale verranno stampate capovolte sulla maggior parte dei driver di stampa per Windows. La soluzione è un semplice scambio prima della chiamata di rendering: mappa ro90 in ro270 e viceversa, lasciando inalterati ro0 e ro180
Verifica questo comportamento sulla tua stampante di destinazione prima di rilasciare il software. Il comportamento dei driver in materia di rotazione non è omogeneo tra i vari fornitori, ed alcuni di essi applicano la propria correzione alla rotazione a livello GDI. Se rilevi una doppia rotazione, rimuovi l'operazione di scambio; qualora tu non osservi alcuna correzione, aggiungila. Il metodo più rapido per identificare entrambe le casistiche di anomalia in fase di collaudo consiste nell'impiegare un documento ad orientamento misto, che alterna pagine con orientamento verticale (portrait) ed orizzontale (landscape)
Gestione della memoria in un processo di stampa prolungato
Ciascuna invocazione di RenderPage stanzia un nuovo oggetto TBitmap che è di proprietà del chiamante ed in quanto tale spetta a lui liberarne le risorse. Nel ciclo descritto poco fa, il blocco try/finally Bitmap.Free gestisce puntualmente tale occorrenza occupandosene di pagina in pagina. Non ammassare bitmap per pagine multiple: processare e renderizzare un plico composto di 200 pagine alla risoluzione di 300 DPI divorerebbe giga interi prima che la prima pagina raggiunga lo spooler. Procedi liberando ogni singola bitmap prima di passare alla pagina successiva
L'accoppiamento d'invocazioni basato su AllocMem / FreeMem residente entro il blocco di trasferimento segue la stessa regola. GetDIBSizes indica quanta memoria richiedono l'intestazione DIB e i dati dei pixel; la si alloca, riempie, dipinge e libera interamente nell'ambito di una singola pagina. Lasciare un leak in uno di questi passaggi esaurirà l'heap del processo della stampa su documenti con più di qualche decina di pagine
Se devi eseguire operazioni di stampa in un thread in background, mantieni TPdf e tutte le chiamate VCL della stampante sullo stesso thread. TPdf stesso non è thread-safe per istanze che condividono lo stato globale della DLL PDFium; il modello più sicuro è avere un TPdf per ogni thread, dove ognuno carica la propria copia del file
Le API per il rendering e la manipolazione dei documenti mostrate in questa sede fanno parte del PDFium Component per Delphi e C++Builder