Artículo técnico

Imprimir valores de formularios PDF y guardar proporciones

Un usuario rellena un formulario PDF en un viewer Delphi, pulsa Imprimir y la hoja sale con todos los campos vacíos y la página tamaño Letter estirada para llenar un A4. Los dos fallos viven en el bucle de impresión: PDFium Component solo pinta los valores de los campos en un bitmap (o los hornea en una copia aplanada), y cada página quiere un único factor de escala que conserve la proporción

La capa de formulario es una segunda pasada que necesita un bitmap

En Windows, la sobrecarga de device context TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) entrega el HDC de la impresora directo a FPDF_RenderPage. Esa llamada pinta el contenido de la página, más las anotaciones de markup cuando reAnnotations está activo. La capa de formulario interactivo nunca llega al DC — pertenece al entorno form-fill de PDFium y la dibuja FPDF_FFLDraw, cuyo único destino es un FPDF_BITMAP. No hay variante HDC. Las dos sobrecargas de bitmap de TPdf.RenderPage ejecutan FPDF_RenderPageBitmap y FPDF_FFLDraw sobre los mismos píxeles siempre que el documento se abriera con FormFill a True, el valor por defecto. Con rePrinting en las opciones, PDFium Component retira el resaltado de campos antes de la pasada de formulario y restaura después el color y alpha de resaltado por defecto, así que las cajas teñidas alrededor de los campos editables se quedan en pantalla. rePrinting se mapea además al flag FPDF_PRINTING del motor, que marca la salida como impresión — el caso que atiende ISO 32000-1 §12.5.3 (Tabla 165) con el bit 3 del flag de anotación: un lector conforme deja fuera del papel una anotación cuyo bit Print está a cero, por mucho que luzca en pantalla

La plataforma decide lo demás. En el motor de PDFium, FPDF_RenderPage dibuja a través de la GDI de Windows en un device context y solo existe en builds Windows, mientras que FPDF_FFLDraw dibuja en un bitmap y nada más, en todas las plataformas. En Linux y los demás destinos no Windows no hay llamada GDI que hacer, así que a la sobrecarga HDC la sirve el renderizado de un bitmap — capa de formulario incluida — y su volcado sobre el canvas detrás del handle. La llamada de device context por tanto lleva los valores de los campos fuera de Windows y los deja fuera en Windows. Deja la rama explícita en el código compartido entre Delphi y Lazarus: cuando el documento tiene entorno de formulario y sus valores deben salir en papel, pide un bitmap en todas las plataformas

Impresión de campos de formulario con PDFium Component en Delphi: TPdf.RenderPage sobre el HDC de la impresora llama a FPDF_RenderPage sin la capa de formulario, mientras que la sobrecarga TBitmap añade una pasada FPDF_FFLDraw antes de StretchDIBits
En Windows solo la sobrecarga de bitmap de RenderPage ejecuta la pasada de formulario FPDF_FFLDraw, por eso los valores tecleados en los campos llegan al papel por esa ruta y desaparecen por la de device context

¿Qué camino debe tomar un trabajo de impresión?

Elige por lo que el papel deba llevar. Los valores vivos de formulario apuntan al camino del bitmap, la salida vectorial al camino del HDC, y una copia aplanada te da ambos al precio de un guardado y una recarga. SetPdfPrintMode vale para toda la biblioteca, así que cámbialo entre trabajos, nunca entre páginas

RutaValores de formulario en papelSalidaCoste
RenderPage(DC, …)No, en WindowsGDI vectorial; SetPdfPrintMode elige EMF (el defecto) o un nivel PostScriptCola más pequeña, el más rápido
RenderPage a un TBitmap, luego StretchDIBitsSí, incluidas las ediciones sin guardarRaster al tamaño en píxeles que pidasAncho × Alto × 4 bytes por página, más la copia DIB
RenderPageBandedSíRaster, una franja cada vezUna banda en memoria en lugar de la hoja entera
Copia aplanada, luego RenderPage(DC, …)Sí, como contenido de página horneadoVectorialGuardar, recargar, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT)

Una hoja A4 entera a 600 dpi ronda los 4960 × 7016 píxeles de dispositivo y, a 32 bits por píxel, son 139.197.440 bytes para una sola página; copiarla a una DIB para StretchDIBits duplica el pico, una carga seria para un proceso de 32 bits. Para el botón Imprimir de un viewer el camino del bitmap sigue siendo el defecto correcto: un usuario que teclea en un campo y pulsa Imprimir espera ver ese valor en el papel, y las bandas mantienen la memoria acotada. Los documentos que salen de la aplicación, como extractos que se archivan, están mejor servidos por la copia aplanada — los valores se vuelven contenido de página ordinario y la impresora recibe salida vectorial

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 sigue en True (el defecto)
    Result.Password := Source.Password;  // la copia conserva su cifrado
    Result.Active := True;
    if not Result.Active then      // una carga fallida deja TPdf inactivo
      raise Exception.Create('Could not reopen the print copy');
    Result.GenerateFormAppearances;          // reconstruye /AP desde el /V actual
    if not Result.FlattenAllPages(FLAT_PRINT) then
      raise Exception.Create('Flattening failed');
  except
    Result.Free;
    TFile.Delete(TempName);
    raise;
  end;
end;

La ruta de aplanado tiene una regla de orden que importa más que el flag. FlattenAllPages hornea en el contenido de página el appearance stream de cada widget (ISO 32000-1 §12.5.5), y un appearance stream es lo último que escribió el productor anterior; el valor del campo en /V no juega. Un formulario rellenado por una herramienta que nunca regeneró sus apariencias se aplana al texto viejo o a nada, así que GenerateFormAppearances corre primero: reconstruye las apariencias de los campos de texto desde los valores actuales y sincroniza los estados /AS de checkboxes y radios, y necesita que FormFill haya estado en True al abrir el documento. Haz todo esto sobre una copia, porque aplanar reescribe las páginas del TPdf sobre el que corre, y un viewer abierto quiere sus campos editables. La copia pasa por la rama vectorial de la rutina de impresión de abajo con la capa de formulario apagada; libérala y borra el fichero temporal cuando el trabajo acabe

Copia de impresión aplanada de PDFium Component en Delphi: SaveAs a un fichero temporal, reabrir con FormFill, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT), y luego impresión vectorial vía RenderPage sobre el device context
Llamar a GenerateFormAppearances antes de FlattenAllPages hace que los appearance streams horneados coincidan con los valores actuales de los campos en lugar de con lo que dejara el último productor en /AP

Encajar cada página en el área imprimible

Escala una vez, por el menor de los dos factores, y centra el resultado. Estirar a Printer.PageWidth × Printer.PageHeight deforma toda página cuya forma difiera del papel, una página Letter (612 × 792 puntos) sobre A4 o una página apaisada sobre una hoja vertical. El lado de la impresora es el área imprimible en píxeles de dispositivo (HORZRES por VERTRES), con los márgenes del driver ya excluidos. En el lado PDF, TPdf.PageWidth y PageHeight ya reportan la página tal y como se muestra: una página A4 vertical almacenada con /Rotate 90 vuelve como 842 × 595 puntos. ISO 32000-1 §7.7.3.3 (Tabla 30) dice que la página debe rotarse según /Rotate al mostrarse o imprimirse, PDFium la aplica dentro de la llamada de render, y el argumento Rotation de RenderPage es solo el giro extra que añade un viewer cuando el usuario rota la vista. Pasa ahí el PageRotation de la propia página y la página gira dos veces. Intercambia ancho y alto para el encaje solo cuando esa rotación extra es de 90 o 270 grados, el mismo par de reglas que gobierna el encaje y zoom de miniaturas bajo rotación; la rutina de abajo extiende el bucle básico de imprimir documentos PDF con PDFium Component con una rama de valores de formulario y un camino de cancelación que descarta el trabajo

Encaje de página de PDFium Component para impresión en Delphi: una página Letter estirada a Printer.PageWidth y PageHeight frente a una única escala que conserva la proporción centrada en el área imprimible, con /Rotate ya dentro de TPdf.PageWidth
Escalar por el menor de los dos factores mantiene cada página sin deformar y centrada, y el intercambio de ancho y alto solo ocurre con una rotación 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 ante un tamaño de página degenerado
        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;     // deja que un botón Cancelar active el flag
      if Cancel then
        Break;
    end;
    Result := not Cancel;
  finally
    if Result then
      Printer.EndDoc                   // entrega el trabajo al spooler
    else
      Printer.Abort;                   // cancelación o excepción: no se imprime nada
  end;
end;

Un viewer llama a esto con Pdf.FormFill como FormLayer; la copia aplanada pasa False. EndDoc es la confirmación, no un paso de limpieza. Tras BeginDoc, Printer.EndDoc entrega a la cola todo lo renderizado hasta entonces como un trabajo válido, así que un EndDoc incondicional en el finally convierte una impresión cancelada en una a medias. La rutina decide en el finally a partir del flag de éxito: EndDoc tras la última página, Abort tras una cancelación o una excepción. La salida del bucle necesita un cuidado parecido, porque Delphi y Free Pascal evalúan ambos el valor final de un bucle for una sola vez, al entrar. Poner ToPage o un conteo de copias a cero desde un manejador de Cancelar no detiene un bucle que ya corre; prueba el flag y haz Break, y con bucles anidados de copias y colación, sal de cada nivel. Y como Application.ProcessMessages deja que también se disparen los atajos de menú, desactiva Abrir, Guardar e Imprimir mientras el trabajo corre

Mantener el camino raster dentro de la memoria

TPdf.RenderPageBanded renderiza una página en franjas horizontales de una altura que eliges y entrega cada franja a un callback. Cada franja es un corte de la página a tamaño completo — nada se reescala — y lleva los mismos valores de campo que un bitmap de página entera. Con bandas de 512 filas, una página de 4960 píxeles de ancho necesita unos 10 MB por banda en lugar de los 139 MB de la hoja. Más o menos un catorceavo de la memoria. TPdfBandCallback es un puntero a método que recibe el índice de banda, el offset superior de la banda en píxeles de página y el bitmap, y devuelve True para continuar; RenderPageBanded devuelve el conteo de bandas, 0 en caso de fallo

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 detiene las bandas restantes
end;

// llamado desde el bucle de páginas en lugar de la rama del bitmap de página completa
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;

¿Qué debe hacer el host con ViewerPreferences y los bits de permisos?

Léelos y actúa sobre ellos en tu propio código; PDFium los reporta y renderiza la página sin más. Un productor puede guardar pistas de impresión en el diccionario /ViewerPreferences del catálogo (ISO 32000-1 §12.2, Tabla 150), y TPdf expone las que dan forma a un diálogo de impresión:

  • PrintScaling (Boolean) es False cuando el fichero dice /PrintScaling /None, que significa imprimir a tamaño real, y True para /AppDefault o una entrada ausente. La norma quiere que se honre incluso cuando no se muestra diálogo de impresión
  • PrintPaperHandling (TPrintPaperHandling) trae /Duplex como phSimplex, phDuplexFlipShortEdge, phDuplexFlipLongEdge o phUndefined. SetPdfPrintPaperHandlingDevMode lo escribe en DEVMODE.dmDuplex y activa DM_DUPLEX; para phUndefined devuelve False y deja el registro tranquilo, así que el defecto del driver sobrevive
  • PrintCopies (Integer) es /NumCopies, el conteo de copias que poner en el diálogo

Los bits de permisos siguen la misma regla. Para el security handler estándar, ISO 32000-1 §7.6.3.2 (Tabla 22) asigna el bit 3 de /P a la impresión y, para los handlers de revisión 3 y posteriores, el bit 12 a imprimir con fidelidad completa; con el bit 3 a uno y el 12 a cero, imprimir queda limitado a una representación de bajo nivel de la apariencia, posiblemente degradada. TPdf.GrantedPermissions decodifica la máscara en permPrint y permPrintHighQuality, y HasPermission prueba un valor; el motor renderiza lo que le pidas, así que hacer cumplir es trabajo del host, normalmente un botón Imprimir deshabilitado. Cuando solo se concede permPrint, la lectura que casa con la norma es el camino del bitmap a una resolución modesta en lugar de la salida vectorial o PostScript que permite una concesión completa. Los bits describen el documento tal y como se abrió: un fichero desbloqueado con su contraseña de owner reporta acceso completo, y UserHasPermission responde por los bits de nivel usuario solos

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);  // el diálogo parte de esto
  end;

  Dialog.MinPage := 1;
  Dialog.MaxPage := Pdf.PageCount;
  Dialog.Options := Dialog.Options + [poPageNums];
  if Pdf.PrintCopies > 1 then
    Dialog.Copies := Pdf.PrintCopies;
end;

// se usa en lugar de FitRect cuando ActualSize es True
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
  // 72 puntos por pulgada en el lado PDF, píxeles de dispositivo por pulgada en la impresora
  W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
  H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
  X := (Printer.PageWidth - W) div 2;  // negativo cuando la página es mayor
  Y := (Printer.PageHeight - H) div 2; // que el área imprimible: los bordes se recortan
end;

Ejecutar las acciones will-print y did-print en el límite real de impresión

Los scripts de impresión a nivel de documento corren cuando el host los ejecuta. El diccionario de additional-actions del catálogo (ISO 32000-1 §12.6.3, Tabla 197) define WP, una acción JavaScript ejecutada antes de imprimir, y DP, ejecutada después. PDFium Component expone ambas mediante ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) y dlaDidPrint, y las deja fuera de las llamadas de render: las miniaturas, las capturas y las conversiones también renderizan, y ninguna debería parecerle a un script de documento la impresión de un usuario. El orden te toca acertarlo a ti. Un script will-print que estampa una fecha o rellena un campo "printed by" cambia valores de formulario, así que ejecútalo antes de renderizar la primera página y, en la ruta del aplanado, antes de guardar la copia; dispara did-print solo después de que EndDoc haya confirmado el trabajo. La llamada devuelve False con un mensaje cuando falta el entorno de formulario o LifecycleActionPolicy lo rechaza; para ficheros no confiables, lapDisabled apaga las acciones de documento. El panorama más amplio de scripting está en ejecutar JavaScript AcroForm con eventos del host

En Windows, un trabajo que tiene que llevar valores de formulario pertenece a la rama del bitmap: RenderPage a un TBitmap en resoluciones ordinarias, RenderPageBanded a 600 dpi y por encima. Reserva la sobrecarga de device context del PDFium Component para las copias aplanadas y para los documentos sin campos, donde su salida vectorial es el mejor resultado