Artículo técnico

Kernels de downsampling y dithering de impresión en HotPDF

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

Curvas de peso de los tres kernels de downsampling de HotPDF: rkHalftone delega en la ruta GDI HALFTONE de clase bilineal con soporte de uno, rkBicubic ejecuta una cúbica Catmull-Rom separable con soporte de dos, y rkLanczos3 ejecuta un sinc con ventana de soporte de tres, cambiando aproximadamente un orden de magnitud de velocidad por fotografías visiblemente mejores
Los tres valores de kernel se sitúan en puntos genuinamente distintos de la curva velocidad-calidad: una ruta GDI de clase bilineal, una cúbica Catmull-Rom y un sinc con ventana de tres lóbulos, y los kernels separables normalizan los pesos para que nada produzca ringing más allá del negro o del blanco

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

Gráfica de la rama exterior bicúbica Catmull-Rom de HotPDF mostrando por qué se esconde un coeficiente equivocado: la errata que escribe -5 en lugar de -4 en la forma de Horner anidada sigue evaluando y deja W(1) en -1 y W(2) en -2 donde se exige cero, así que afirmar que W(0) es igual a 1 más las dos restricciones de cero lo caza en segundos
Un kernel roto nunca hace crash, solo devuelve números que parecen plausibles, que es por lo que el ojo no puede cazar una errata en un coeficiente. W(0) = 1 con ceros en más y menos uno y dos es un test unitario de tres líneas

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

Colocación en el pipeline de render del dithering Floyd-Steinberg de HotPDF: RenderOutputDither corre después de la composición de página sobre el raster de 24 bits terminado, umbraliza la luminancia al 50 por ciento y difunde cada error de cuantización a la derecha y abajo con pesos 7/16, 3/16, 5/16 y 1/16 a través de un buffer de fila que debe acumular, produciendo salida monocroma de un bit
El dithering va después de la composición porque reduce un raster terminado a un bit, no por cómo se escalaron las imágenes. Los pesos de difusión suman uno, y el buffer de fila tiene que acumular en lugar de sobrescribir
// 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