Artículo técnico

Vista previa de impresión y device-context con PDF Library for Delphi

Renderizar una página PDF en un contexto de dispositivo de Windows para la vista previa de impresión pone tres sistemas de coordenadas en la misma línea de código, y rara vez se ponen de acuerdo. La página PDF se mide en puntos con el origen en la parte inferior izquierda. El DC de pantalla se mide en píxeles con el origen en la parte superior izquierda y un factor de zoom que usted elige. El DC de impresora, aquel cuya vista previa se supone que debe predecir, mide píxeles en la resolución del dispositivo pero coloca su origen en la esquina del área imprimible, no en la esquina de la hoja. Equivoque cualquiera de ellos y la vista previa se ve bien mientras la página impresa sale desplazada, escalada o recortada por un borde. El síntoma habitual es un formulario con bordes que se previsualiza centrado y se imprime con las reglas superior e izquierda cortadas, porque la impresora láser no puede poner tinta en los milímetros exteriores y nadie le dijo a la vista previa. losLab PDF Library (PDF Library for Delphi) cubre toda la ruta con llamadas de renderizado en device-context, una capa de configuración de impresora virtual, y mapas de bits de vista previa generados a partir de las propias métricas de la impresora, que es la parte que hace que la vista previa sea honesta sobre ese margen

La geometría del papel no es la geometría imprimible

Dos rectángulos describen cualquier destino de impresión, y el desplazamiento entre ellos es donde viven la mayoría de los errores de vista previa. El rectángulo de papel es la hoja física. El rectángulo imprimible es la región más pequeña que el motor de impresión realmente puede alcanzar, insertada por un margen de hardware que difiere por modelo de impresora y a veces por bandeja. La capa de impresión de la biblioteca mide ambos. La clase subyacente TPLPrinter expone PageWidth y PageHeight para el área imprimible, FullPageWidth y FullPageHeight para la hoja completa, y PrintOffsetX con PrintOffsetY para el hueco entre sus orígenes, todos en píxeles de dispositivo a la resolución que GetDPI reporta. Una vista previa honesta escala esos mismos números a la resolución de pantalla en lugar de pintar la página en cualquier rectángulo que el control resulte tener. Sáltelo y la vista previa asume silenciosamente un margen cero, que es el único valor que ninguna impresora real usa

Diagrama de PDF Library for Delphi de la hoja de papel completa frente al rectángulo imprimible más pequeño, con PrintOffsetX y PrintOffsetY marcando el margen de hardware entre sus orígenes
El rectángulo de papel es la hoja física mientras que el rectángulo imprimible es lo que el motor de impresión puede alcanzar, y la separación entre sus orígenes es donde viven la mayoría de los errores de vista previa

Vista previa en pantalla a través de RenderPageToDC

Para un control de vista previa en pantalla, RenderPageToDC(DPI, Page, DC) dibuja una página del documento cargado directamente sobre cualquier contexto de dispositivo GDI, sea un lienzo TPaintBox, un mapa de bits fuera de pantalla, o un DC de metarchivo. El argumento DPI fija el zoom. 96 aproxima una vista del 100 % en una pantalla clásica, y doblarlo dobla el tamaño renderizado

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
  // estos tres son estado persistente de la biblioteca, no parámetros por llamada:
  FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
  FPdf.SetRenderDCErasePage(1);
  FPdf.SetRenderCropType(0);
  FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;

La trampa es que la ruta de renderizado DC se dirige por estado adherente de la biblioteca, no por parámetros por llamada. SetRenderDCOffset, SetRenderDCErasePage y SetRenderCropType persisten cada uno hasta que algo los cambia, así que un bucle de miniaturas que corre después de que el usuario ajustó la vista con zoom hereda cualquier desplazamiento o recorte que la ruta de código anterior dejara. El síntoma es una vista previa que deriva solo en secuencias de navegación específicas, que es casi tan miserable de reproducir como un error puede llegar a ser. Fijar todo el estado relevante en la parte superior del manejador de pintura, como arriba, no cuesta nada y elimina toda la clase. Un segundo multiplicador se esconde cerca. La resolución de salida efectiva es la escala de render por el argumento DPI, y mientras SetRenderScale por defecto es 1.0, también persiste una vez cambiado, así que una característica de exportación que lo subió reescala silenciosamente cada vista previa posterior hasta que algo lo restablece

Los visores de desplazamiento y las repinturas parciales tienen una variante dedicada. RenderPageToDCClip toma una especificación de recorte junto con el contexto de dispositivo, así que invalidar una banda de la ventana repinta solo esa banda en lugar de volver a rasterizar la página completa. En zoom alto en páginas de gran formato esa es la diferencia entre un visor que sigue a la barra de desplazamiento y uno que se emborrona detrás de ella

Un trabajo de impresión que coincide con la vista previa

El lado de impresión funciona a través de una impresora virtual. NewCustomPrinter clona una impresora del sistema en una configuración privada de la biblioteca, y SetupPrinter ajusta ese clon sin tocar el DevMode de toda la máquina: el papel entra como ajuste 1 (una constante DMPAPER_*) y la orientación como ajuste 11. La ganancia es el aislamiento. Un servicio puede imprimir etiquetas A4 mientras la impresora predeterminada del host se queda en Carta, y nada necesita restaurarse después

PDF Library for Delphi: Flujo desde la impresora predeterminada del sistema por NewCustomPrinter y SetupPrinter hasta un trabajo de impresión aislado que nunca toca el DevMode global de la máquina
SetupPrinter reorienta un clon privado de la biblioteca para que un servicio pueda imprimir A4 mientras la impresora predeterminada del anfitrión conserva intacto su DevMode Letter
var
  Pdf: TPDFlib;
  Virt: WideString;
  Opt: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
    Pdf.SetupPrinter(Virt, 1, 9);        // setting 1 = paper, DMPAPER_A4
    Pdf.SetupPrinter(Virt, 11, 1);       // setting 11 = orientation, 1 = portrait
    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // ajusta al papel, autorrotar + centrar
    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
  finally
    Pdf.Free;
  end;
end;

PrintOptions merece una lectura cuidadosa. Devuelve un handle de opciones que debe pasar a PrintDocument o PrintPages; no es estado ambiente. Construir las opciones y luego olvidar pasar el handle falla silenciosamente. El trabajo se imprime con los predeterminados, y nadie lo nota hasta que se esperaba una política de ajustar-al-papel y salió una página sobredimensionada recortada en su lugar. El argumento de escalado de página es donde vive esa política. Sin escalado se preserva la precisión dimensional, lo que importa para formularios que se miden contra una regla. Ajustar-al-papel reescala todo a la hoja. Shrink-large-pages deja solas las páginas normales y solo interviene cuando una página excede el área imprimible, que suele ser el predeterminado correcto para un conjunto de documentos mixto. La bandera de auto-rotar-y-centrar maneja páginas horizontales sin una segunda ruta de código

Las aplicaciones que ya gestionan un TPrinter a través del flujo de diálogo VCL pueden entregarlo directamente. PrintDocumentToPrinterObject y PrintPagesToPrinterObject aceptan la instancia TPrinter configurada, lo que mantiene el diálogo de impresión estándar como superficie de configuración orientada al usuario mientras la biblioteca maneja el renderizado de página. Mezclar los dos enfoques en una ruta de código tiende a reintroducir la deriva geométrica que el resto de este trabajo pretendía matar, así que elija uno. La ruta de impresora virtual sirve para servicios sin supervisión; la ruta TPrinter para aplicaciones interactivas

La salida selectiva funciona igual. PrintPages toma una cadena de rango, así que pasar el nombre de impresora virtual, '2-5,12', y el handle de opciones imprime las páginas 2 a 5 y 12 con el contrato geométrico intacto, y la misma sintaxis dirige las variantes de impresión-a-archivo. Esas variantes de archivo son la respuesta práctica para un entorno sin supervisión sin dispositivo físico conectado: probar por regresión la geometría de impresión en un servidor de build que no tiene cola de driver en absoluto. Renderice el mismo documento a través de las mismas opciones en un artefacto de archivo en cada build, y una regresión de geometría se convierte en un diff en lugar de un informe de cliente tres semanas después

Mapas de bits de vista previa con las propias métricas de la impresora

Una vista previa renderizada a 96 DPI contra un tamaño de página asumido responde a la pregunta equivocada. Muestra el aspecto de la página, no lo que esta impresora pondrá en este papel. GetPrintPreviewBitmapToString colma ese hueco construyendo la vista previa a partir de la misma impresora personalizada y el mismo handle de opciones que el trabajo eventual, así que tamaño de papel, orientación, política de escalado, rotación y el desplazamiento de hardware alimentan todos al mapa de bits. Lo que vuelve es lo que la hoja mostrará

PDF Library for Delphi: Contraste entre una vista previa en pantalla con tamaño de página supuesto que tergiversa los márgenes y un mapa de bits fiel a la impresora construido desde el handle de impresora y opciones del propio trabajo
GetPrintPreviewBitmapToString dibuja la vista previa desde el mismo manejador de impresora y opciones personalizadas que el trabajo final, de modo que el mapa de bits muestra los márgenes y la rotación que la hoja recibirá de verdad
procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
  Data: AnsiString;
  Strm: TMemoryStream;
  Bmp: TBitmap;
begin
  Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
  Strm := TMemoryStream.Create;
  try
    Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
    Strm.Position := 0;
    Bmp := TBitmap.Create;
    try
      Bmp.LoadFromStream(Strm);
      PreviewImage.Picture.Assign(Bmp);
    finally
      Bmp.Free;
    end;
  finally
    Strm.Free;
  end;
end;

El argumento MaxDimension limita el borde largo del mapa de bits. 1200 píxeles se mantiene nítido para un diálogo de vista previa y mantiene la memoria modesta incluso para planos de ingeniería tamaño E, donde un render a resolución completa a los 600 DPI de la impresora correría a gigabytes

Recordar las elecciones de impresora del usuario

Los diálogos de impresión que olvidan sus ajustes entre sesiones generan sus propios tickets de soporte. El par DevMode, GetPrinterDevModeToString y SetPrinterDevModeFromString, serializa la configuración completa del driver de una impresora a una cadena opaca que puede guardar en las preferencias de usuario y restaurar la próxima sesión, incluidas las opciones específicas del driver que ninguna API genérica se molesta en modelar. Persista la impresora por nombre desde GetPrinterNames, nunca por índice de lista. El orden del índice cambia cada vez que se añade o quita una impresora, así que un índice guardado apunta silenciosamente al dispositivo equivocado la próxima vez que la lista cambie. GetDefaultPrinterName cubre la reserva cuando el dispositivo recordado ha desaparecido por completo

La selección de bandeja completa la historia de persistencia. GetPrinterBins reporta las fuentes de papel que un driver expone, lo que importa para los flujos de trabajo con membrete donde la página uno tira de la bandeja de membrete y el resto del papel normal. Es una política que los usuarios esperan que la aplicación recuerde junto con todo lo demás, y un trabajo de impresión que aterriza en el papel equivocado se lee como un error incluso cuando cada byte del PDF era correcto

Mantenga un solo motor entre vista previa e impresión

Una última decisión gobierna silenciosamente la fidelidad. La selección de motor de renderizado aplica a ambos destinos, pantalla e impresora, así que la tentación es previsualizar con un motor rápido e imprimir con uno preciso. Resístase. Conducir la vista previa y el trabajo a través de motores diferentes reintroduce la deriva de fidelidad exacta que una vista previa fiel a la impresora se construyó para eliminar, y lo hace de una forma que solo se muestra en el papel. Las concesiones entre los motores integrado, Cairo y PDFium se sopesan en renderizado PDF multimotor en Delphi; elija uno y úselo en ambos lados

Los documentos demasiado grandes para cargar cómodamente antes de imprimir se pueden abrir a través de la ruta de acceso directo descrita en fusión, división y acceso directo de PDF grandes, que renderiza páginas a un contexto de dispositivo desde un handle de archivo sin construir el árbol del documento. La referencia completa de la API de impresión está en la página del producto losLab PDF Library para Delphi