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 PDFlibPas acaba diciendo algo útil sobre cómo ampliar un renderer que preferirías no reescribir
PDFlibPas 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
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 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
// 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 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
// 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 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
// 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 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 PDFlibPas 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 PDFlibPas 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 PDFlibPas Delphi PDF Library muestra el panorama completo