Dos quejas llegan la semana posterior al envío de una función de compresión: el contrato escaneado ahora tiene letras escalonadas y borrosas, y el logo transparente de la portada queda dentro de un halo pálido. PDFiumPas responde a ambas en un solo lugar. TPdf.OptimizeImages mide cada imagen antes de reducirla, después elige un kernel de remuestreo y acumula el color en forma consciente del alfa
No siempre fue así. Antes de v3.100.0 el mismo método reducía cada imagen no binivel con un paso fijo de vecino más cercano, que es exactamente el algoritmo que produce ambas quejas: muestrea por puntos un píxel de origen por píxel de salida, y trata el RGB que hay bajo un píxel totalmente transparente como si un lector fuera a verlo jamás. La reescritura en v3.100.0 sustituye ese camino único por cinco kernels, una regla de selección medida y un presupuesto explícito de memoria de trabajo
¿Por qué el submuestreo hace que el texto escaneado se vea dentado?
Porque el muestreo por puntos responde a la pregunta equivocada. Cuando un escaneo de 300 PPP se redestina a 150 PPP, cada píxel de destino representa un bloque de dos por dos de píxeles de origen, y el vecino más cercano conserva uno de los cuatro y descarta el resto. Cuál sobrevive depende del redondeo, así que un borde de trazo que estaba suavemente antialiased en el origen se convierte en una moneda al aire por píxel. El resultado es la clásica escalera con aliasing a lo largo de los bordes de los glifos, más moiré en las regiones de semitono donde las muestras descartadas llevaban el patrón. Esto importa más en un PDF que en pantalla porque el daño es permanente. Un XObject de imagen lleva sus datos de muestra junto a /Width, /Height y /BitsPerComponent (ISO 32000-1 §8.9.5), y el remuestreo reescribe los tres dentro del archivo. Un zoom malo en un visor es un fotograma que puedes redibujar, y PDFiumPas tiene maquinaria aparte para eso en caché de renderizado y rendimiento de zoom. Un submuestreo malo es un documento nuevo que entregas al cliente
Cómo mide el detalle PDFiumPas y elige un kernel
PDFiumPas decide por imagen, no por documento. Antes de elegir un kernel calcula una puntuación normalizada de detalle de luminancia a partir de una rejilla de muestreo acotada: los pasos horizontal y vertical son (Width + 63) div 64 y (Height + 63) div 64, de modo que un escaneo de 12000 píxeles y una miniatura de 300 píxeles cuestan aproximadamente la misma barrida de 64 por 64. En cada posición muestreada suma la diferencia absoluta con el vecino de la derecha y el vecino de abajo, a lo sumo en tres canales, y después divide por el recuento de muestras por 255. La puntuación cae entre 0 y 1, donde los gráficos de negocio planos se sitúan cerca de cero y la textura fotográfica densa escala
La escalera de selección se ejecuta entonces en un orden fijo. Si ResampleFilter es cualquier cosa distinta de pirfAdaptive, ese filtro se usa tal cual. Si no: el contenido de 1 bit toma pirfBilevel; un ContentClass de piccLineArt toma pirfBox; un factor de escala de 4 o más toma también pirfBox, porque a esa reducción un promedio de área es a la vez la respuesta más barata y la más correcta; piccPhoto, una puntuación de detalle de 0.08 o superior, o un PreferredQuality de 0.9 o superior toma pirfLanczos con su kernel de tres lóbulos; una escala de 2 o más o una calidad de 0.7 o superior toma pirfBicubic de radio 2; todo lo restante toma pirfBilinear. Como TPdfImageOptimizeOptions.Default fija PreferredQuality en 0.85, una ejecución por defecto nunca recae en bilineal salvo que la reducción sea leve y el contenido plano
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// Valores por defecto: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, calidad 0.85, presupuesto de 64 MiB.
Options := TPdfImageOptimizeOptions.Default;
Options.TargetDpi := 150;
Options.MinDpiRatio := 1.5;
Options.ContentClass := piccAuto;
Options.PreferredQuality := 0.85;
if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
Pdf.SaveAs(OutputFile);
finally
Pdf.Free;
end;
end;
Solo se toca una imagen cuando el mayor de sus PPP de colocación horizontal y vertical, dividido por TargetDpi, alcanza MinDpiRatio. Esa guarda existe para que una foto de 160 PPP dirigida a un objetivo de 150 PPP no se recodifique por una ganancia del seis por ciento que cuesta una generación de calidad. Las imágenes por debajo de MinDimension en cualquier eje, 8 por defecto, se saltan como iconos o reglas
¿Por qué los logos transparentes adquieren un borde blanco?
Porque el color bajo un píxel totalmente transparente es arbitrario, y un promedio ponderado simple le deja votar. Exporta un logo desde una herramienta de diseño y el margen invisible suele ser blanco, o negro, o lo que fuera el lienzo; el canal alfa lo esconde, y una suma directa sobre la huella del kernel lo mezcla de vuelta en el borde visible con presteza. PDFiumPas lo evita acumulando muestras BGRA en forma premultiplicada y deshaciendo la premultiplicación solo en el píxel de destino
En concreto, cada muestra contribuyente añade channel * alpha * weight al acumulador de color, alpha * weight a un acumulador de alfa y weight a la suma de pesos. El color de destino se divide entonces por el acumulador de alfa en lugar de por la suma de pesos, y ese es el paso que importa: dividir por la suma de pesos arrastraría el color hacia los píxeles invisibles, mientras que dividir por el alfa acumulado reconstruye el color en el que las muestras visibles coincidieron realmente. El alfa de destino es una cantidad aparte, 255 * AlphaSum / WeightSum. Los formatos sin alfa dividen por la suma de pesos como siempre, el byte de relleno de un destino FPDFBitmap_BGRx se escribe como un 255 constante, y cada canal se acota entre 0 y 255 antes de almacenarse. Ese alfa normalmente proviene de una entrada de máscara suave en el diccionario de la imagen (ISO 32000-1 §11.4), que PDFium ya ha compuesto en el búfer BGRA que recibe el remuestreador
// Forma del bucle interno de acumulación, por muestra de origen contribuyente
if SrcFormat = FPDFBitmap_BGRA then
Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
Accumulated[Channel] := Accumulated[Channel] +
PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;
// ... y en el píxel de destino, deshacer la premultiplicación contra la suma de alfa
if SrcFormat = FPDFBitmap_BGRA then
begin
if Abs(AlphaSum) > 1E-12 then
ValueSum := Accumulated[Channel] / AlphaSum
else
ValueSum := 0;
end
else
ValueSum := Accumulated[Channel] / WeightSum;
Mantener el arte lineal de 1 bit fuera de la zona gris
Cualquier kernel continuo aplicado a un escaneo binivel produce gris, y el gris es precisamente lo que una imagen de estilo fax no puede contener. PDFiumPas deja por eso las imágenes de 1 bit en paz por defecto: PreserveBilevel es True en TPdfImageOptimizeOptions.Default, y tales imágenes caen en SkippedCount sin tocar. Ponlo a False y la ruta pirfBilevel toma el relevo en lugar de un kernel de suavizado. Esa ruta recorre el rectángulo de origen exacto que cubre cada píxel de destino, promedia la luminancia con los pesos 0.114, 0.587 y 0.299 en orden de memoria BGR, y umbraliza el resultado a 127.5 en un 0 o un 255 planos. Nada intermedio puede escribirse, así que los bordes siguen nítidos y no se forma halo gris alrededor de los trazos finos; el canal alfa de un origen BGRA se promedia normalmente, y un destino BGRx recibe el 255 constante. Si necesitas los píxeles subyacentes en lugar de un documento más pequeño, extraer imágenes de documentos PDF es la ruta aparte
¿Qué ocurre cuando una imagen excede el presupuesto de memoria de trabajo?
Se deja exactamente como estaba, y se cuenta. MaxWorkingBytes vale 64 MiB por defecto y se aplica dos veces. Antes de crear el bitmap de destino, PDFiumPas rechaza la imagen si ancho por alto por bytes por píxel excede el presupuesto. Tras lograr FPDFBitmap_CreateEx comprueba de nuevo usando el stride real por la altura, porque el relleno de fila puede empujar una asignación más allá de un límite que el producto ingenuo daba por despejado. Cualquiera de los dos rechazos destruye el destino y no devuelve nada. Ten clara la degradación que esto implica: una imagen fuera de presupuesto no se remuestrea a menor calidad, y no se parte en teselas. El original permanece en el documento, BudgetExceededCount y SkippedCount aumentan ambos, y una ejecución puede por tanto informar de éxito mientras un documento queda solo parcialmente optimizado. Es un comportamiento fail-safe deliberado, pero significa que el informe no es lectura opcional. Existe además un modo de fallo distinto: las imágenes cuyo bitmap PDFium no puede producir en absoluto, como CMYK, JPX, JBIG2 u orígenes enmascarados, aumentan FailedCount en su lugar y también quedan sin tocar
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // usa el voto de área binivel
Options.ContentClass := piccPhoto; // fuerza Lanczos para lotes fotográficos
Options.MaxWorkingBytes := 256 * 1024 * 1024; // margen para escaneos grandes
Pdf := TPdf.Create(nil);
try
for I := Low(Files) to High(Files) do
begin
Pdf.FileName := Files[I];
if not Pdf.OptimizeImages(Options, Report) then
begin
WriteLn('optimize failed: ', Report.ErrorMessage);
Continue;
end;
if Report.BudgetExceededCount > 0 then
WriteLn(Files[I], ': ', Report.BudgetExceededCount,
' image(s) over budget and kept at full size');
if Report.FailedCount > 0 then
WriteLn(Files[I], ': ', Report.FailedCount,
' image(s) could not be decoded to a bitmap');
if Report.OptimizedCount > 0 then
Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
end;
finally
Pdf.Free;
end;
end;
Leer el informe antes de enviar el archivo
TPdfImageOptimizeReport está construido para diagnosticarse, no solo para registrarse. Junto a OptimizedCount, SkippedCount y FailedCount expone un contador por kernel, así que BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount y BilevelFilterCount te dicen qué concluyó realmente la regla adaptativa sobre tu corpus. Un resultado todo-box significa que las reducciones fueron pronunciadas o que el contenido se clasificó como arte lineal; un resultado todo-Lanczos en un documento que creías arte lineal es señal de que ContentClass debería fijarse explícitamente. AverageDetailScore es el número que comparar con el umbral Lanczos de 0.08 al ajustar PreferredQuality, y PeakWorkingBytes muestra cuánto de MaxWorkingBytes necesitó realmente la ejecución. Las opciones inválidas fallan a voces en lugar de en silencio: un TargetDpi no positivo, un MinDpiRatio por debajo de 1, un PreferredQuality fuera de 0 a 1, o un MaxWorkingBytes no positivo lanzan EPdfError antes de tocar cualquier página. Y OptimizeImages edita solo el documento en memoria; cada página modificada se consolida con FPDFPage_GenerateContent, tras lo cual sigues llamando tú a SaveAs. Para echar un ojo a lo que cambió, renderiza los documentos de antes y después a bitmaps como se describe en convertir páginas PDF a imágenes JPEG y compáralos a zoom completo
El remuestreo adaptativo es una de esas funciones que son invisibles cuando funcionan y generan tickets de soporte cuando no, razón por la que la medición, el manejo del alfa y el presupuesto de memoria tuvieron que llegar juntos en lugar de como tres refinamientos separados. Si lo estás evaluando para un producto Delphi, C++Builder o Lazarus, la superficie completa de la API y los detalles de licencia están en la página de producto del PDFiumPas Delphi PDFium Component