Un utente compila un form PDF in un viewer Delphi, preme Stampa, e il foglio esce con tutti i campi vuoti e la pagina di formato Letter allungata per riempire un A4. Entrambi i guasti stanno nel ciclo di stampa: PDFium Component dipinge i valori dei campi solo in una bitmap (o li cuoce in una copia appiattita), e ogni pagina vuole un solo fattore di scala che conservi le proporzioni
Il livello del form è un secondo passaggio che richiede una bitmap
Su Windows, l'overload a device context TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) consegna l'HDC della stampante dritto a FPDF_RenderPage. Quella chiamata dipinge il contenuto della pagina, più le annotazioni di markup quando reAnnotations è impostato. Il livello del form interattivo non arriva mai al DC — appartiene all'ambiente form-fill di PDFium e viene disegnato da FPDF_FFLDraw, che ha come unico bersaglio un FPDF_BITMAP. Non esiste una variante HDC. I due overload a bitmap di TPdf.RenderPage fanno girare FPDF_RenderPageBitmap e FPDF_FFLDraw sugli stessi pixel ogni volta che il documento è stato aperto con FormFill a True, il default. Con rePrinting nelle opzioni, PDFium Component toglie l'evidenziazione dei campi prima del passaggio del form e ripristina dopo il colore e l'alpha di default, così le caselle tinte attorno ai campi editabili restano sullo schermo. rePrinting si mappa anche sulla flag FPDF_PRINTING del motore, che marca l'output come stampa — il caso che ISO 32000-1 §12.5.3 (Tabella 165) affronta col bit 3 della flag di annotazione: un reader conforme lascia fuori dalla carta un'annotazione il cui bit Print è a zero, per quanto appaia sullo schermo
La piattaforma decide il resto. Nel motore PDFium, FPDF_RenderPage disegna attraverso la GDI di Windows in un device context e esiste solo nelle build Windows, mentre FPDF_FFLDraw disegna in una bitmap e in nient'altro, su ogni piattaforma. Su Linux e sugli altri target non Windows non c'è alcuna chiamata GDI da fare, quindi l'overload HDC viene servito renderizzando una bitmap — livello del form compreso — e disegnandola sulla canvas dietro l'handle. La chiamata a device context porta dunque i valori dei campi fuori da Windows e li lascia fuori su Windows. Tenete il ramo esplicito nel codice condiviso fra Delphi e Lazarus: quando il documento ha un ambiente form e i suoi valori devono finire sulla carta, chiedete una bitmap su ogni piattaforma
Quale strada deve prendere un lavoro di stampa?
Scegliete in base a cosa deve portare la carta. I valori vivi del form puntano alla strada bitmap, l'output vettoriale alla strada HDC, e una copia appiattita vi dà entrambe le cose al prezzo di un save e un ricaricamento. SetPdfPrintMode vale per tutta la libreria, quindi cambiatela fra un lavoro e l'altro, mai fra una pagina e l'altra
| Strada | Valori del form sulla carta | Output | Costo |
|---|---|---|---|
RenderPage(DC, …) | No, su Windows | GDI vettoriale; SetPdfPrintMode sceglie EMF (il default) o un livello PostScript | Spool più piccolo, più veloce |
RenderPage verso una TBitmap, poi StretchDIBits | Sì, incluse le modifiche non salvate | Raster alla dimensione in pixel che chiedete | Larghezza × Altezza × 4 byte per pagina, più la copia DIB |
RenderPageBanded | Sì | Raster, una banda alla volta | Una banda in memoria invece dell'intero foglio |
Copia appiattita, poi RenderPage(DC, …) | Sì, come contenuto di pagina cotto | Vettoriale | Save, ricaricamento, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT) |
Un foglio A4 intero a 600 dpi fa circa 4960 × 7016 pixel dispositivo, e a 32 bit per pixel sono 139.197.440 byte per una pagina; copiarla in una DIB per StretchDIBits raddoppia il picco, un carico pesante per un processo a 32 bit. Per il pulsante Stampa di un viewer la strada bitmap resta comunque il default giusto: un utente che digita in un campo e preme Stampa si aspetta di vedere quel valore sulla carta, e le bande tengono la memoria dentro un limite. I documenti che lasciano l'applicazione, come gli estratti conto da archiviare, stanno meglio con la copia appiattita — i valori diventano normale contenuto di pagina e la stampante riceve output vettoriale
uses
System.SysUtils, System.IOUtils, PDFium, FPdfUtils; // FPdfUtils: FLAT_PRINT
function OpenFlattenedPrintCopy(Source: TPdf; out TempName: string): TPdf;
begin
TempName := TPath.Combine(TPath.GetTempPath, TPath.GetRandomFileName + '.pdf');
if not Source.SaveAs(TempName) then
raise Exception.Create('Could not write the print copy');
Result := TPdf.Create(nil);
try
Result.FileName := TempName; // FormFill resta True (il default)
Result.Password := Source.Password; // la copia conserva la sua cifratura
Result.Active := True;
if not Result.Active then // un caricamento fallito lascia TPdf inattivo
raise Exception.Create('Could not reopen the print copy');
Result.GenerateFormAppearances; // ricostruisce /AP dai /V correnti
if not Result.FlattenAllPages(FLAT_PRINT) then
raise Exception.Create('Flattening failed');
except
Result.Free;
TFile.Delete(TempName);
raise;
end;
end;
La strada appiattita ha una regola d'ordine che conta più della flag. FlattenAllPages cuoce nella pagina lo stream di appearance di ogni widget (ISO 32000-1 §12.5.5), e uno stream di appearance è quel che ha scritto l'ultimo producer; il valore del campo in /V non gioca alcun ruolo. Un form compilato da uno strumento che non ha mai rigenerato le proprie appearances si appiattisce sul vecchio testo o sul nulla, quindi GenerateFormAppearances gira prima: ricostruisce le appearance dei campi testo dai valori correnti e sincronizza gli stati /AS di checkbox e radio, e ha bisogno che FormFill fosse True all'apertura del documento. Fate tutto su una copia, perché l'appiattimento riscrive le pagine di qualunque TPdf su cui gira, e un viewer aperto vuole che i propri campi restino editabili. La copia passa per il ramo vettoriale della routine di stampa qui sotto col livello del form spento; liberatela e cancellate il file temporaneo quando il lavoro finisce
Adattare ogni pagina all'area stampabile
Scala una volta sola, col più piccolo dei due fattori, e centra il risultato. Allungare su Printer.PageWidth × Printer.PageHeight distorce ogni pagina la cui forma differisce dalla carta, una pagina Letter (612 × 792 punti) su A4 o una pagina landscape su un foglio portrait. Il lato stampante è l'area stampabile in pixel dispositivo (HORZRES per VERTRES), con i margini del driver già esclusi. Sul lato PDF, TPdf.PageWidth e PageHeight riferiscono già la pagina come viene mostrata: una pagina A4 portrait conservata con /Rotate 90 torna come 842 × 595 punti. ISO 32000-1 §7.7.3.3 (Tabella 30) dice che la pagina va ruotata secondo /Rotate quando viene mostrata o stampata, PDFium lo applica dentro la chiamata di render, e l'argomento Rotation di RenderPage è solo la rotazione extra che un viewer aggiunge quando l'utente ruota la vista. Passate lì la PageRotation della pagina e la pagina gira due volte. Scambiate larghezza e altezza per il fit solo quando quella rotazione extra è di 90 o 270 gradi, la stessa coppia di regole che governa fit delle miniature e zoom sotto rotazione; la routine qui sotto estende il ciclo di base per stampare documenti PDF con PDFium Component con un ramo per i valori del form e un percorso di annullamento che butta via il lavoro
uses
Winapi.Windows, System.SysUtils, Vcl.Graphics, Vcl.Forms, Vcl.Printers, PDFium;
procedure FitRect(PageW, PageH: Double; Swap: Boolean;
AreaW, AreaH: Integer; out X, Y, W, H: Integer);
var
T, Scale: Double;
begin
if Swap then
begin
T := PageW; PageW := PageH; PageH := T;
end;
if (PageW <= 0) or (PageH <= 0) then
begin
X := 0; Y := 0; W := AreaW; H := AreaH;
Exit;
end;
Scale := AreaW / PageW;
if AreaH / PageH < Scale then
Scale := AreaH / PageH;
W := Round(PageW * Scale);
H := Round(PageH * Scale);
X := (AreaW - W) div 2;
Y := (AreaH - H) div 2;
end;
procedure BlitBitmap(DC: HDC; Bitmap: TBitmap; X, Y: Integer);
var
InfoSize, ImageSize: DWORD;
Info: PBitmapInfo;
Bits: Pointer;
begin
GetDIBSizes(Bitmap.Handle, InfoSize, ImageSize);
Info := AllocMem(InfoSize);
try
Bits := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, Info^, Bits^);
StretchDIBits(DC, X, Y, Bitmap.Width, Bitmap.Height, 0, 0,
Bitmap.Width, Bitmap.Height, Bits, Info^, DIB_RGB_COLORS, SRCCOPY);
finally
FreeMem(Bits);
end;
finally
FreeMem(Info);
end;
end;
function PrintPages(Pdf: TPdf; FromPage, ToPage: Integer; FormLayer: Boolean;
ExtraRotation: TRotation; var Cancel: Boolean): Boolean;
const
Opts: TRenderOptions = [reAnnotations, rePrinting];
var
Page, X, Y, W, H: Integer;
Bitmap: TBitmap;
begin
Result := False;
Printer.BeginDoc;
try
for Page := FromPage to ToPage do
begin
if Page > FromPage then
Printer.NewPage;
Pdf.PageNumber := Page;
FitRect(Pdf.PageWidth, Pdf.PageHeight, ExtraRotation in [ro90, ro270],
Printer.PageWidth, Printer.PageHeight, X, Y, W, H);
if FormLayer then
begin
Bitmap := Pdf.RenderPage(0, 0, W, H, ExtraRotation, Opts);
if Bitmap <> nil then // nil per una dimensione di pagina degenerata
try
BlitBitmap(Printer.Canvas.Handle, Bitmap, X, Y);
finally
Bitmap.Free;
end;
end
else
Pdf.RenderPage(Printer.Canvas.Handle, X, Y, W, H, ExtraRotation, Opts);
Application.ProcessMessages; // lascia che un pulsante Annulla imposti la flag
if Cancel then
Break;
end;
Result := not Cancel;
finally
if Result then
Printer.EndDoc // consegna il lavoro allo spooler
else
Printer.Abort; // annullamento o eccezione: non stampa nulla
end;
end;
Un viewer chiama questa routine con Pdf.FormFill come FormLayer; la copia appiattita passa False. EndDoc è la consegna, non un passo di pulizia. Dopo BeginDoc, Printer.EndDoc consegna allo spooler tutto quanto renderizzato fin lì come lavoro valido, quindi un EndDoc incondizionato nel finally trasforma una stampa annullata in una stampata a metà. La routine decide nel finally dalla flag di successo invece: EndDoc dopo l'ultima pagina, Abort dopo un annullamento o un'eccezione. Anche l'uscita dal ciclo richiede cure simili, perché Delphi e Free Pascal valutano entrambi il valore finale di un ciclo for una volta sola, all'ingresso. Impostare ToPage o un numero di copie a zero da un gestore di Annulla non ferma un ciclo già in corsa: testate la flag e fate Break, e con cicli annidati di copie e collazionamento uscite da ogni livello. E siccome Application.ProcessMessages lascia scattare anche le scorciatoie di menu, disabilitate Apri, Salva e Stampa mentre il lavoro gira
Tenere la strada raster dentro la memoria
TPdf.RenderPageBanded renderizza una pagina in strisce orizzontali dell'altezza che scegliete e consegna ogni striscia a una callback. Ogni striscia è una fetta della pagina a dimensione piena — nulla viene riscaleato — e porta gli stessi valori dei campi di una bitmap a pagina intera. Con bande da 512 righe, una pagina larga 4960 pixel ha bisogno di circa 10 MB per banda invece dei 139 MB del foglio. All'incirca un quattordicesimo della memoria. TPdfBandCallback è un method pointer che riceve l'indice della banda, l'offset superiore della banda in pixel di pagina e la bitmap, e restituisce True per continuare; RenderPageBanded restituisce il numero di bande, 0 in caso di fallimento
type
TBandPrinter = class
private
FDC: HDC;
FLeft, FTop: Integer;
public
constructor Create(DC: HDC; Left, Top: Integer);
function PrintBand(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean;
end;
constructor TBandPrinter.Create(DC: HDC; Left, Top: Integer);
begin
inherited Create;
FDC := DC;
FLeft := Left;
FTop := Top;
end;
function TBandPrinter.PrintBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
begin
BlitBitmap(FDC, Bitmap, FLeft, FTop + BandTopY);
Result := True; // False ferma le bande rimanenti
end;
// chiamata dal ciclo delle pagine al posto del ramo della bitmap a pagina intera
procedure PrintPageBanded(Pdf: TPdf; Page, X, Y, W, H: Integer;
ExtraRotation: TRotation);
var
Bands: TBandPrinter;
begin
Bands := TBandPrinter.Create(Printer.Canvas.Handle, X, Y);
try
if Pdf.RenderPageBanded(W, H, 512, Bands.PrintBand, ExtraRotation,
[reAnnotations, rePrinting]) = 0 then
raise Exception.CreateFmt('Page %d could not be rendered', [Page]);
finally
Bands.Free;
end;
end;
Cosa deve fare l'host con ViewerPreferences e i bit di permesso?
Leggeteli e agite di conseguenza nel vostro codice; PDFium li riferisce e renderizza la pagina a prescindere. Un producer può conservare suggerimenti di stampa nel dizionario /ViewerPreferences del catalogo (ISO 32000-1 §12.2, Tabella 150), e TPdf porta in superficie quelli che danno forma a una finestra di stampa:
PrintScaling(Boolean) vale False quando il file dice/PrintScaling /None, che significa stampare a dimensione reale, e True per/AppDefaulto una voce assente. Lo standard vuole che venga onorato anche quando nessuna finestra di stampa viene mostrataPrintPaperHandling(TPrintPaperHandling) porta/DuplexcomephSimplex,phDuplexFlipShortEdge,phDuplexFlipLongEdgeophUndefined.SetPdfPrintPaperHandlingDevModelo scrive inDEVMODE.dmDuplexe impostaDM_DUPLEX; perphUndefinedrestituisce False e lascia il record in pace, così sopravvive il default del driverPrintCopies(Integer) è/NumCopies, il numero di copie da mettere nella finestra
I bit di permesso seguono la stessa regola. Per il security handler standard, ISO 32000-1 §7.6.3.2 (Tabella 22) assegna il bit 3 della /P alla stampa e, per i handler di revision 3 e successive, il bit 12 alla stampa a piena fedeltà; col bit 3 impostato e il bit 12 a zero, la stampa è limitata a una rappresentazione di basso livello dell'appearance, eventualmente degradata. TPdf.GrantedPermissions decodifica la maschera in permPrint e permPrintHighQuality, e HasPermission testa un valore; il motore renderizza qualunque cosa chiediate, quindi l'applicazione è compito dell'host, di norma un pulsante Stampa disabilitato. Quando è concesso solo permPrint, la lettura che corrisponde allo standard è la strada bitmap a una risoluzione modesta anziché l'output vettoriale o PostScript che una concessione piena permette. I bit descrivono il documento così com'è stato aperto: un file sbloccato con la propria owner password riferisce accesso pieno, e UserHasPermission risponde per i bit a livello utente da soli
uses
Winapi.Windows, System.SysUtils, Vcl.Dialogs, Vcl.Printers, PDFium;
procedure PreparePrintJob(Pdf: TPdf; Dialog: TPrintDialog;
out ActualSize, HighQuality: Boolean);
var
Device, Driver, Port: array[0..MAX_PATH] of Char;
DevModeHandle: THandle;
DevMode: PDeviceMode;
begin
if not Pdf.HasPermission(permPrint) then
raise Exception.Create('This document does not permit printing');
HighQuality := Pdf.HasPermission(permPrintHighQuality);
ActualSize := not Pdf.PrintScaling; // /PrintScaling /None
Printer.GetPrinter(Device, Driver, Port, DevModeHandle);
if DevModeHandle <> 0 then
begin
DevMode := GlobalLock(DevModeHandle);
try
SetPdfPrintPaperHandlingDevMode(DevMode^, Pdf.PrintPaperHandling);
finally
GlobalUnlock(DevModeHandle);
end;
Printer.SetPrinter(Device, Driver, Port, DevModeHandle); // la finestra parte da qui
end;
Dialog.MinPage := 1;
Dialog.MaxPage := Pdf.PageCount;
Dialog.Options := Dialog.Options + [poPageNums];
if Pdf.PrintCopies > 1 then
Dialog.Copies := Pdf.PrintCopies;
end;
// usata al posto di FitRect quando ActualSize è True
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
// 72 punti per pollice sul lato PDF, pixel dispositivo per pollice sulla stampante
W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
X := (Printer.PageWidth - W) div 2; // negativa quando la pagina è più grande
Y := (Printer.PageHeight - H) div 2; // dell'area stampabile: i bordi vengono tagliati
end;
Eseguire le azioni will-print e did-print al vero confine di stampa
Gli script di stampa a livello documento girano quando li fa girare l'host. Il dizionario additional-actions del catalogo (ISO 32000-1 §12.6.3, Tabella 197) definisce WP, un'azione JavaScript eseguita prima della stampa, e DP, eseguita dopo. PDFium Component espone entrambe tramite ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) e dlaDidPrint, e le tiene fuori dalle chiamate di render: miniature, snapshot e conversioni renderizzano anch'essi, e nessuno dovrebbe sembrare una stampa dell'utente a uno script di documento. L'ordine è vostra cura. Uno script will-print che timbra una data o riempie un campo "printed by" cambia i valori del form, quindi fatelo girare prima che la prima pagina venga renderizzata e, sulla strada appiattita, prima che la copia venga salvata; lanciate did-print solo dopo che EndDoc ha consegnato il lavoro. La chiamata restituisce False con un messaggio quando manca l'ambiente del form o LifecycleActionPolicy la rifiuta; per file non fidati, lapDisabled spegne le azioni documento. Il quadro più ampio dello scripting sta in eseguire AcroForm JavaScript con gli eventi dell'host
Su Windows, un lavoro che deve portare con sé i valori del form sta sul ramo bitmap: RenderPage in una TBitmap alle risoluzioni ordinarie, RenderPageBanded a 600 dpi e oltre. Tenete l'overload a device context del PDFium Component per le copie appiattite e per i documenti senza campi, dove il suo output vettoriale è il risultato migliore