Articolo tecnico

Stampa di documenti PDF con PDFium VCL in Delphi

Le coordinate del PDF sono in punti, quelle della stampante sono in unità di dispositivo, e le due cose non hanno nulla a che fare l'una con l'altra finché non le si converte intenzionalmente. Questa discrepanza è alla base della maggior parte dei problemi di stampa nelle applicazioni Delphi: il codice invia il file corretto, ma la pagina risulta ritagliata, allungata o vuota. PDFium VCL gestisce la parte di rendering in modo pulito; il sistema di stampa è quello standard della VCL. I due componenti si integrano con una modesta quantità di codice una volta compreso ciò che ciascuna parte si aspetta.

Come funziona la pipeline di rendering e stampa

PDFium VCL non comunica direttamente con le stampanti. Il pattern è: eseguire il rendering di una pagina su un TBitmap alla risoluzione desiderata, quindi trasferire tale bitmap sul canvas della stampante con StretchDIBits. TPdf.RenderPage restituisce un bitmap di proprietà del chiamante, consentendo di controllare le dimensioni in pixel. Passando [rePrinting] nel set di opzioni, PDFium passa a un percorso di rendering che omette gli effetti esclusivi dello schermo, come l'hinting dei subpixel LCD, e gestisce correttamente il MediaBox della pagina per l'output di stampa. Escludendo rePrinting, ciò che viene inviato alla stampante è un rendering per schermo, che appare corretto su un monitor ma tende a produrre un output più sfocato su stampanti ad alta densità (high-DPI), poiché le decisioni di hinting prese per schermi a 96 DPI non sono adatte alla stampa a 300 o 600 DPI.

TPdf.Active è l'unico controllo da verificare prima di accedere a qualsiasi proprietà della pagina. Il componente ignora gli errori di caricamento in modo silenzioso: impostare Active := True su un file danneggiato o protetto da password non solleva un'eccezione, ma lascia semplicemente Active impostato su False. Verificatelo sempre dopo l'assegnazione. La lettura di PageCount o PageWidth su un documento non attivo restituisce zero, generando operazioni nulle silenziose difficili da diagnosticare una volta arrivate allo spooler di stampa.

Un ciclo di stampa minimale

Il caso d'uso più semplice carica un file, avvia un processo di stampa, scorre le pagine e lo chiude. L'unico dettaglio a cui prestare attenzione è che Printer.NewPage non deve essere chiamato prima della prima pagina, da cui l'uso del flag FirstPage. Il trasferimento con StretchDIBits passa attraverso GetDIBSizes e GetDIB per estrarre i bit indipendenti dal dispositivo dall'handle del bitmap, per poi disegnarli sul canvas della stampante alle dimensioni complete 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 del bitmap significa eseguire il rendering alla risoluzione in pixel nativa della stampante, che tiene già conto dei DPI del dispositivo. La chiamata a StretchDIBits mappa quindi questi pixel con un rapporto 1:1 sulla pagina. Questo garantisce la massima fedeltà possibile senza dover calcolare esplicitamente i DPI, ma funziona solo quando la pagina PDF e la carta fisica hanno le stesse dimensioni. Quando differiscono, è necessario un ridimensionamento esplicito.

Ridimensionamento quando le dimensioni della pagina e della carta differiscono

Una pagina PDF in formato A4 verticale non si adatta automaticamente a una stampante con formato US Letter, e una pagina in orizzontale inviata a una stampante orientata in verticale verrà ritagliata. 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 della pagina corrente in punti, dove un punto equivale a 1/72 di pollice. Moltiplicando per la risoluzione DPI target e dividendo per 72 si ottiene la conversione in pixel a quella risoluzione. Utilizzando il valore minimo (Min) tra i rapporti X e Y si ottiene la scala massima che rientra 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 per la maggior parte delle stampanti da ufficio. A 600 DPI, il bitmap per una singola pagina A4 raggiunge circa 34 megapixel, il che equivale a circa 100 MB per un bitmap a 32 bit; il miglioramento della qualità per i normali documenti di testo è minimo a fronte di un costo di memoria significativo per pagina. Riservate i 600 DPI per le tipografie o per disegni tecnici ricchi di elementi vettoriali in cui è realmente necessario.

Il flag reAnnotations nel secondo blocco di codice è indipendente da rePrinting. Includetelo quando l'utente si aspetta che timbri, evidenziature e riquadri di commento appaiano sulla carta. Omittetelo per un output contenente solo il testo principale. Entrambi i flag possono essere combinati liberamente.

Rotazione della pagina

PDFium memorizza la rotazione della pagina nel PDF come voce /Rotate, accessibile tramite Pdf.PageRotation, che 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 passate il valore grezzo di PageRotation direttamente a RenderPage senza alcuna correzione, le pagine orientate in orizzontale all'interno di un documento verticale verranno stampate capovolte sulla maggior parte dei driver di stampa Windows. La soluzione consiste in un semplice scambio prima della chiamata di rendering: mappare ro90 su ro270 e ro270 su ro90, lasciando invariati ro0 e ro180.

Verificate questo comportamento sulla stampante di destinazione specifica prima del rilascio. Il comportamento dei driver in merito alla rotazione non è uniforme tra i vari produttori, e alcuni driver applicano la propria correzione a livello di GDI. Se si verifica una doppia rotazione, rimuovete lo scambio; se non notate alcuna correzione, aggiungetelo. Un documento con orientamento misto, che alterna pagine verticali e orizzontali, è il modo più rapido per identificare queste anomalie durante i test.

Gestione della memoria per processi di stampa lunghi

Ogni chiamata a RenderPage alloca un nuovo TBitmap che il chiamante possiede e deve liberare. Nel ciclo descritto sopra, il blocco try/finally Bitmap.Free gestisce questa operazione correttamente una pagina alla volta. Non accumulate i bitmap tra le pagine: il rendering a 300 DPI di un documento di 200 pagine consumerebbe gigabyte prima ancora che la prima pagina raggiunga lo spooler. Liberate ciascun bitmap prima di passare alla pagina successiva.

La coppia AllocMem / FreeMem all'interno del blocco di trasferimento segue la stessa regola. GetDIBSizes indica quanta memoria è necessaria per l'intestazione DIB e per i dati dei pixel; l'allocazione, il riempimento, il disegno e il rilascio avvengono interamente all'interno dell'ambito di una singola pagina. Lasciare che uno dei due blocchi causi perdite di memoria (leak) porterà il processo di stampa a esaurire l'heap del processo su documenti più lunghi di poche decine di pagine.

Se è necessario eseguire processi di stampa in un thread in background, mantenete TPdf e tutte le chiamate di stampa VCL nello stesso thread. TPdf di per sé non è thread-safe tra istanze che condividono lo stato globale della DLL di PDFium; il modelo più sicuro consiste nell'avere un'istanza di TPdf per ciascun thread, ognuna caricando la propria copia del file.

Le API di rendering e di gestione dei documenti mostrate qui fanno parte del PDFium VCL Component per Delphi e C++Builder.