HotPDF expone tres kernels de downsampling de imagen mediante la propiedad ImageDownsampleKernel y una pasada Floyd-Steinberg aparte mediante RenderOutputDither. La primera controla cómo quedan las fotografías después de encogerlas para cumplir un presupuesto de tamaño, la segunda controla cómo quedan después de que una página se reduce a blanco y negro. Ninguno está activo por defecto, y ambos son opt-in por la misma razón: cuestan tiempo real
La presión que lleva a la gente hasta aquí es conocida. Un contrato escaneado de 60 MB tiene que salir por una pasarela de correo que rechaza cualquier cosa por encima de 10 MB, o un lote de extractos tiene que aterrizar en un dispositivo monocromo estilo fax que renderiza cada píxel gris como papel o tóner. Ambos problemas son problemas de remuestreo, y ambos tienen una respuesta rápida que se ve mal y una respuesta lenta que se ve bien
En qué se diferencian de verdad los tres kernels
THPDFResampleKernel tiene tres valores, y se sitúan en puntos genuinamente distintos de la curva velocidad-calidad. rkHalftone delega en la histórica ruta GDI StretchBlt con el modo HALFTONE, que pese al nombre es un filtrado de clase bilineal: rápido, adecuado para line art y capturas de pantalla, y proclive a los bordes duros que uno reconoce al instante en fotografías reescaladas. rkBicubic ejecuta un kernel separable Catmull-Rom, y rkLanczos3 ejecuta un sinc con ventana separable de soporte de tres lóbulos
Ambos kernels separables corren como dos pasadas, horizontal y luego vertical, con 6 a 12 taps por píxel de destino en Pascal puro. Eso es aproximadamente un orden de magnitud más lento que la ruta GDI, que es justo por lo que rkHalftone sigue siendo el valor por defecto. En un lote nocturno de miles de páginas la diferencia es una decisión de planificación, no una preferencia. En un documento único que un usuario está esperando, Lanczos3 es casi gratis y visiblemente mejor
Dos propiedades de la implementación merecen conocerse porque determinan lo que la salida puede y no puede hacer. Los bordes se fijan por replicación del borde en lugar de envolver o desvanecer, y los pesos se normalizan por píxel de destino. Juntas, esas dos cosas significan que el resultado nunca produce ringing por debajo del negro o por encima del blanco, así que el clásico halo de overshoot de Lanczos alrededor de un borde duro no aparece como artefactos recortados en la imagen codificada
var
Pdf: THotPDF;
Info: THPDFLoadedResourceOptimizationInfo;
Changed: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('scanned-contract.pdf');
Pdf.ImageDownsampleKernel := rkLanczos3; // fije antes de la llamada
Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
if Changed > 0 then
begin
Writeln('resampled images: ', Info.DownsampledImageCount);
Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
Pdf.SaveToFile('scanned-contract-150dpi.pdf');
end;
finally
Pdf.Free;
end;
end;
El argumento MinimumSavingsBytes, 4096 arriba, es el guard que mantiene honesta la operación. Recodificar una imagen que ya estaba eficientemente comprimida puede producir un stream más grande que el original, y un downsampler que reemplaza a ciegas cada imagen ocasionalmente agrandará el fichero al que le pidieron encogerse. El umbral dice: solo comprometa el reemplazo cuando ahorre al menos estos bytes. PreservedCalibratedImageCount informa de la otra decisión conservadora, las imágenes dejadas intactas porque llevan un espacio de color calibrado que el remuestreo comprometería
¿Por qué un coeficiente polinómico equivocado es tan difícil de ver?
Porque un kernel de interpolación roto no hace crash ni lanza una excepción, simplemente produce una imagen que se ve sutilmente mal de una manera que nadie puede atribuir. El kernel Catmull-Rom es cúbico por tramos, y su rama exterior en forma de Horner anidada es ((-0.5t + 2.5)t - 4)t + 2. Escriba ese coeficiente central como -5 en lugar de -4 y la función sigue evaluando, sigue devolviendo números en un rango plausible y sigue produciendo una imagen
El daño aparece como W(1) evaluando a -1 donde debe ser 0. Los pesos negativos se acumulan, la suma se recorta a cero, y el síntoma visible es un gradiente cuyo extremo izquierdo se va a negro y un borde de paso que pierde sus tonos intermedios. Nada en el fallo apunta a un polinomio. La comprobación que lo caza en segundos es aritmética y no visual: un kernel de interpolación debe satisfacer W(0) = 1 y W(±1) = W(±2) = 0, y cualquier kernel que falle esos tres puntos tiene un error de coeficiente, punto. Afirme esos tres valores en un test unitario y toda la clase de defectos por errata desaparece
Dithering Floyd-Steinberg, y dónde va en el pipeline
La pasada de dithering es un problema distinto del remuestreo y vive en otro punto del pipeline. RenderOutputDither aplica difusión de error Floyd-Steinberg después de la composición de página, que es la única colocación que tiene sentido para una previsualización de impresión monocroma o una exportación estilo fax: la operación va de reducir un raster terminado a un bit por píxel, no de cómo se escalaron las imágenes individuales en la entrada
El algoritmo en sí es corto. La luminancia se umbraliza al 50 por ciento, y el error de cuantización se difunde a cuatro vecinos con los clásicos pesos 7/16, 3/16, 5/16 y 1/16, hacia la derecha, abajo a la izquierda, abajo y abajo a la derecha. El píxel de salida es 0 o 255 en todos los canales. Lo que le da la alternativa ingenua en cambio, un umbral duro sin difusión, convierte una fotografía en una silueta y pierde cada medio tono que llevaba el contenido
// Dithering en el render para un dispositivo de previsualización monocromo
Pdf.RenderOutputDither := True;
// O aplique la misma pasada a un bitmap que ya posee. El bitmap debe
// ser pf24bit; la función devuelve False en lugar de adivinar
if not HPDFFloydSteinbergDitherBitmap(Preview) then
raise Exception.Create('dither expects a 24-bit bitmap');
// Acceso directo al kernel cuando remuestrea fuera del pipeline del documento
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
Small.SaveToFile('thumb.bmp');
finally
Small.Free;
end;
Hay un detalle de implementación en la difusión de error que muerde a todos una vez. El buffer de error de fila a fila tiene que acumular. Cada píxel de la fila siguiente recibe contribuciones de tres píxeles distintos de la fila actual, los taps 3/16, 5/16 y 1/16, y si el código asigna en lugar de sumar, cada escritura descarta la contribución previa y solo el último tap sobrevive. La imagen todavía parece ditherizada, que es lo que lo hace difícil de notar, pero la textura está mal y la reproducción tonal se desvía. El test que lo caza es cuantitativo: ditherice un campo uniforme de gris medio y exija que la cobertura interior quede entre el 40 y el 60 por ciento
Qué combinación debería usar un pipeline de reducción de tamaño
Empareje el kernel con lo que las imágenes realmente son, y trate el dithering como un asunto del dispositivo y no de la compresión. Para escaneos fotográficos que tienen que sobrevivir a un presupuesto de tamaño, rkLanczos3 a 150 o 200 DPI conserva el detalle que la gente nota mientras recorta el recuento de píxeles por un factor de cuatro o más. Para capturas de pantalla, diagramas y line art, rkHalftone está genuinamente bien y es mucho más rápido, porque esas imágenes tienen pocos gradientes tonales que preservar. Para un lote mixto donde no puede inspeccionar cada imagen, rkBicubic es el término medio razonable: mejor que bilineal, aproximadamente la mitad de taps que Lanczos3
El downsampling es una palanca entre varias, y no siempre la más grande. Los escaneos bilevel suelen responder mucho mejor al codificador cubierto en la compresión bilevel JBIG2 nativa en Delphi, donde la ganancia viene de los diccionarios de símbolos y no del recuento de píxeles. Antes de decidir, ayuda saber qué hay realmente en el fichero, para lo que está extraer imágenes y sus filtros de decodificación: un inventario de objetos de imagen y su compresión existente le dice si el remuestreo tiene algo que ganar
Si está construyendo la superficie de previsualización que muestra el resultado, la misma ruta de renderizado documentada en renderizar una página PDF a un bitmap es donde RenderOutputDither hace efecto, así que la previsualización ditherizada y la salida ditherizada vienen de una sola ruta de código y no de dos implementaciones que se separan
El principio amplio detrás de ambas funciones es que los ajustes de calidad deberían ser explícitos y reversibles. HotPDF mantiene el comportamiento histórico como valor por defecto para que una aplicación existente actualice sin un cambio sorpresa en la salida o los tiempos, y pone las rutas de mejor aspecto y más lentas a una asignación de propiedad de distancia. Ambas forman parte del componente PDF Delphi HotPDF, junto a la maquinaria de optimización de recursos y renderizado sobre la que se construyen