Il rendering di una pagina PDF su un contesto di dispositivo Windows per l'anteprima di stampa mette tre sistemi di coordinate nella stessa riga di codice, e raramente concordano. La pagina PDF è misurata in punti con l'origine in basso a sinistra. Il DC dello schermo è misurato in pixel con l'origine in alto a sinistra e un fattore di zoom scelto dall'utente. Il DC della stampante, quello che l'anteprima dovrebbe prevedere, misura i pixel alla risoluzione del dispositivo ma pone l'origine nell'angolo dell'area stampabile, non nell'angolo del foglio. Se uno solo di questi elementi è errato, l'anteprima appare corretta mentre la pagina stampata risulta spostata, scalata o tagliata lungo un bordo. Il sintomo tipico è un modulo con bordo che nell'anteprima appare centrato e in stampa ha le linee superiore e sinistra tagliate, perché la stampante laser non può depositare inchiostro negli ultimi millimetri esterni e nessuno l'ha comunicato all'anteprima. losLab PDF Library (PDF Library for Delphi) copre l'intero percorso con chiamate di rendering su contesto di dispositivo, un livello di configurazione della stampante virtuale e bitmap di anteprima generate dalle metriche della stampante stessa, che è la parte che rende l'anteprima sincera su quel margine
La geometria della carta non è la geometria stampabile
Due rettangoli descrivono ogni destinazione di stampa, e lo scostamento tra loro è il punto in cui vive la maggior parte degli errori di anteprima. Il rettangolo della carta è il foglio fisico. Il rettangolo stampabile è la regione più piccola che il motore di stampa può effettivamente raggiungere, rientrata di un margine hardware che differisce per modello di stampante e talvolta per cassetto. Il livello di stampa della libreria misura entrambi. La classe sottostante TPLPrinter espone PageWidth e PageHeight per l'area stampabile, FullPageWidth e FullPageHeight per il foglio completo e PrintOffsetX con PrintOffsetY per la distanza tra le loro origini, tutto in pixel del dispositivo alla risoluzione riportata da GetDPI. Un'anteprima fedele riduce gli stessi numeri alla risoluzione dello schermo invece di disegnare la pagina nel rettangolo che il controllo ha a disposizione. Saltare questo passaggio fa presumere silenziosamente all'anteprima un margine zero, che è l'unico valore che nessuna stampante reale usa
Anteprima a schermo tramite RenderPageToDC
Per un controllo di anteprima a schermo, RenderPageToDC(DPI, Page, DC) disegna una pagina del documento caricato direttamente su qualunque contesto di dispositivo GDI, sia esso il canvas di un TPaintBox, una bitmap fuori schermo o un DC metafile. L'argomento DPI imposta lo zoom. 96 approssima una vista al 100% su un display classico e raddoppiarlo raddoppia la dimensione renderizzata
procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
// questi tre sono stato persistente della libreria, non parametri per chiamata:
FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
FPdf.SetRenderDCErasePage(1);
FPdf.SetRenderCropType(0);
FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;
L'insidia è che il percorso di rendering DC è governato dallo stato persistente della libreria, non da parametri per chiamata. SetRenderDCOffset, SetRenderDCErasePage e SetRenderCropType persistono ciascuno finché qualcosa non li modifica, quindi un ciclo di miniature eseguito dopo che l'utente ha regolato la vista ingrandita eredita l'offset o il ritaglio lasciato dal percorso di codice precedente. Il sintomo è un'anteprima che deriva solo in sequenze specifiche di navigazione, quasi il peggiore tipo di errore da riprodurre. Impostare tutto lo stato pertinente all'inizio del gestore di disegno, come sopra, non costa nulla ed elimina l'intera categoria. Un secondo moltiplicatore è nascosto vicino. La risoluzione di output effettiva è la scala di rendering moltiplicata per l'argomento DPI e, sebbene SetRenderScale sia predefinito a 1.0, anch'esso persiste dopo la modifica; quindi una funzione di esportazione che l'abbia aumentato ridimensiona silenziosamente ogni anteprima successiva finché qualcosa non lo reimposta
I visualizzatori scorrevoli e i ridisegni parziali dispongono di una variante dedicata. RenderPageToDCClip accetta una specifica di ritaglio insieme al contesto di dispositivo, quindi l'invalidazione di una fascia della finestra ridisegna solo quella fascia invece di rasterizzare nuovamente l'intera pagina. Ad alto zoom su pagine di grande formato, questa è la differenza tra un visualizzatore che segue la barra di scorrimento e uno che lascia scie dietro di sé
Un lavoro di stampa che corrisponde all'anteprima
Il lato della stampa funziona tramite una stampante virtuale. NewCustomPrinter clona una stampante di sistema in una configurazione privata della libreria e SetupPrinter modifica tale clone senza toccare il DevMode dell'intero computer: la carta passa come impostazione 1 (una costante DMPAPER_*) e l'orientamento come impostazione 11. Il vantaggio è l'isolamento. Un servizio può stampare etichette A4 mentre la stampante predefinita dell'host resta su Letter, senza che sia necessario ripristinare nulla in seguito
var
Pdf: TPDFlib;
Virt: WideString;
Opt: Integer;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('report.pdf', '') <> 1 then
raise Exception.Create('load failed');
Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
Pdf.SetupPrinter(Virt, 1, 9); // setting 1 = paper, DMPAPER_A4
Pdf.SetupPrinter(Virt, 11, 1); // setting 11 = orientation, 1 = portrait
Opt := Pdf.PrintOptions(1, 1, 'Monthly Report'); // adatta al foglio, rotazione automatica + centratura
Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
finally
Pdf.Free;
end;
end;
PrintOptions merita una lettura attenta. Restituisce un handle di opzioni che deve essere passato a PrintDocument o PrintPages; non è uno stato ambiente. Creare le opzioni e poi dimenticare di passare l'handle fallisce in silenzio. Il lavoro stampa con i valori predefiniti e nessuno se ne accorge finché ci si attendeva una politica di adattamento alla carta e una pagina sovradimensionata non risulta tagliata. L'argomento di ridimensionamento pagina è il punto in cui risiede tale politica. Nessun ridimensionamento preserva l'accuratezza dimensionale, importante per moduli misurati con un righello. L'adattamento alla carta ridimensiona tutto al foglio. La riduzione delle pagine grandi lascia inalterate le pagine normali e interviene solo quando una pagina supera l'area stampabile, che in genere è il valore predefinito corretto per un insieme misto di documenti. Il flag di rotazione automatica e centratura gestisce le pagine orizzontali senza un secondo percorso di codice
Le applicazioni che gestiscono già un TPrinter tramite il flusso di dialogo VCL possono consegnarlo direttamente. PrintDocumentToPrinterObject e PrintPagesToPrinterObject accettano l'istanza TPrinter configurata, mantenendo la finestra di dialogo di stampa standard come superficie di configurazione per l'utente mentre la libreria gestisce il rendering delle pagine. Mescolare i due approcci nello stesso percorso di codice tende a reintrodurre la deriva geometrica che il resto di questo lavoro dovrebbe eliminare, quindi scegline uno. Il percorso della stampante virtuale è adatto ai servizi non presidiati; il percorso TPrinter è adatto alle applicazioni interattive
Anche l'output selettivo funziona nello stesso modo. PrintPages accetta una stringa di intervallo, quindi passare il nome della stampante virtuale, '2-5,12' e l'handle di opzioni stampa le pagine da 2 a 5 e la 12 con il contratto geometrico intatto; la stessa sintassi gestisce le varianti di stampa su file. Quelle varianti su file sono la risposta pratica per un ambiente non presidiato senza dispositivo fisico collegato: il test di regressione della geometria di stampa su un server di build senza alcuna coda driver. Esegui lo stesso documento con le stesse opzioni in un artefatto file a ogni build e una regressione geometrica diventa un diff invece di una segnalazione del cliente tre settimane dopo
Bitmap di anteprima con le metriche della stampante stessa
Un'anteprima renderizzata a 96 DPI rispetto a una dimensione pagina presunta risponde alla domanda sbagliata. Mostra l'aspetto della pagina, non ciò che questa stampante metterà su questa carta. GetPrintPreviewBitmapToString colma questa lacuna creando l'anteprima dalla stessa stampante personalizzata e dallo stesso handle di opzioni del lavoro finale, quindi dimensione della carta, orientamento, politica di ridimensionamento, rotazione e offset hardware contribuiscono tutti alla bitmap. Ciò che ritorna è ciò che il foglio mostrerà
procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
Data: AnsiString;
Strm: TMemoryStream;
Bmp: TBitmap;
begin
Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
Strm := TMemoryStream.Create;
try
Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
Strm.Position := 0;
Bmp := TBitmap.Create;
try
Bmp.LoadFromStream(Strm);
PreviewImage.Picture.Assign(Bmp);
finally
Bmp.Free;
end;
finally
Strm.Free;
end;
end;
L'argomento MaxDimension limita il lato lungo della bitmap. 1200 pixel mantiene nitida un'anteprima in una finestra di dialogo e conserva un uso modesto della memoria perfino per disegni tecnici in formato E, dove un rendering a piena risoluzione ai 600 DPI della stampante raggiungerebbe gigabyte
Ricordare le scelte di stampante dell'utente
Le finestre di dialogo di stampa che dimenticano le impostazioni tra le sessioni generano richieste di supporto proprie. La coppia DevMode, GetPrinterDevModeToString e SetPrinterDevModeFromString, serializza la configurazione completa del driver di una stampante in una stringa opaca che puoi conservare nelle preferenze dell'utente e ripristinare nella sessione successiva, comprese le opzioni specifiche del driver che nessuna API generica si preoccupa di modellare. Memorizza la stampante per nome da GetPrinterNames, mai per indice dell'elenco. L'ordine degli indici cambia ogni volta che una stampante viene aggiunta o rimossa, quindi un indice salvato punta silenziosamente al dispositivo errato la volta successiva che l'elenco cambia. GetDefaultPrinterName fornisce il fallback quando il dispositivo ricordato è completamente scomparso
La selezione del cassetto completa la storia della persistenza. GetPrinterBins riporta le sorgenti della carta esposte da un driver, importante per flussi di lavoro con carta intestata in cui la prima pagina proviene dal cassetto della carta intestata e le altre da carta normale. È una politica che gli utenti si aspettano che l'applicazione ricordi insieme a tutto il resto e un lavoro di stampa che finisce sul supporto errato sembra un errore anche quando ogni byte del PDF era corretto
Mantenere un solo motore tra anteprima e stampa
Un'ultima decisione governa silenziosamente la fedeltà. La selezione del motore di rendering si applica sia alle destinazioni a schermo sia a quelle di stampa, quindi la tentazione è usare un motore rapido per l'anteprima e uno preciso per la stampa. Resistila. Gestire l'anteprima e il lavoro con motori diversi reintroduce l'esatta deriva di fedeltà che un'anteprima fedele alla stampante era stata creata per eliminare, e lo fa in modo che appare solo sulla carta. I compromessi tra i motori integrato, Cairo e PDFium sono analizzati in il rendering PDF multi-motore in Delphi; scegline uno e usalo su entrambi i lati
I documenti troppo grandi per essere caricati comodamente prima della stampa possono essere aperti tramite il percorso di accesso diretto descritto in unione, divisione e accesso diretto a PDF di grandi dimensioni, che esegue il rendering delle pagine in un contesto di dispositivo da un handle di file senza costruire l'albero del documento. Il riferimento API completo per la stampa è nella pagina del prodotto losLab PDF Library per Delphi