losLab PDF Library for Delphi comprime una página escaneada con Mixed Raster Content mediante TPDFlib.CompressPageAsMRC. El método renderiza la página, umbraliza una máscara de primer plano de 1 bit a resolución completa, reduce el resto a un fondo JPEG, y reconstruye la página como dos imágenes: un fondo /DCTDecode y una máscara stencil /CCITTFaxDecode pintada en el color de relleno vigente
La página para la que esto existe es la que llena cualquier archivo de documentos: un escaneo a color de 300 DPI de una factura o un contrato, guardado como un único JPEG grande. Codificadlo con una calidad que mantenga el texto legible y el fichero es enorme; bajad la calidad y las letras desarrollan halos y terminales emborronados, porque lo peor que puede hacer JPEG es un borde negro nítido sobre papel blanco. Mixed Raster Content deja de pedirle a un códec que haga dos trabajos
Por qué un único JPEG vuelve el texto escaneado borroso y grande a la vez
Un único JPEG trata mal el texto escaneado porque el códec DCT gasta sus bits en gradientes suaves y aproxima los bordes duros con coeficientes de alta frecuencia que la cuantización tira primero. El texto de un escaneo no es casi nada más que bordes duros. Mantener esos bordes limpios obliga a una calidad alta en toda la página, incluida la textura del papel, el logo y la mancha de café, ninguno de los cuales lo necesitaba
La idea de MRC divide la página según lo que cada región es en realidad. La tinta se convierte en una imagen bilevel, donde un códec de fax como CCITT registra rachas de píxeles negros y blancos sin pérdidas y muy compactas. Todo lo demás, el tono del papel, las fotos, los fondos de color, se convierte en una imagen de tono continuo que se puede guardar a una fracción de la resolución, porque nadie lee un fondo a 300 DPI. La capa bilevel mantiene el texto nítido a resolución completa; la capa de fondo mantiene el color a bajo coste
Cómo divide CompressPageAsMRC una página en dos capas
TPDFlib.CompressPageAsMRC ejecuta un pipeline fijo por página. Renderiza la página al DPI solicitado en un bitmap de 24 bits, clasifica cada píxel con una luminancia ponderada Rec. 601 (299/587/114 por mil) contra Threshold, y escribe el resultado en una máscara de 1 bit a la misma resolución. En paralelo encoge el mismo render por BGScale con un promedio de caja, de modo que un BGScale de 4 convierte cada bloque de 4×4 píxeles en un píxel de fondo
El contenido de la página se sustituye entonces. Los operadores originales se eliminan, el fondo reducido se incrusta como una imagen /DCTDecode en /DeviceRGB (ISO 32000-1 §7.4.8), la máscara se codifica con el codificador CCITT de la biblioteca como una imagen /CCITTFaxDecode (§7.4.6) marcada con /ImageMask true, y ambas se dibujan a lo largo de todo el page box con la máscara encima. La firma es corta:
function CompressPageAsMRC(Page: Integer; DPI: Double;
BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
Pagees 1-based y debe estar dentro dePageCountDPIes la resolución de render del raster de origen, de 50 a 1200BGScalees el factor de reducción del fondo, de 1 a 8JPEGQualityes la calidad JPEG del fondo, de 1 a 100Thresholdes el corte de luminancia, de 1 a 255; los píxeles en o por encima son papel, los más oscuros son tintaOptionsestá reservado y debe ser 0
Un lote típico sobre un fichero escaneado se ve así. Llamad a SetFillColor justo antes de cada página, por un motivo que explica la siguiente sección
uses
SysUtils, PDFlibrary;
procedure CompressScannedFile(const SrcFile, DstFile: string);
var
Lib: TPDFlib;
P, Replaced: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(SrcFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0); // color de tinta de la capa stencil
// máscara a 300 DPI, fondo a 75 DPI, calidad JPEG 60, corte en luma 160
Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
if Replaced = 0 then
raise Exception.CreateFmt('MRC failed on page %d', [P]);
end;
if Lib.SaveToFile(DstFile) <> 1 then
raise Exception.CreateFmt('Cannot save %s', [DstFile]);
finally
Lib.Free;
end;
end;
Por qué el primer plano se pinta con el color de relleno vigente
La capa de primer plano es una stencil mask, y una stencil mask no tiene color propio. ISO 32000-1 §8.9.6.2 define una imagen con /ImageMask true como una forma de 1 bit: los valores de muestra marcan dónde va la pintura, y la pintura es el color nonstroking vigente en el graphics state. Eso es exactamente lo que hace barata la capa, y también por qué CompressPageAsMRC no toma ningún parámetro de color
TPDFlib.CompressPageAsMRC pinta la máscara con el color fijado por SetFillColor, así que el orden de las llamadas importa. El pintor emite el color de relleno de forma perezosa: SetFillColor registra el color y lo marca como cambiado, y el dibujo del stencil escribe el operador rg dentro de su propio bloque q/Q solo mientras ese cambio sigue pendiente. Dibujad una forma rellena en medio y el cambio pendiente lo consume esa forma en su lugar. Fijar el color directamente antes de cada página, como en el lote de arriba, elimina las conjeturas. Negro es la elección normal; un azul marino oscuro funciona en formularios de tinta azul donde queréis que la salida se parezca menos a una fotocopia
// Formulario de bolígrafo azul: renderizad la capa de tinta en un azul marino oscuro
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);
La colocación sigue el origen de coordenadas de la biblioteca. CompressPageAsMRC lee GetOrigin y dibuja ambas imágenes desde el borde superior del page box: coordenada superior 0 cuando el origen es arriba a la izquierda (SetOrigin(1)), coordenada superior PageHeight con el origen por defecto abajo a la izquierda. No necesitáis ajustar nada, pero explica por qué las capas caen correctamente sea cual sea el origen que vuestro pipeline ya usaba
Una consecuencia de la semántica stencil merece enunciarse sin rodeos: cada píxel clasificado como tinta toma el mismo color. Un sello rojo, una firma azul y texto negro de cuerpo acaban todos en un solo color de relleno en la capa de primer plano. Solo el JPEG de fondo conserva debajo un rastro tenue y de baja resolución de los colores originales
Elegir DPI, BGScale, Threshold y calidad JPEG
El valor de DPI decide la nitidez del texto, y 300 es el valor por defecto sensato para escaneos de oficina. La máscara se mantiene a la resolución completa del render, así que DPI fija qué trazo fino sobrevive al umbralizado. Ir por encima de la resolución del escaneo original no compra nada, porque el renderizador solo haría upsampling de la imagen de escaneo incrustada. La memoria es el otro límite: una página US Letter a 300 DPI se renderiza a 2550×3300 píxeles, 25.245.000 bytes de datos de píxeles de 24 bits antes de derivar máscara y fondo, y la misma página en el techo de 1200 DPI son 10200×13200 píxeles, unos 404 MB
BGScale cambia detalle del fondo por tamaño. A 300 DPI, un factor de 4 da un fondo de 75 DPI, 638×825 píxeles para Letter, de sobra para el tono del papel y áreas de color plano. Las páginas con fotografías o ilustraciones de semitono que el lector va a mirar de verdad merecen un 2; el texto plano sobre papel blanco aguanta un 6 u 8, ya que el fondo apenas es más que un tinte
Threshold es el parámetro que cambia el aspecto de la página. Subirlo clasifica más píxeles de gris medio como tinta, lo que engrosa trazos finos y marcas de lápiz pero también arrastra ruido del escáner y sombras del papel como motas. Bajarlo da un papel más limpio y letras más finas, a veces rotas. Valores en torno a 128 a 170 son un rango de partida razonable para texto negro sobre blanco; el compromiso es el mismo que se discute en renderizar páginas PDF a monocromo de 1 bit en Delphi, donde el mismo umbral de luminancia gobierna RenderPageToMonochromeFileEx. A diferencia de ese método, CompressPageAsMRC no tiene pasada de despeckle, así que los escaneos con ruido van mejor con un umbral algo más bajo
JPEGQuality importa menos que en un escaneo de una sola imagen, porque el fondo ya no lleva ningún borde de texto. Una calidad de 50 a 70 suele ser invisible en un fondo de 75 DPI detrás de una máscara nítida
Qué os dice el valor devuelto, y qué no
TPDFlib.CompressPageAsMRC devuelve el tamaño en bytes del contenido de página que sustituyó, y 0 cuando algo falló. Es un indicador de éxito, no una medida de ahorro. En una página escaneada típica el content stream original son unas pocas docenas de bytes, justamente los operadores para colocar una imagen, así que un número positivo pequeño es el resultado normal incluso cuando la página encoge megabytes. Una página cuyo content stream original estaba vacío informa 0 aunque fue reconstruida
El comportamiento en caso de fallo no es atómico, y esa es la parte para la que hay que diseñar. Las comprobaciones de argumentos, el render y la codificación JPEG corren antes de tocar la página, así que un BGScale fuera de rango, un JPEGQuality de 101 o un fallo de render dejan la página exactamente como estaba. Una vez limpiado el contenido original, sin embargo, un fallo al incrustar una de las capas o dibujarla también devuelve 0, y en ese punto la página puede estar ya parcialmente reconstruida. Tratad cualquier 0 como motivo para descartar el documento en memoria en lugar de guardarlo, y nunca escribáis el resultado sobre el fichero de origen
Para saber qué ahorró realmente la conversión, medid el documento. Comparar la longitud de SaveToString antes y después es la comprobación más rápida; para una respuesta por categorías, la auditoría de tamaño de ficheros PDF con AuditDocumentSpace muestra si el peso salió de las imágenes como se esperaba
function MRCSavings(Lib: TPDFlib): Int64;
var
Before: Int64;
P: Integer;
begin
Before := Length(Lib.SaveToString);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0);
if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
end;
Result := Before - Length(Lib.SaveToString);
end;
Dónde este MRC de dos capas se queda corto respecto a ITU-T T.44
CompressPageAsMRC implementa un MRC deliberadamente simplificado. El modelo completo de ITU-T T.44 usa tres capas por stripe: una imagen de fondo, una imagen de primer plano que lleva los colores de tinta, y una máscara bilevel que elige entre ellas píxel a píxel, con sitio para varios pares de máscara y primer plano en una página. En términos PDF la tercera capa sería una imagen de primer plano de color a baja resolución con una máscara explícita (§8.9.6.3) en lugar de un stencil pintado de un color. PDF Library for Delphi se salta la capa de color de primer plano, que es lo que hace el resultado tan pequeño y también por qué la tinta de color colapsa en el color de relleno
- El método sirve para texto negro o de un solo color sobre papel escaneado; los documentos con código de color, el texto resaltado y los sellos de color pierden su color de tinta
- La página reconstruida es raster puro: cualquier texto real, arte vectorial o caracteres seleccionables de la página original se vuelven píxeles, así que ejecutadlo sobre escaneos, no sobre páginas born-digital, donde el subsetting de fuentes y la reducción de imágenes son las herramientas correctas
- La máscara usa el codificador CCITT de la biblioteca en modo bidimensional T.4, escrito como
/CCITTFaxDecodecon/K 1; el codificador también tiene un modo Group 4 medianteAddImageCCITTFromBitmapStream, peroCompressPageAsMRCno lo expone, y no usa JBIG2 - Las anotaciones y los campos de formulario no se aplanan; se quedan como objetos por encima del contenido reconstruido
- El fondo es
/DeviceRGB, así que la salida no sirve para flujos PDF/X-1a, ySetFillColoren sí se rechaza en modo PDF/X-1a - Un escaneo que ya necesita OCR debería reconocerse antes del MRC, mientras los píxeles originales aún existen a profundidad de color completa
FAQ
¿CompressPageAsMRC mantiene el texto buscable?
No. CompressPageAsMRC sustituye el contenido de la página por dos imágenes, así que cualquier operador de texto de la página original, incluida una capa de texto OCR invisible, se elimina con ella. Ejecutad OCR sobre la página comprimida después, o devolved la capa de texto tras la compresión
¿Puedo comprimir solo algunas páginas de un documento?
Sí. CompressPageAsMRC trabaja sobre una página 1-based por llamada, así que elegís las páginas e incluso podéis variar BGScale y Threshold por página, por ejemplo un factor de 2 para una inserción de foto y 6 para páginas de texto plano
¿Qué valor debe tener el parámetro Options?
Pasad 0. Options está reservado para uso futuro en la versión actual y no hay bits definidos, así que un valor distinto de cero no significa nada hoy
CompressPageAsMRC llegó en losLab PDF Library for Delphi v3.561.0 junto con los bloques de render, JPEG, CCITT y máscara stencil con los que está ensamblado, todos disponibles como métodos públicos si necesitáis una variante del pipeline. La biblioteca, su soporte de C++Builder y la referencia completa de la API están en la página de producto de PDF Library for Delphi