Artículo técnico

Imprimir valores de formularios PDF y conservar proporciones

Un usuario llena un formulario PDF en un visor Delphi, pulsa Print y la hoja sale con todos los campos vacíos y la página tamaño Letter estirada para llenar un A4. Ambos fallos viven en el ciclo de impresión: PDFium Component pinta los valores de los campos únicamente en un bitmap (o los hornea en una copia aplanada), y cada página quiere un único factor de escala que conserve el aspecto

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

En Windows, la sobrecarga con contexto de dispositivo TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) le 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 interactiva nunca llega al DC — pertenece al entorno form-fill de PDFium y la dibuja FPDF_FFLDraw, cuyo único objetivo es un FPDF_BITMAP; no existe variante con HDC. Las dos sobrecargas con bitmap de TPdf.RenderPage ejecutan FPDF_RenderPageBitmap y FPDF_FFLDraw sobre los mismos píxeles siempre que el documento se haya abierto con FormFill en True, el valor por defecto. Con rePrinting en las opciones, PDFium Component quita el resaltado de campos antes de la pasada de formulario y restaura después el color y el alfa de resaltado por defecto, para que las cajas teñidas alrededor de los campos editables se queden en pantalla. rePrinting también se mapea al flag FPDF_PRINTING del motor, que marca la salida como impresión — el caso que ISO 32000-1 §12.5.3 (Tabla 165) atiende con el bit 3 del flag de anotación: un lector conforme deja fuera del papel una anotación cuyo bit Print está limpio, sin importar cómo se vea en pantalla

La plataforma decide el resto. En el motor PDFium, FPDF_RenderPage dibuja vía Windows GDI hacia un contexto de dispositivo y solo existe en builds de Windows, mientras que FPDF_FFLDraw dibuja hacia un bitmap y nada más, en todas las plataformas. En Linux y los demás objetivos no Windows no hay llamada GDI que hacer, así que la sobrecarga con HDC se atiende renderizando un bitmap — capa de formulario incluida — y dibujándolo sobre el canvas detrás del handle. La llamada con contexto de dispositivo, por tanto, carga los valores de los campos fuera de Windows y los deja fuera en Windows. Deje la rama explícita en código compartido entre Delphi y Lazarus: cuando el documento tiene un entorno de formulario y sus valores deben ir al papel, pida un bitmap en todas las plataformas

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

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

Elija por lo que el papel debe llevar. Los valores de formulario vivos apuntan a la ruta con bitmap, la salida vectorial a la ruta con HDC, y una copia aplanada le da ambos al precio de un guardado y una recarga. SetPdfPrintMode aplica a toda la biblioteca, así que cámbielo entre trabajos, nunca entre páginas

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

Una hoja A4 completa a 600 dpi son unos 4960 × 7016 píxeles de dispositivo, y a 32 bits por píxel eso son 139,197,440 bytes para una sola página; copiarla a un DIB para StretchDIBits duplica el pico, una carga pesada para un proceso de 32 bits. Para el botón Print de un visor la ruta con bitmap sigue siendo el defecto correcto: un usuario que teclea en un campo y pulsa Print espera ver ese valor en el papel, y el banding mantiene la memoria acotada. Los documentos que salen de la aplicación, como estados de cuenta archivados, quedan mejor servidos por la copia aplanada — los valores se vuelven contenido ordinario de página 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 queda 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 el appearance stream de cada widget (ISO 32000-1 §12.5.5) en el contenido de la página, y un appearance stream es lo último que escribió el productor anterior; el valor del campo en /V no participa. Un formulario llenado por una herramienta que nunca regeneró sus appearances se aplana al texto viejo o a nada, así que GenerateFormAppearances corre primero: reconstruye los appearances 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. Haga todo esto sobre una copia, porque el aplanado reescribe las páginas del TPdf sobre el que corre, y un visor 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; líbreela y borre el archivo temporal cuando el trabajo termine

PDFium Component copia de impresión aplanada en Delphi: SaveAs a un archivo temporal, reabrir con FormFill, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT), y luego impresión vectorial vía RenderPage sobre el contexto de dispositivo
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 el último productor dejó en /AP

Ajustar cada página al área imprimible

Escale una sola vez, por el menor de los dos factores, y centre el resultado. Estirar a Printer.PageWidth × Printer.PageHeight deforma cada página cuya forma difiera del papel, una página Letter (612 × 792 puntos) sobre A4 o una página landscape sobre una hoja portrait. Del lado de la impresora está el área imprimible en píxeles de dispositivo (HORZRES por VERTRES), con los márgenes del driver ya excluidos. Del lado del PDF, TPdf.PageWidth y PageHeight ya reportan la página tal como se muestra: una página A4 portrait 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 por /Rotate al mostrarse o imprimirse, PDFium lo aplica dentro de la llamada de render, y el argumento Rotation de RenderPage es solo el giro extra que añade un visor cuando el usuario rota la vista. Pase ahí el PageRotation propio de la página y la página gira dos veces. Intercambie ancho y alto para el ajuste solo cuando esa rotación extra sea de 90 o 270 grados, el mismo par de reglas que gobierna el ajuste y zoom de miniaturas bajo rotación; la rutina de abajo extiende el ciclo básico para imprimir documentos PDF con PDFium Component con una rama de valores de formulario y un camino de cancelación que descarta el trabajo

PDFium Component ajuste de página para impresión en Delphi: una página Letter estirada a Printer.PageWidth y PageHeight frente a una sola escala que conserva el aspecto y se centra 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 ancho y el alto se intercambian solo para una rotación extra del visor de ro90 o ro270
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 para 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 Cancel fije la bandera
      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 visor llama a esto con Pdf.FormFill como FormLayer; la copia aplanada pasa False. EndDoc es la confirmación, no un paso de limpieza. Después de BeginDoc, Printer.EndDoc entrega todo lo renderizado hasta el momento al spooler como un trabajo válido, así que un EndDoc incondicional en el finally convierte una impresión cancelada en una a medio imprimir. La rutina decide en el finally a partir de la bandera de éxito: EndDoc tras la última página, Abort tras una cancelación o una excepción. La salida del ciclo necesita un cuidado parecido, porque Delphi y Free Pascal evalúan una sola vez el valor final de un ciclo for, al entrar. Fijar ToPage o un conteo de copias en cero desde un manejador Cancel no detiene un ciclo que ya está corriendo: pruebe la bandera y haga Break, y con ciclos de copias y collation anidados, rompa en cada nivel. Y como Application.ProcessMessages deja que los atajos de menú también se disparen, deshabilite Open, Save y Print mientras el trabajo corre

Mantener la ruta raster dentro de la memoria

TPdf.RenderPageBanded renderiza una página en franjas horizontales de la altura que usted elija y pasa cada franja a un callback. Cada franja es una rebanada de la página a tamaño completo — nada se reescala — y lleva los mismos valores de campo que un bitmap de página completa. Con bandas de 512 filas, una página de 4960 píxeles de ancho necesita unos 10 MB por banda en lugar de 139 MB por la hoja: aproximadamente una catorava parte de la memoria. TPdfBandCallback es un method pointer que recibe el índice de la 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 ciclo de páginas en lugar de la rama de 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éalos y actúe sobre ellos en su propio código; PDFium los reporta y renderiza la página sin importar nada. 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 moldean un diálogo de impresión:

  • PrintScaling (Boolean) es False cuando el archivo dice /PrintScaling /None, que significa imprimir a tamaño real, y True para /AppDefault o una entrada ausente. La norma quiere que se respete aun cuando no se muestre ningún diálogo de impresión
  • PrintPaperHandling (TPrintPaperHandling) lleva /Duplex como phSimplex, phDuplexFlipShortEdge, phDuplexFlipLongEdge o phUndefined. SetPdfPrintPaperHandlingDevMode lo escribe en DEVMODE.dmDuplex y activa DM_DUPLEX; para phUndefined devuelve False y deja el record tranquilo, de modo que el defecto del driver sobrevive
  • PrintCopies (Integer) es /NumCopies, el conteo de copias para 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 activo y el 12 limpio, la impresión queda limitada 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 usted pida, así que la aplicación de la política es tarea del host, normalmente un botón Print deshabilitado. Cuando solo se concede permPrint, la lectura que concuerda con la norma es la ruta con 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 como se abrió: un archivo liberado con su contraseña de propietario reporta acceso total, y UserHasPermission responde solo por los bits de nivel de usuario

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 eso
  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 del 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 más grande
  Y := (Printer.PageHeight - H) div 2; // que el área imprimible: los bordes se recortan
end;

Ejecutar las acciones will-print y did-print en la frontera real de impresión

Los scripts de impresión a nivel de documento corren cuando el host los corre. El diccionario additional-actions del catálogo (ISO 32000-1 §12.6.3, Tabla 197) define WP, una acción JavaScript realizada antes de imprimir, y DP, realizada después. PDFium Component expone ambas vía ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) y dlaDidPrint, y las mantiene 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 se lo toca acertar a usted: un script will-print que estampa una fecha o llena un campo "printed by" cambia valores de formulario, así que ejecútelo antes de que la primera página se renderice y, en la ruta de aplanado, antes de que la copia se guarde; dispare el 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 archivos no confiables, lapDisabled apaga las acciones de documento. El panorama más amplio del scripting está en ejecutar JavaScript de AcroForm con eventos del host

En Windows, un trabajo que deba llevar valores de formulario pertenece a la rama con bitmap: RenderPage hacia un TBitmap a resoluciones ordinarias, RenderPageBanded a 600 dpi y más. Reserve la sobrecarga con contexto de dispositivo del PDFium Component para copias aplanadas y para documentos sin campos, donde su salida vectorial es el mejor resultado