Artículo técnico

Kernels de downsampling de imagen y dithering en HotPDF

HotPDF expone tres kernels de downsampling de imágenes a través de la propiedad ImageDownsampleKernel y una pasada separada de Floyd-Steinberg vía RenderOutputDither. La primera controla cómo se ven las fotografías después de que usted las achica para cumplir un presupuesto de tamaño; la segunda controla cómo se ven después de que una página se reduce a blanco y negro. Ninguna va activa por defecto, y ambas son opt-in por la misma razón: cuestan tiempo real

La presión que lleva a la gente aquí es conocida. Un contrato escaneado de 60 MB tiene que salir por una pasarela de correo que rechaza cualquier cosa de más de 10 MB, o un lote de estados de cuenta 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 resampling, y ambos tienen una respuesta rápida que se ve mal y una respuesta lenta que se ve bien

En qué se diferencian realmente los tres kernels

THPDFResampleKernel tiene tres valores, y viven en puntos genuinamente distintos de la curva de velocidad y calidad. rkHalftone delega en la histórica ruta GDI StretchBlt con el modo HALFTONE, que a pesar del nombre es filtrado de clase bilinear: rápido, adecuado para line art y screenshots, y proclive a esos bordes dentados que uno reconoce al instante en fotografías reescaladas. rkBicubic corre un kernel separable Catmull-Rom, y rkLanczos3 corre 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 justamente por lo que rkHalftone sigue siendo el default. En un lote nocturno de miles de páginas la diferencia es una decisión de scheduling, 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 bilinear con soporte de uno, rkBicubic corre un cúbico separable Catmull-Rom con soporte de dos, y rkLanczos3 corre 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 viven en puntos genuinamente distintos de la curva de velocidad y calidad: una ruta GDI de clase bilinear, un cúbico Catmull-Rom y un sinc con ventana de tres lóbulos, y los kernels separables normalizan pesos para que nada suene más allá del negro o el blanco

Dos propiedades de implementación valen la pena conocer porque determinan lo que la salida puede y no puede hacer. Los bordes hacen clamp por replicación de 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 suena por debajo del negro ni 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;   // fijar 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 ciegamente cada imagen ocasionalmente agranda el archivo que le pidieron achicar. El umbral dice: solo comprometa el reemplazo cuando ahorre al menos esta cantidad de bytes. PreservedCalibratedImageCount reporta la otra decisión conservadora, las imágenes dejadas intactas porque cargan un espacio de color calibrado que el resampling comprometería

¿Por qué un coeficiente polinomial equivocado es tan difícil de ver?

Porque un kernel de interpolación roto no truena ni lanza excepciones, 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 del medio como -5 en lugar de -4 y la función igual evalúa, igual devuelve números en un rango plausible, e igual produce una imagen

El daño aparece como W(1) evaluando a -1 donde debe ser 0. Los pesos negativos se acumulan, la suma hace clip en cero, y el síntoma visible es un gradiente cuyo extremo izquierdo va a negro y un borde de paso que pierde sus tonos intermedios. Nada en la falla apunta a un polinomio. La comprobación que lo caza en segundos es aritmética, no visual: un kernel interpolante 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 unit test y toda la clase de defectos por dedoazo desaparece

Gráfica de la rama exterior del cúbico Catmull-Rom de HotPDF que muestra por qué se esconde un coeficiente equivocado: el typo que escribe -5 en lugar de -4 en la forma de Horner anidada igual evalúa 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 truena, solo devuelve números que se ven plausibles, y por eso el ojo no puede cazar un typo de coeficiente. W(0) = 1 con ceros en más y menos uno y dos es un unit test de tres líneas

Dithering Floyd-Steinberg, y dónde vive en el pipeline

La pasada de dithering es un problema distinto del resampling 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 ubicación con sentido para una vista previa 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 el camino de 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, un umbral duro sin difusión, es convertir una fotografía en una silueta y perder cada tono medio que cargaba el contenido

Ubicació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 de 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 pertenece 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 al renderizar para un dispositivo de vista previa 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 reescala 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 siguiente fila 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 igual se ve dithered, que es lo que hace difícil notarlo, pero la textura está mal y la reproducción tonal se desvía. El test que lo caza es cuantitativo: ditheree un campo uniforme de gris medio y exija que la cobertura interior caiga entre el 40 y el 60 por ciento

¿Qué combinación debe 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, no de la compresión. Para escaneos fotográficos que deben sobrevivir a un presupuesto de tamaño, rkLanczos3 a 150 o 200 DPI conserva el detalle que la gente nota mientras corta el conteo de píxeles por un factor de cuatro o más. Para screenshots, diagramas y line art, rkHalftone genuinamente está 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 punto medio razonable: mejor que bilinear, con aproximadamente la mitad de los taps de Lanczos3

El downsampling es una palanca entre varias, y no siempre la más grande. Los escaneos bilevel suelen responder mucho mejor al encoder cubierto en compresión bilevel JBIG2 nativa en Delphi, donde la ganancia viene de diccionarios de símbolos y no de conteos de píxeles. Antes de decidir, ayuda saber qué hay realmente en el archivo, que es para lo que sirve extraer imágenes y sus decode filters: un inventario de objetos de imagen y su compresión existente le dice si el resampling tiene algo que ganar

Si está construyendo la superficie de vista previa que muestra el resultado, la misma ruta de render documentada en renderizar una página PDF a un bitmap es donde RenderOutputDither hace efecto, así que la vista previa dithered y la salida dithered vienen de una sola ruta de código en lugar de dos implementaciones que se separan

El principio amplio detrás de ambas funciones es que los ajustes de calidad deben ser explícitos y reversibles. HotPDF conserva el comportamiento histórico como default para que una aplicación existente actualice sin un cambio sorpresa en salida ni tiempos, y deja las rutas de mejor apariencia y más lentas a una asignación de propiedad de distancia. Ambas son parte del HotPDF Delphi PDF component, junto con la maquinaria de optimización de recursos y renderizado sobre la que se construyen