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