Un stream marcado como /Predictor 12 no significa que cada fila use el filtro PNG 2. HotPDF, el componente VCL nativo de PDF para Delphi y C++Builder, trata los valores de predictor 10 a 15 como una sola familia: la etiqueta de filtro real, de 0 a 4, es el primer byte de cada fila codificada, y HPDFDecodePredictor lee y valida esa etiqueta fila por fila. Esa distinción es la forma que adopta casi todos los bugs en este rincón del PDF, porque nada lanza una excepción cuando te equivocas. La cadena de filtros se ejecuta, el raster tiene el tamaño que esperabas, y la imagen sale como estática diagonal o un degradado que se desvía cada vez más con cada línea de barrido. Los cinco números de /DecodeParms (ISO 32000-1 §7.4.4) en su mayoría cambian el significado de los bytes en lugar de su longitud, así que uno equivocado produce basura plausible en lugar de un error
¿Por qué /Predictor 12 no significa filtro PNG 2 en cada fila?
Porque el número de predictor solo dice "se está usando predicción PNG", no cuál filtro. Los codificadores PNG eligen un filtro por línea de barrido y el filtro de PDF hereda eso, así que los valores de predictor 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) y 15 (Optimum) se decodifican todos de forma idéntica: el byte de etiqueta inicial de cada fila es lo que el decodificador debe obedecer. La consecuencia en el layout importa tanto como la semántica. Cada fila codificada mide 1 + RowBytes bytes, la entrada por tanto supera a la salida exactamente en el número de filas, y un stream cuya longitud no sea un múltiplo entero de RowBytes + 1 está truncado por definición. HotPDF comprueba ese límite antes de tocar un solo byte, rechaza cualquier etiqueta superior a 4 con Invalid PNG predictor row tag, y lee la fila anterior directamente del único búfer de salida en lugar de materializar un array de filas bidimensional. Los filtros 1 y 3 retroceden BytesPerPixel dentro de la fila actual, el filtro 2 lee directamente hacia arriba, el filtro 4 ejecuta la elección Paeth entre izquierda, arriba y arriba-izquierda —y los cuatro operan sobre salida ya reconstruida, razón por la cual la fila-arriba tiene que ser la fila decodificada y nunca la entrada filtrada
uses
HPDFPredictor;
var
Filtered, Raster: AnsiString;
ErrorText: string;
begin
// /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
Int64(1024) * 3 * 8192, Raster, ErrorText) then
ConsumeRaster(Raster)
else
LogStreamDefect('predictor', ErrorText); // no exception, no partial raster
end;
El argumento MaxOutputBytes no es decoración. Una etapa de predictor es una etapa de descompresión disfrazada, y un valor de /Columns hostil o simplemente roto convierte unos pocos kilobytes de entrada en una petición de asignación de varios gigabytes. HotPDF calcula bits por fila, bytes por fila y tamaño total del raster primero en Int64, rechaza geometrías que desbordan, y respeta el techo proporcionado por el llamante. Pasa un límite real derivado del diccionario de imagen y el modo de fallo se convierte en un mensaje registrado en lugar de un diálogo de falta de memoria en la máquina de un cliente
¿Por qué TIFF Predictor 2 corrompe imágenes de 4 bits?
Porque el Predictor 2 es diferenciación horizontal por muestra, no por byte, y a 1, 2 o 4 bits por componente varias muestras comparten un byte. La implementación habitual suma el byte N-Colors al byte N, lo cual resulta ser correcto a 8 bits por componente y silenciosamente incorrecto en cualquier otro caso. Un escaneo RGB de 8 bits se decodifica perfectamente, y luego el mismo código destroza una imagen indexada de 4 bits la primera vez que aparece una en producción
La aritmética correcta trabaja dentro del campo de bits. HotPDF recorre las muestras desde el índice Colors hasta Colors * Columns - 1, extrae la muestra y su vecina izquierda del mismo componente con una máscara de (1 shl BitsPerComponent) - 1 en el desplazamiento apropiado, las suma módulo esa máscara, y escribe el resultado de vuelta sin alterar las demás muestras empaquetadas en el mismo byte. La cola también importa: una fila se rellena hasta un límite de byte, así que los bits de relleno tras la última muestra deben sobrevivir intactos en lugar de incorporarse a la aritmética. A 16 bits por componente cada muestra es un par de bytes big-endian y la suma se desborda en $FFFF a través del par en lugar de acarrear entre bytes de forma independiente; a 8 bits la simple recurrencia de bytes es correcta, avanzando de Colors en Colors para que el rojo acumule contra el rojo y el alfa contra el alfa. En cada variante el primer píxel de una fila es un literal, nunca una diferencia, y la recurrencia se reinicia en cada límite de fila —la predicción TIFF nunca lee la fila de arriba, que es toda la diferencia entre ella y la familia PNG
¿Qué controla realmente EarlyChange en LZWDecode?
Controla cuándo el lector amplía su tamaño de código en un bit, y estar un código desincronizado corrompe todo lo que sigue. HotPDF expresa la regla como un único invariante: después de añadir una entrada al diccionario, la siguiente lectura amplía cuando NextCode alcanza (1 shl CodeSize) - Ord(EarlyChange). Con /EarlyChange 1, el valor por defecto de ISO 32000-1 §7.4.4, el cambio ocurre un código antes; con /EarlyChange 0 ocurre exactamente en el límite. Ambos aparecen en ficheros reales y nada en el flujo de bits te dice cuál usó el codificador. El resto de la máquina de estados tiene que moverse en sincronía: un código clear reinicia el tamaño de código, la máscara de bits, el siguiente código libre y el almacenamiento de frases todos a la vez, y el código de fin de información se lee con el ancho que esté vigente en ese momento, no con los 9 bits iniciales. HotPDF arranca en InitialCodeSize 9, limita el tamaño de código a 12 y el diccionario a 4096 entradas, y por defecto pone FillOrder a foTop porque PDF empaqueta los códigos con el bit de mayor orden primero —foBottom existe para streams de estilo TIFF que no lo hacen
uses
HPDFLZW;
var
Decoder: TPDFLZWDecompressor;
Parms: TPDFLZWParms;
Plain: AnsiString;
begin
Decoder := TPDFLZWDecompressor.Create;
try
Decoder.EarlyChange := True; // /EarlyChange 1 is the PDF default
Decoder.FillOrder := foTop; // high-order bit first
Decoder.MaxOutputBytes := 256 * 1024 * 1024;
Decoder.RequireInitialClear := False;
Decoder.RequireEndOfInformation := False;
Parms.Predictor := 12;
Parms.Colors := 3;
Parms.BitsPerComponent := 8;
Parms.Columns := 1024;
Parms.ExpandedTo8Bit := False;
Parms.ColorSpace := 'DeviceRGB';
if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
Decoder.KwKwKExpansions, Decoder.OutputBytes)
else
LogStreamDefect('lzw', Decoder.LastError);
finally
Decoder.Free;
end;
end;
Las estadísticas existen para el triaje, no por vanidad. Cuando un fichero se decodifica con la longitud correcta pero los píxeles equivocados, PeakCodeSize y DictionaryAdds te dicen de inmediato si el lector llegó a ampliar en el mismo punto que el escritor. Invierte EarlyChange, decodifica de nuevo, compara ambos: si los números se mueven, tienes tu respuesta en una sola ejecución en lugar de recorrer un lector de bits paso a paso
La rama KwKwK, y cuándo un stream simplemente debe fallar
El único caso legal que parece ilegal es Code = NextCode, y HotPDF lo maneja construyendo la entrada antes de emitirla. Un codificador puede emitir el código de una frase que está definiendo en el mismo paso, lo cual ocurre siempre que la entrada contiene un patrón de la forma K w K w K; el decodificador no puede buscar ese código porque todavía no existe, así que debe construir Previous + First(Previous), añadirlo como la nueva entrada, y emitir la entrada que acaba de crear. HotPDF cuenta esos casos en KwKwKExpansions y verifica de forma cruzada que el código que añadió es el código que se le pidió. Todo lo que esté por encima de NextCode es corrupción, y ahí un decodificador debe detenerse en lugar de improvisar: HotPDF lanza una excepción ante un código futuro, ante un prefijo de diccionario que apunta fuera del área de frases, ante un diccionario lleno, y ante un primer código que no es un literal. Dos interruptores de severidad están deliberadamente desactivados por defecto, RequireInitialClear y RequireEndOfInformation, porque bastantes PDF de producción omiten el código clear inicial o se quedan sin datos sin un terminador. Actívalos al validar tu propia salida, desactívalos al consumir ficheros salvajes
Dónde se lee realmente /DecodeParms en el lado del documento cargado
HotPDF resuelve /DecodeParms o su abreviatura /DP en el diccionario de stream de imagen, acepta tanto un diccionario como un array y toma el último elemento cuando es un array, y luego lleva Predictor, Colors, BitsPerComponent, Columns y EarlyChange a la ruta de raster. El caso del array es el que la gente olvida: un stream filtrado por [/ASCII85Decode /FlateDecode] lleva un array de parámetros paralelo, y los ajustes del predictor pertenecen al último filtro, no al primero
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
for I := 0 to Pdf.GetLoadedImageCount - 1 do
if Pdf.GetLoadedImageInfo(I, Info) then
begin
Bmp := Pdf.ExtractLoadedImage(I); // nil when the raster is unusable
if Bmp <> nil then
try
Bmp.SaveToFile(Format('image-%d.bmp', [I]));
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Merece la pena nombrar un defecto histórico en esa ruta, porque esa clase de bug se repite. La antigua rutina de Flate con parámetros creaba un stream de descompresión y luego copiaba de la entrada comprimida original, así que la etapa de predictor recibía bytes comprimidos y diligentemente los des-predecía: siempre incorrecto, nunca lanzaba una excepción. El código actual solo lee del decodificador antes de entregar el resultado al predictor compartido, y rechaza un raster más corto que el tamaño calculado en lugar de caer de vuelta a los bytes todavía comprimidos —una recuperación que solía convertir un fallo de decodificación en un bitmap corrupto. Esa misma implementación de predictor ahora sirve también a los cross-reference streams, lo cual es una consistencia útil si además trabajas con object streams y actualizaciones incrementales, y la maquinaria de extracción circundante se cubre en la pieza complementaria sobre extracción de imágenes cargadas y sus filtros de decodificación. Las imágenes que llegan como DCTDecode o JPXDecode nunca llegan al predictor en absoluto; llevan su propio modelo de píxeles comprimido
Rendimiento: un área de frases contigua frente a cadenas por entrada
Sustituir el diccionario de cadenas por entrada por un área de frases contigua midió aproximadamente 1,61 veces más rápido en una entrada patológica: 1558 MiB/s frente a 969 MiB/s en un benchmark cuya frase individual más larga alcanza 7.370.880 bytes. La forma de esa entrada explica la diferencia, porque las implementaciones clásicas eligen una de dos malas alternativas. Un diccionario de valores AnsiString asigna y copia una cadena nueva por cada una de hasta 4096 entradas, copiando cada nueva entrada a su padre entero; una pila de prefijo/sufijo evita esa memoria por completo pero reconstruye cada frase recorriendo la cadena hacia atrás byte a byte e invirtiéndola, lo cual está bien para texto ordinario y resulta doloroso cuando una frase alcanza los megabytes. HotPDF añade cada frase de forma contigua a un área que crece geométricamente, indexa las entradas por offset y longitud, y emite una frase con un único Move al búfer de salida. El coste honesto es la memoria: un área que contiene cada frase entera está acotada por la suma de todas las longitudes de frase en lugar de por el número de entradas, que es precisamente por lo que existe MaxOutputBytes tanto en el descompresor como en el predictor. Deriva ese límite de lo que el diccionario de imagen afirma que debería ser el raster, y un stream mentiroso falla rápido
El descompresor LZW, el predictor compartido y la ruta de extracción de imágenes cargadas que se muestran aquí se incluyen como parte del HotPDF Component estándar para Delphi y C++Builder, con la referencia completa de filtros y DecodeParms en la página del producto