Artículo técnico

Renderizar páginas PDF a monocromo de 1 bit en Delphi

Una pasarela de fax no quiere tu renderizado de página a 24 bits. Tampoco lo quiere la canalización de archivo que guarda un millón de facturas con aspecto de escaneadas, ni el front-end de OCR que umbraliza todo a blanco y negro antes incluso de buscar un carácter. Los tres quieren lo mismo: un bitmap limpio de 1 bit, un bit por píxel, donde cada punto es tinta o papel. Si les entregas un BMP a todo color, de todas formas descartarán 23 bits por píxel, por lo general con un tramado peor que el que tú podrías haber hecho. La cuestión interesante es dónde debe ocurrir esa conversión descendente, y la respuesta en PDF Library for Delphi acaba diciendo algo útil sobre cómo ampliar un renderer que preferirías no reescribir

PDF Library for Delphi es una biblioteca PDF nativa en Object Pascal para Delphi y C++Builder. Su núcleo de renderizado rasteriza una página en un bitmap y puede emitir BMP, PNG, JPEG, WMF y unos cuantos formatos más. Lo que no hacía hasta hace poco era devolver un bitmap monocromo real, ni renderizar solo una parte de una página. Ambas funciones llegaron en la v3.83.0, y las dos se construyeron como capas de conveniencia muy finas sobre el renderer existente, no como cambios en el propio rasterizador. Esa restricción lo es todo

Por qué convertir a monocromo después de renderizar, no dentro del renderer

La forma obvia de producir una imagen de 1 bit es decirle al rasterizador que dibuje en 1 bit. Esa también es la forma de romper todo lo demás. El bitmap interno del renderer se crea con un PixelFormat := pf24bit incrustado a pelo en el constructor de PDFlibRenderer, y esa superficie de 24 bits se comparte entre todas las rutas de renderizado: exportación PNG, la vista previa sobre un contexto de dispositivo, salida JPEG, todo ello. Si lo cambias a pf1bit en el origen, no has añadido una función monocroma, sino que has degradado la fidelidad de color para todos los llamadores de la biblioteca y te has comprometido a depurar una docena de regresiones aguas abajo

Así que RenderPageToMonochromeFile toma la ruta opuesta. Renderiza la página con normalidad, a un BMP temporal de 24 bits, y solo después la reduce a 1 bit como paso de posprocesado. El renderer queda intacto. El comportamiento monocromo vive por completo en el método de conveniencia, lo que significa que no puede afectar a nadie que no lo llame. Este es el tipo de compensación que conviene nombrar explícitamente: un posprocesado paga una asignación extra de bitmap y un archivo temporal, y a cambio mantiene un núcleo crítico completamente fuera de alcance. Para una función pensada para servir casos extremos de fax y archivo, esa es la elección correcta

Pipeline de PDF Library for Delphi que muestra una página PDF renderizada a un BMP temporal de 24 bits, colapsada por un blit GDI HALFTONE a un mapa de bits monocromo de 1 bit, y consumida por flujos de fax, archivo y OCR
Los nuevos métodos renderizan primero una página a todo color y después reducen el raster terminado fuera del renderer. Pasarelas de fax, almacenes de archivo y frontales de OCR reciben un mapa de bits pf1bit auténtico
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI es la resolución clásica de fax Group 4; el índice de página está basado en 1
    Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
  finally
    Pdf.Free;
  end;
end;

Cómo ocurre realmente el paso a 1 bit

La conversión descendente se apoya en GDI y no en un bucle de umbralización hecho a mano, y la elección importa para la calidad de salida. Dentro del método, el bitmap temporal de 24 bits se carga en un TBitmap, se crea un segundo TBitmap con PixelFormat := pf1bit en las mismas dimensiones, y los píxeles pasan con un único blit

PDF Library for Delphi: Detalle del colapso GDI comparando un stretch blit HALFTONE que trama los grises en patrones de puntos frente al umbral predeterminado BLACKONWHITE de bloques
Dentro de RenderPageToMonochromeFile un único StretchBlt traslada cada píxel a una superficie pf1bit de tamaño idéntico. Con HALFTONE activo, los grises se convierten en tramas de puntos de tinta difuminados en lugar de las formas en bloques que produce el umbral por defecto
// dentro de RenderPageToMonochromeFile, después de cargar el ColorBmp de 24 bits
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE le indica a GDI que aplique dithering a la fuente de 24 bits hasta 1 bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
  ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');

El truco está en SetStretchBltMode con HALFTONE. Aunque origen y destino tienen el mismo tamaño, de modo que no hay escalado, el modo de estirado sigue determinando cómo GDI asigna colores a la paleta de 1 bit. HALFTONE hace que aplique tramado halftone, convirtiendo las zonas grises y los bordes de texto suavizado en patrones de puntos negros y blancos en lugar de recortarlos a secas al color más cercano de los dos. Si quitas la llamada al modo, o usas el BLACKONWHITE predeterminado, el contenido en escala de grises se posteriza en formas umbralizadas y toscas. Para salida de documentos escaneados y preprocesado OCR, el resultado con tramado casi siempre es lo que quieres

Un detalle no es negociable y es fácil equivocarse: el render temporal debe ser un BMP. RenderPageToMonochromeFile llama al renderer general con un código de opciones de 0, que es BMP. El argumento de opciones de RenderPageToFile es un pequeño enum entero, y los valores no son intercambiables para este propósito: 0 es BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, y así sucesivamente. Después, el convertidor hace TBitmap.LoadFromStream sobre el archivo temporal. Si le das un WMF, pasando 2, esa carga lanza "Bitmap image is not valid", porque un Windows Metafile es un flujo de registros vectoriales, no un DIB. La conversión descendente a monocromo es una operación raster de principio a fin, así que el intermedio tiene que ser un formato raster

Renderizar solo una subregión de una página

El segundo método, RenderPageRegionToFile, renderiza solo un rectángulo de la página en lugar de la página completa. Los casos de uso son familiares en cuanto has construido cualquier visor de documentos: recortar un bloque de firma de un contrato, generar una tesela para una ampliación de un dibujo grande o extraer una región sellada para una miniatura sin pagar el coste de rasterizar toda la página a DPI altos. La firma es sencilla

PDF Library for Delphi: Renderizado de recorte de región en puntos PDF: un rectángulo 72,72,180,72 sobre una página PDF renderizada a resolución completa se convierte en un mapa de bits de 375 por 150 píxeles a 150 DPI porque el recorte corta en lugar de escalar
RenderPageRegionToFile recorta una ventana medida en puntos PDF de un render a resolución completa. El mapa de bits de salida se dimensiona a partir de width y height por DPI entre 72, nunca de una página entera reducida
// Clip es "Left,Top,Width,Height" en puntos PDF (72 pt = 1 pulgada)
// Aquí: una caja de 2.5in x 1in, a una pulgada desde la esquina superior izquierda de la página
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');

La cadena de recorte es un rectángulo de cuatro dobles separados por comas en puntos PDF, analizado manualmente dentro del método para sortear las peculiaridades de la configuración regional y de DelimitedText. A partir de la anchura y la altura, el método calcula el tamaño del bitmap de salida como Round(Width * DPI / 72) por Round(Height * DPI / 72), reserva un bitmap pf24bit en memoria con ese tamaño exacto y renderiza en su contexto de dispositivo a través de RenderPageToDCClip. El archivo resultante contiene solo el rectángulo recortado, dimensionado para la región y no para la página completa

El parámetro de recorte que no hacía nada

Aquí es donde el trabajo era más fino de lo que parecía. RenderPageToDCClip llevaba un parámetro Clip desde hacía mucho tiempo, y era una mentira. La llamada aceptaba el argumento, lo pasaba a TPDFPageTree.RenderPageToDC, y esa implementación lo ignoraba por completo, sin entregárselo nunca al renderer. Podías pasar cualquier rectángulo y obtener de vuelta la página entera. Cualquiera que hubiera conectado RenderPageToDCClip esperando un recorte estaba recibiendo un render de página completa y, según su maquetación, quizá ni lo había notado

La v3.83.0 conectó el cable. RenderPageToDC ahora analiza el mismo rectángulo de puntos "Left,Top,Width,Height" y lo aplica como una región de recorte GDI real en el contexto de dispositivo de destino antes de que el renderer dibuje. La conversión de puntos a píxeles de dispositivo es el factor de escala habitual DPI / 72, aplicado a los cuatro bordes. La secuencia alrededor del render es el clásico baile de guardar, recortar y restaurar

// dentro de TPDFPageTree.RenderPageToDC, cuando Clip no está vacío
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... el renderizador dibuja la página aquí ...
// en el bloque finally:
RestoreDC(TargetDC, -1);

El par SaveDC / RestoreDC(-1) es lo que hace que esto sea seguro para llamarlo repetidamente: la región de recorte se apila en la pila de estado del DC, la página se dibuja y el recorte original vuelve a extraerse pase lo que pase con el renderizado. RestoreDC(TargetDC, -1) restablece el estado guardado más recientemente, que es el idiomatismo estándar para equilibrar guardar y restaurar. Si omites la restauración, un llamador que reutilice el mismo DC para un render de página completa posterior lo encontraría misteriosamente recortado a la última región. Corregir el parámetro muerto también corrigió gratis RenderPageRegionToFile, ya que ese nuevo método pasa exactamente por esta ruta

Un punto de comportamiento que conviene fijar: el recorte corta, no escala. La página sigue rasterizándose a la DPI que pediste, en su posición normal, y la región de recorte simplemente descarta todo lo que queda fuera del rectángulo. No estás ampliando la región para que ocupe la salida; estás sacando una ventana del renderizado a resolución completa. Si quieres magnificar una región, sube la DPI. Las coordenadas del rectángulo se interpretan en el espacio de dispositivo después del escalado de puntos a píxeles, medidas desde la esquina superior izquierda de la superficie renderizada, así que planifica Left y Top desde la parte superior de la página hacia abajo. Para una visita más profunda sobre cómo PDF Library for Delphi conduce un contexto de dispositivo para salida en pantalla, el artículo complementario sobre vista previa de impresión y salida de contexto de dispositivo repasa la misma tubería DC desde el lado de la visualización

La frontera honesta: BMP de 1 bit, no TIFF G4

Sería fácil vender esto como "salida lista para fax", así que aquí va el límite dicho con claridad. RenderPageToMonochromeFile produce un BMP pf1bit. No produce un TIFF CCITT Group 4, que es el formato que suele esperar un flujo de fax real o un archivo TIFF. La razón es concreta y no una omisión: la unidad CCITT de PDF Library for Delphi actualmente decodifica flujos G4, pero no tiene un encoder G4. Sin un encoder no hay dónde escribir trazos monocromos comprimidos, así que la ruta monocroma se queda en un DIB de 1 bit sin comprimir

En la práctica sigue siendo útil. Un BMP de 1 bit es el formato de píxel correcto, con tramado y listo, y la mayoría de las cadenas de fax, archivo u OCR lo ingerirán encantadas o lo convertirán a G4 ellas mismas con un paso posterior. Pero si tu requisito es literalmente un TIFF Group 4 directamente salido de la biblioteca, todavía no es eso, y deberás planear una etapa de compresión propia. Saber dónde termina una función vale tanto como saber qué hace

Ambos métodos son deliberadamente pequeños, y esa es la lección de diseño que conviene llevarse de esta página: una API de conveniencia que se apoya sobre un renderer puede añadir capacidad real, salida monocroma y recorte de regiones, sin meterse en el rasterizador y desestabilizar a todos los demás llamadores. Cuando necesites elegir entre motores de renderizado para la rasterización subyacente, el resumen sobre renderizado PDF multimotor en Delphi analiza los compromisos en profundidad. Para ver la superficie completa de renderizado y el resto de la API, la página de producto de PDF Library for Delphi Delphi PDF Library muestra el panorama completo