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
¿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
| Ruta | Valores de formulario en papel | Salida | Coste |
|---|---|---|---|
RenderPage(DC, …) | No, en Windows | GDI vectorial; SetPdfPrintMode elige EMF (el defecto) o un nivel PostScript | Cola más pequeña, el más rápido |
RenderPage a un TBitmap, luego StretchDIBits | Sí, incluidas las ediciones sin guardar | Raster al tamaño en píxeles que pidas | Ancho × Alto × 4 bytes por página, más la copia DIB |
RenderPageBanded | Sí | Raster, una franja cada vez | Una banda en memoria en lugar de la hoja entera |
Copia aplanada, luego RenderPage(DC, …) | Sí, como contenido de página horneado | Vectorial | Guardar, 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
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
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/AppDefaulto una entrada ausente. La norma quiere que se honre incluso cuando no se muestra diálogo de impresiónPrintPaperHandling(TPrintPaperHandling) trae/DuplexcomophSimplex,phDuplexFlipShortEdge,phDuplexFlipLongEdgeophUndefined.SetPdfPrintPaperHandlingDevModelo escribe enDEVMODE.dmDuplexy activaDM_DUPLEX; paraphUndefineddevuelve False y deja el registro tranquilo, así que el defecto del driver sobrevivePrintCopies(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