Technical Article

Impresión de documentos PDF con PDFium VCL en Delphi

Las coordenadas PDF están en puntos, las coordenadas de impresora están en unidades de dispositivo, y ambas no tienen relación entre sí hasta que se convierten explícitamente. Esa discrepancia es la raíz de la mayoría de las salidas de impresión defectuosas en aplicaciones Delphi: el código envía el fichero correcto pero la página sale recortada, estirada o en blanco. PDFium VCL gestiona el lado del renderizado de forma limpia; la fontanería de la impresora es VCL estándar. Los dos encajan con una cantidad moderada de código una vez que se entiende qué espera cada parte.

Cómo funciona el proceso renderizar-luego-imprimir

PDFium VCL no se comunica directamente con las impresoras. El patrón es: renderizar una página a un TBitmap a la resolución deseada y luego transferir ese mapa de bits al lienzo de la impresora con StretchDIBits. TPdf.RenderPage devuelve un mapa de bits cuya gestión corresponde al llamante, por lo que se controlan las dimensiones en píxeles. Pasando [rePrinting] en el conjunto de opciones, PDFium cambia su ruta de renderizado a una que omite efectos exclusivos de pantalla como el hinting de subpíxeles LCD, y gestiona correctamente el MediaBox de la página para la salida impresa. Si se omite rePrinting, lo que se envía a la impresora es un renderizado de pantalla, que se ve bien en un monitor pero tiende a producir una salida más borrosa en impresoras de alta resolución porque las decisiones de hinting para pantallas de 96 DPI no son adecuadas para impresión a 300 o 600 DPI.

TPdf.Active es la única compuerta que hay que comprobar antes de acceder a cualquier propiedad de página. El componente absorbe los errores de carga silenciosamente: establecer Active := True sobre un fichero dañado o protegido con contraseña no lanza una excepción; simplemente deja Active en False. Hay que comprobarlo siempre tras la asignación. Leer PageCount o PageWidth sobre un documento inactivo devuelve cero, produciendo operaciones sin efecto que son muy difíciles de diagnosticar una vez que llegan al spooler.

Un bucle de impresión mínimo

El caso más sencillo carga un fichero, abre un trabajo de impresión, itera las páginas y cierra. El único detalle delicado es que Printer.NewPage no debe llamarse antes de la primera página, de ahí el indicador FirstPage. La transferencia con StretchDIBits pasa por GetDIBSizes y GetDIB para extraer los bits independientes del dispositivo del manejador del mapa de bits y luego los pinta en el lienzo de la impresora al tamaño completo de la página:

procedure PrintPdfFile(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Bitmap: TBitmap;
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
  FirstPage: Boolean;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;  // load failed silently; bail out

    Printer.Title := Pdf.Title;
    Printer.BeginDoc;
    try
      FirstPage := True;
      for I := 1 to Pdf.PageCount do
      begin
        if FirstPage then
          FirstPage := False
        else
          Printer.NewPage;

        Pdf.PageNumber := I;

        // Render at printer resolution; rePrinting adjusts the render path
        Bitmap := Pdf.RenderPage(
          0, 0,
          Printer.PageWidth,
          Printer.PageHeight,
          ro0,
          [rePrinting]
        );
        try
          GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
          InfoHeader := AllocMem(InfoHeaderSize);
          try
            Image := AllocMem(ImageSize);
            try
              GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
              StretchDIBits(
                Printer.Canvas.Handle,
                0, 0, Printer.PageWidth, Printer.PageHeight,
                0, 0, Bitmap.Width, Bitmap.Height,
                Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
              );
            finally
              FreeMem(Image);
            end;
          finally
            FreeMem(InfoHeader);
          end;
        finally
          Bitmap.Free;
        end;
      end;
    finally
      Printer.EndDoc;
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Pasar Printer.PageWidth y Printer.PageHeight como dimensiones del mapa de bits significa renderizar al tamaño de píxel nativo de la impresora, que ya tiene en cuenta los DPI del dispositivo. La llamada a StretchDIBits mapea entonces esos píxeles 1:1 sobre la página. Esto proporciona la mejor fidelidad posible sin aritmética explícita de DPI, pero solo funciona cuando la página PDF y el papel físico tienen el mismo tamaño. Cuando difieren, se necesita escalado explícito.

Escalado cuando los tamaños de página y papel difieren

Una página PDF en A4 vertical no encaja automáticamente en una impresora de papel Carta US, y una página apaisada enviada a una impresora orientada verticalmente se recortará. El enfoque estándar es calcular un factor de escala uniforme a partir de la relación entre los píxeles de la impresora y los puntos PDF, y aplicarlo a ambas dimensiones para preservar la relación de aspecto. Pdf.PageWidth y Pdf.PageHeight exponen las dimensiones actuales de la página en puntos, donde un punto equivale a 1/72 de pulgada. Multiplicar por los DPI de destino y dividir entre 72 convierte a píxeles a esa resolución. Se toma el mínimo de las relaciones X e Y para obtener la mayor escala que aún cabe dentro del área imprimible:

// Fit PDF page to printable area, preserving aspect ratio
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // target render resolution
  Pdf.PageNumber := PageIndex;

  ScaleX := Printer.PageWidth  / (Pdf.PageWidth  * Dpi / 72);
  ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
  Scale  := Min(ScaleX, ScaleY);

  // Clamp to 1.0 for shrink-to-fit only (no enlargement)
  if Scale > 1.0 then Scale := 1.0;

  DestWidth  := Round(Pdf.PageWidth  * Dpi / 72 * Scale);
  DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);

  Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
    [rePrinting, reAnnotations]);
  // ... transfer with StretchDIBits as above
end;

Renderizar con Dpi = 300 es adecuado para la mayoría de las impresoras de oficina. A 600 DPI, el mapa de bits de una sola página A4 alcanza unos 34 megapíxeles, lo que supone unos 100 MB como mapa de bits de 32 bits; la ganancia en calidad para documentos de texto ordinarios es mínima y el coste de memoria por página es considerable. Conviene reservar 600 DPI para imprentas o dibujos técnicos con muchos vectores donde realmente importa.

El indicador reAnnotations del segundo bloque de código es independiente de rePrinting. Se incluye cuando el usuario espera que los sellos, los resaltados y los cuadros de comentarios aparezcan en papel. Se omite para una salida solo de contenido. Ambos indicadores se pueden combinar libremente.

Rotación de página

PDFium almacena la rotación de página en el PDF como una entrada /Rotate, accesible mediante Pdf.PageRotation, que devuelve un valor TRotation (ro0, ro90, ro180, ro270). El sistema de coordenadas de la impresora invierte las rotaciones de 90 y 270 grados respecto a la pantalla. Si se pasa el valor bruto de PageRotation directamente a RenderPage sin ningún ajuste, las páginas apaisadas incrustadas en un documento vertical se imprimirán boca abajo en la mayoría de los controladores de impresora de Windows. La solución es un simple intercambio antes de la llamada de renderizado: mapear ro90 a ro270 y ro270 de vuelta a ro90, dejando ro0 y ro180 sin cambios.

Conviene verificar este comportamiento en la impresora de destino específica antes de distribuir. El comportamiento de los controladores en cuanto a la rotación no es uniforme entre fabricantes, y algunos aplican su propia corrección de rotación a nivel GDI. Si se observa una rotación doble, hay que eliminar el intercambio; si no hay corrección en absoluto, hay que añadirlo. Un documento de orientación mixta con páginas verticales y apaisadas alternadas es la forma más rápida de detectar cualquiera de los dos modos de fallo durante las pruebas.

Gestión de memoria en trabajos de impresión largos

Cada llamada a RenderPage asigna un nuevo TBitmap cuya gestión corresponde al llamante. En el bucle anterior, el bloque try/finally Bitmap.Free gestiona esto correctamente para una página a la vez. No hay que acumular mapas de bits entre páginas: un renderizado a 300 DPI de un documento de 200 páginas consumiría gigabytes antes de que la primera página llegase al spooler. Hay que liberar cada mapa de bits antes de avanzar a la siguiente página.

El par AllocMem / FreeMem dentro del bloque de transferencia sigue la misma regla. GetDIBSizes indica cuánta memoria necesitan la cabecera DIB y los datos de píxeles; se asigna, rellena, pinta y libera todo dentro del ámbito de una página. Dejar que cualquiera de los bloques tenga fugas hará que el trabajo de impresión agote el montículo del proceso en documentos de más de unas pocas docenas de páginas.

Si se necesita ejecutar trabajos de impresión en un hilo de fondo, hay que mantener TPdf y todas las llamadas a la impresora VCL en el mismo hilo. TPdf no es seguro para subprocesos entre instancias que comparten el estado global de la DLL de PDFium; el modelo más seguro es un TPdf por hilo, cada uno cargando su propia copia del fichero.

El API de renderizado y documento mostrado aquí forma parte del componente PDFium VCL para Delphi y C++Builder.