Articolo tecnico

Stampare i valori dei form PDF e tenere le proporzioni

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

Stampa dei campi form con PDFium Component in Delphi: TPdf.RenderPage sull'HDC della stampante chiama FPDF_RenderPage senza il livello del form, mentre l'overload TBitmap aggiunge un passaggio FPDF_FFLDraw prima di StretchDIBits
Su Windows solo l'overload a bitmap di RenderPage esegue il passaggio form di FPDF_FFLDraw, per questo i valori digitati nei campi arrivano sulla carta per quella strada e svaniscono su quella a device context

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

StradaValori del form sulla cartaOutputCosto
RenderPage(DC, …)No, su WindowsGDI vettoriale; SetPdfPrintMode sceglie EMF (il default) o un livello PostScriptSpool più piccolo, più veloce
RenderPage verso una TBitmap, poi StretchDIBitsSì, incluse le modifiche non salvateRaster alla dimensione in pixel che chiedeteLarghezza × Altezza × 4 byte per pagina, più la copia DIB
RenderPageBandedSìRaster, una banda alla voltaUna banda in memoria invece dell'intero foglio
Copia appiattita, poi RenderPage(DC, …)Sì, come contenuto di pagina cottoVettorialeSave, 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

Copia di stampa appiattita con PDFium Component in Delphi: SaveAs su un file temporaneo, riapertura con FormFill, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT), poi stampa vettoriale tramite RenderPage sul device context
Chiamare GenerateFormAppearances prima di FlattenAllPages fa combaciare gli stream di appearance cotti con i valori correnti dei campi invece che con quel che l'ultimo producer ha lasciato in /AP

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

Fit della pagina per la stampa Delphi con PDFium Component: una pagina Letter allungata su Printer.PageWidth e PageHeight contro una scala che conserva le proporzioni centrata nell'area stampabile, con /Rotate già dentro TPdf.PageWidth
Scalare col più piccolo dei due fattori tiene ogni pagina indistorta e centrata, e larghezza e altezza si scambiano solo per una rotazione extra ro90 o ro270 del viewer
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 /AppDefault o una voce assente. Lo standard vuole che venga onorato anche quando nessuna finestra di stampa viene mostrata
  • PrintPaperHandling (TPrintPaperHandling) porta /Duplex come phSimplex, phDuplexFlipShortEdge, phDuplexFlipLongEdge o phUndefined. SetPdfPrintPaperHandlingDevMode lo scrive in DEVMODE.dmDuplex e imposta DM_DUPLEX; per phUndefined restituisce False e lascia il record in pace, così sopravvive il default del driver
  • PrintCopies (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