Dos quejas llegan la semana posterior al lanzamiento de una función de compresión: el contrato escaneado ahora tiene glifos dentados y peludos, 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, luego 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 binaria con un paso fijo de vecino más cercano, que es exactamente el algoritmo que produce ambas quejas: toma una muestra puntual de un píxel fuente por píxel de salida, y trata el RGB que está debajo de un píxel totalmente transparente como si algún lector fuera a verlo. La reescritura de v3.100.0 reemplaza esa única ruta con cinco kernels, una regla de selección medida y un presupuesto explícito de memoria de trabajo
¿Por qué el remuestreo hace que el texto escaneado se vea dentado?
Porque el muestreo puntual responde la pregunta equivocada. Cuando un escaneo de 300 DPI se re-targetiza a 150 DPI, cada píxel de destino representa un bloque de dos por dos de píxeles fuente, 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 suavemente antialiased en la fuente se convierte en una moneda al aire por píxel. El resultado es la clásica escalera aliased en los bordes de los glifos, más moiré en las regiones de semitonos donde las muestras descartadas casualmente cargaban el patrón. Esto importa más en un PDF que en pantalla porque el daño es permanente. Un image XObject lleva sus datos de muestra junto con /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 cuadro que pueden redibujar, y PDFiumPas tiene maquinaria aparte para eso en la caché de renderizado y el rendimiento de zoom. Un remuestreo malo es un documento nuevo que entregan al cliente
Cómo mide PDFiumPas el detalle y elige un kernel
PDFiumPas decide por imagen, no por documento. Antes de elegir un kernel calcula un puntaje de detalle de luminancia normalizado desde una cuadrícula de muestreo acotada: los pasos horizontal y vertical son (Width + 63) div 64 y (Height + 63) div 64, así que un escaneo de 12000 píxeles y una miniatura de 300 píxeles cuestan aproximadamente el mismo barrido 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 largo de hasta tres canales, y divide por el conteo de muestras por 255. El puntaje cae entre 0 y 1, donde los gráficos de negocios planos se quedan cerca de cero y la textura fotográfica densa sube
La escalera de selección corre entonces en un orden fijo. Si ResampleFilter es cualquier cosa distinta de pirfAdaptive, ese filtro se usa tal cual. De lo contrario: el contenido de 1 bit toma pirfBilevel; un ContentClass de piccLineArt toma pirfBox; un factor de escala de 4 o más también toma 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, un puntaje de detalle de 0.08 o más, o un PreferredQuality de 0.9 o más toma pirfLanczos con su kernel de tres lóbulos; una escala de 2 o más o una calidad de 0.7 o más toma pirfBicubic a radio 2; todo lo restante toma pirfBilinear. Como TPdfImageOptimizeOptions.Default fija PreferredQuality en 0.85, una corrida por defecto nunca retrocede a 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;
// Defaults: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, calidad 0.85, presupuesto 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;
Una imagen solo se toca cuando el mayor de sus DPI de colocación horizontal y vertical, dividido por TargetDpi, alcanza MinDpiRatio. Esa guarda existe para que una foto de 160 DPI dirigida a un objetivo de 150 DPI no se re-codifique por una ganancia de seis por ciento que cuesta una generación de calidad. Las imágenes por debajo de MinDimension en cualquiera de los ejes, 8 por defecto, se saltan como iconos o reglas
¿Por qué los logos transparentes agarran un fleco blanco?
Porque el color debajo de un píxel totalmente transparente es arbitrario, y un promedio ponderado simple lo deja votar. Exporten un logo desde una herramienta de diseño y el margen invisible suele ser blanco, o negro, o lo que sea que era el lienzo; el canal alfa lo esconde, y una suma directa sobre la huella del kernel lo mezcla de vuelta en el borde visible enseguida. PDFiumPas lo evita acumulando muestras BGRA en forma premultiplicada y deshaciendo la premultiplicación solo en el píxel de destino
Concretamente, cada muestra contribuidora agrega 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 realmente coincidieron. 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 una constante 255, y cada canal se recorta entre 0 y 255 antes de almacenarse. Ese alfa normalmente proviene de una entrada de soft mask en el diccionario de la imagen (ISO 32000-1 §11.4), que PDFium ya compuso en el buffer BGRA que el remuestreador recibe
// Forma del bucle interno de acumulación, por muestra fuente contribuidora
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 line art de 1 bit fuera de la zona gris
Cualquier kernel continuo aplicado a un escaneo binario produce gris, y el gris es precisamente lo que una imagen tipo fax no puede contener. PDFiumPas por eso deja las imágenes de 1 bit tranquilas por defecto: PreserveBilevel es True en TPdfImageOptimizeOptions.Default, y tales imágenes aterrizan en SkippedCount intactas. Pónganlo en False y la ruta pirfBilevel toma el relevo en lugar de un kernel de suavizado. Recorre el rectángulo fuente 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 plano 0 o 255. Nada intermedio puede escribirse, así que los bordes permanecen nítidos y no se forma halo gris alrededor de trazos finos; el canal alfa de una fuente BGRA se promedia normalmente, y un destino BGRx recibe la constante 255. Si necesitan los píxeles subyacentes y no un documento más pequeño, extraer imágenes de documentos PDF es la ruta aparte
¿Qué pasa cuando una imagen excede el presupuesto de memoria de trabajo?
Queda exactamente como estaba, y se cuenta. MaxWorkingBytes por defecto es 64 MiB y se aplica dos veces. Antes de crear el bitmap de destino, PDFiumPas rechaza la imagen si ancho por altura por bytes por píxel excede el presupuesto. Después de que FPDFBitmap_CreateEx tiene éxito verifica de nuevo usando el stride real por altura, porque el relleno de fila puede empujar una asignación más allá de un límite que el producto ingenuo dejaba pasar. Cualquiera de los dos rechazos destruye el destino y no devuelve nada. Sean claros sobre la degradación que esto implica: una imagen fuera de presupuesto no se remuestrea a menor calidad, y no se divide en tiles. La original permanece en el documento, BudgetExceededCount y SkippedCount suben ambos, y una corrida puede por tanto reportar éxito mientras el documento queda solo parcialmente optimizado. Ese es un comportamiento fail-safe deliberado, pero significa que el reporte no es lectura opcional. También existe un modo de fallo distinto: imágenes cuyo bitmap PDFium no puede producir en absoluto, como CMYK, JPX, JBIG2 o fuentes con máscara, incrementan FailedCount en su lugar y de igual forma quedan intactas
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 bilevel
Options.ContentClass := piccPhoto; // fuerza Lanczos para sets de fotos
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 reporte antes de entregar el archivo
TPdfImageOptimizeReport está construido para diagnosticarse, no solo para loguearse. Junto con OptimizedCount, SkippedCount y FailedCount expone un contador por kernel, así que BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount y BilevelFilterCount les dicen qué concluyó realmente la regla adaptativa sobre su corpus. Un resultado todo-box significa que las reducciones fueron pronunciadas o que el contenido se clasificó como line art; un resultado todo-Lanczos en un documento que ustedes creían line art es señal de que ContentClass debería fijarse explícitamente. AverageDetailScore es el número para comparar contra el umbral Lanczos de 0.08 al afinar PreferredQuality, y PeakWorkingBytes muestra cuánto de MaxWorkingBytes necesitó realmente la corrida. Las opciones inválidas fallan ruidosamente en lugar de en silencio: un TargetDpi no positivo, un MinDpiRatio menor que 1, un PreferredQuality fuera de 0 a 1, o un MaxWorkingBytes no positivo lanza EPdfError antes de tocar cualquier página. Y OptimizeImages edita solo el documento en memoria; cada página modificada se confirma con FPDFPage_GenerateContent, después de lo cual siguen llamando SaveAs ustedes mismos. Para echar un ojo a qué cambió, rendericen los documentos antes y después a bitmaps como se describe en convertir páginas PDF a imágenes JPEG y compárenlos 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 aterrizar juntos y no como tres refinamientos separados. Si lo están 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 del componente PDFiumPas Delphi PDFium