Un gateway de fax no quiere tu renderizado de página en 24 bits. Tampoco lo quiere el flujo de archivo que guarda un millón de facturas con aspecto de escaneo, ni el front-end de OCR que umbraliza todo a blanco y negro antes de buscar una sola letra. Los tres quieren lo mismo: un bitmap limpio de 1 bit, un bit por píxel, donde cada punto sea tinta o papel. Si les das un BMP a color, descartarán igual 23 bits por píxel, por lo general con un tramado peor que el que habrías podido hacer tú mismo. La pregunta interesante es dónde debe ocurrir esa conversión descendente, y la respuesta en PDFlibPas termina diciendo algo útil sobre cómo extender un renderizador que preferirías no reescribir
PDFlibPas es una biblioteca nativa de PDF en Object Pascal para Delphi y C++Builder. Su núcleo de renderizado rasteriza una página a un bitmap y puede emitir BMP, PNG, JPEG, WMF y algunos otros formatos. Lo que no hacía hasta hace poco era devolver un verdadero bitmap monocromo, ni renderizar solo una parte de una página. Ambas funciones llegaron en v3.83.0, y ambas se construyeron como capas de conveniencia delgadas sobre el renderizador existente, no como cambios al rasterizador en sí. Esa restricción es toda la historia
Por qué convertir después de renderizar, no dentro del renderizador
La forma obvia de producir una imagen de 1 bit es decirle al rasterizador que dibuje en 1 bit. También es la forma de romper todo lo demás. El bitmap interno del renderizador se crea con un PixelFormat := pf24bit codificado de forma fija en el constructor PDFlibRenderer, y esa superficie de 24 bits se comparte por todas las rutas de renderizado: exportación PNG, la vista previa en contexto de dispositivo, salida JPEG, todo. Si lo cambias a pf1bit en el origen, no agregas una función monocroma, degradas la fidelidad de color para todos los consumidores de la biblioteca y te inscribes para depurar una docena de regresiones aguas abajo
Así que RenderPageToMonochromeFile toma la ruta opuesta. Renderiza la página de forma normal, en un BMP temporal de 24 bits, y solo después la reduce a 1 bit como paso de posprocesamiento. El renderizador no se toca. El comportamiento monocromo vive por completo en el método de conveniencia, lo que significa que no puede afectar a nadie que no lo invoque. Este es el tipo de intercambio que vale la pena nombrar explícitamente: un posprocesamiento paga una asignación extra de bitmap y un archivo temporal, y a cambio mantiene completamente fuera de alcance un núcleo que sostiene todo. Para una función que existe para atender casos límite de fax y archivo, ese es el lado correcto de la balanza
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// 200 DPI is the classic Group 4 fax resolution; page index is 1-based
Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
finally
Pdf.Free;
end;
end;
Cómo sucede de verdad la reducción a 1 bit
La conversión descendente se apoya en GDI en lugar de un bucle de umbral hecho a mano, y esa elección importa para la calidad de salida. Dentro del método, el BMP 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 solo blit:
// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 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 el origen y el destino tienen el mismo tamaño, así que no ocurre ningún escalado, el modo de stretch 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 con antialiasing en patrones de puntos negros y blancos en lugar de un corte duro hacia el color más cercano de los dos. Si quitas la llamada al modo, o usas el valor predeterminado BLACKONWHITE, el contenido en escala de grises se posteriza en formas en bloques umbralizadas. Para salida de documentos escaneados y preprocesamiento de OCR, el resultado con tramado es casi siempre lo que quieres
Un detalle no negociable y fácil de hacer mal: el renderizado temporal debe ser un BMP. RenderPageToMonochromeFile llama al renderizador general con un código de opciones de 0, que es BMP. El argumento de opciones en RenderPageToFile es una pequeña enumeración entera, y los valores no son intercambiables para este propósito: 0 es BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, y así sucesivamente. Luego el convertidor descendente hace TBitmap.LoadFromStream sobre el archivo temporal. Si le pasas un WMF, usando 2, esa carga arroja "Bitmap image is not valid", porque un Windows Metafile es un flujo de registros vectoriales, no un DIB. La conversión monocroma es una operación raster de punta a punta, 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 construyes cualquier visor de documentos: recortar un bloque de firma de un contrato, generar un mosaico para un mapa ampliado de un plano grande, o tomar una región estampada para una miniatura sin pagar el costo de rasterizar toda la página a alto DPI. La firma es sencilla:
// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');
La cadena de recorte son cuatro double separados por comas en puntos de PDF, analizados manualmente dentro del método para evitar las particularidades de la configuración regional y de DelimitedText. A partir del ancho y el alto, 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 exactamente de ese tamaño y renderiza en su contexto de dispositivo a través de RenderPageToDCClip. El archivo resultante contiene solo el rectángulo recortado, dimensionado según la región y no según la página completa
El parámetro de recorte que no hacía nada
Aquí es donde el trabajo fue más fino de lo que parece. 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 entregarlo nunca al renderizador. Podías pasar cualquier rectángulo y recibir de vuelta la página completa. Cualquiera que hubiera conectado RenderPageToDCClip esperando un recorte recibía un render de página entera y, según su diseño, quizá ni siquiera lo notaba
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 real de recorte GDI en el contexto de dispositivo de destino antes de que el renderizador dibuje. La conversión de puntos a píxeles de dispositivo es el factor de escala usual DPI / 72, aplicado a los cuatro bordes. La secuencia alrededor del renderizado es el baile estándar de guardar, recortar y restaurar:
// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
Round(ClipLeft * ScaleFactor),
Round(ClipTop * ScaleFactor),
Round((ClipLeft + ClipWidth) * ScaleFactor),
Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
RestoreDC(TargetDC, -1);
El par SaveDC / RestoreDC(-1) es lo que permite llamarlo repetidamente con seguridad: la región de recorte se apila en el estado del DC, la página se dibuja y el recorte original se restaura sin importar cómo termine el renderizado. RestoreDC(TargetDC, -1) restaura el estado guardado más reciente, que es el idiom estándar para equilibrar guardar y restaurar. Si omites la restauración, un consumidor que reutilice el mismo DC para un render de página completa posterior encontraría, sin explicación, que quedó recortado a la última región. Corregir el parámetro muerto también corrigió RenderPageRegionToFile gratis, ya que ese nuevo método pasa exactamente por esta ruta
Un punto de comportamiento que conviene internalizar: el recorte corta, no escala. La página sigue rasterizándose al 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 acercando la región para que llene la salida; estás cortando una ventana de la renderización a resolución completa. Si quieres ampliar una región, sube el DPI. Las coordenadas del rectángulo se interpretan en espacio de dispositivo después de la conversión 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 revisión más profunda de cómo PDFlibPas maneja un contexto de dispositivo para salida en pantalla, el artículo complementario sobre vista previa de impresión y salida con contexto de dispositivo recorre la misma tubería DC desde el lado de la visualización
El límite honesto: 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 real de fax o un archivo TIFF. La razón es concreta y no una omisión: la unidad CCITT de PDFlibPas actualmente decodifica flujos G4, pero no tiene un G4 encoder. Sin un encoder no hay dónde escribir corridas monocromas comprimidas, así que la ruta monocroma se detiene en un DIB de 1 bit sin compresión
En la práctica eso 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 sin problema o lo convertirán a G4 con un paso posterior. Pero si tu requisito es literalmente un TIFF Group 4 directo desde la biblioteca, eso todavía no es este caso, y deberías planear tu propia etapa de compresión. Saber dónde se detiene una función vale tanto como saber lo que hace
Ambos métodos son deliberadamente pequeños, y esa es la lección de diseño que vale la pena llevarse de esta página: una API de conveniencia que se apoya sobre un renderizador puede sumar capacidad real, salida monocroma, recorte por región, sin meterse en el rasterizador y desestabilizar a los demás consumidores. Cuando necesites elegir entre motores de renderizado para la rasterización subyacente, el panorama general de renderizado PDF con varios motores en Delphi cubre los compromisos en profundidad. Para ver la superficie completa de renderizado y el resto de la API, la página del producto PDFlibPas Delphi PDF Library ofrece la imagen completa