Un flujo de objeto PDF que se infla sin error pero que sigue leyéndose como ruido normalmente le falta un paso: revertir el Predictor de ISO 32000-1. Cuando el diccionario /DecodeParms de un flujo lleva /Predictor 2 o superior, los bytes que devuelve FlateDecode no son los datos originales: son valores diferenciados por fila al estilo PNG o diferenciados horizontalmente al estilo TIFF que necesitan una segunda pasada de reconstrucción antes de que cualquier búsqueda de diccionario tenga sentido. PDFiumPas, la biblioteca de componente PDF VCL nativa para Delphi y C++Builder, agregó esa pasada de reconstrucción en v2.16.0, específicamente porque los flujos de objeto de PDF 1.5+ se estaban expandiendo a bytes diferenciados que ningún analizador de diccionario podía leer
Por qué FlateDecode por sí solo no es suficiente
FlateDecode en sí mismo es solo descompresión DEFLATE (ISO 32000-1 §7.4.4.1): reproduce cualesquiera bytes que el codificador le entregó al compresor, nada más. El Predictor vive una capa más arriba, en el diccionario /DecodeParms del flujo, y describe una transformación que el codificador aplicó antes de la compresión —la diferenciación convierte largas secuencias de valores estructurados similares, como los enteros densamente empaquetados dentro de un flujo de referencia cruzada o un flujo de objeto, en largas secuencias de números pequeños que DEFLATE comprime mucho mejor. ISO 32000-1 §7.4.4.3 (Tabla 8) es explícita en que deshacer esta transformación es parte de decodificar un flujo filtrado, no una pasada de limpieza opcional, pero es fácil escribir un helper de FlateDecode que solo llama a inflate y se detiene ahí
El síntoma es distintivo una vez que se sabe qué buscar. Los bytes diferenciados por Predictor no son ruido aleatorio: todavía llevan la forma de un flujo comprimido, así que un analizador ingenuo a menudo pasa por unos pocos tokens que se ven válidos antes de toparse con una secuencia de bytes que de ninguna manera puede ser un nombre, número, o delimitador de PDF, y filas distintas fallan en desplazamientos distintos según cuánto resultaron diferir los valores subyacentes de sus vecinos. Esa inconsistencia es lo que hace difícil precisar el bug a partir de un solo archivo fallido: dos PDF del mismo productor pueden diferir solo en qué valores resultan repetirse, así que uno se analiza casi por accidente mientras el otro falla directamente
¿Qué hace realmente el parámetro Predictor de PDF?
La entrada /Predictor en /DecodeParms le dice a un lector conforme cuál reversión ejecutar, y la Tabla 8 de ISO 32000-1 define los valores que importan en la práctica: 1 significa que no se aplicó predicción, 2 selecciona TIFF Predictor 2 (diferenciación horizontal), y cualquier valor de 10 a 15 selecciona predicción al estilo PNG. Tres claves más viajan junto a ella —/Colors, /BitsPerComponent, y /Columns— y juntas describen la geometría de fila contra la que se calculó la diferenciación, incluso cuando el flujo no contiene ningún dato de imagen en absoluto: un flujo de objeto no es una imagen, pero los escritores de PDF reutilizan la misma maquinaria de predictor basada en filas para él porque delta-y-luego-deflate comprime enteros y desplazamientos de objeto densamente empaquetados mejor que desinflarlos crudos
TIFF Predictor 2 es el más simple de los dos esquemas: cada componente se almacena como la diferencia respecto al mismo componente en el píxel anterior de la misma fila, y cada fila se reinicia en su borde izquierdo en lugar de llevar una diferencia desde la fila de arriba. La predicción PNG es más particular, porque el filtro real puede cambiar de fila a fila: cada fila empieza con un solo byte de etiqueta —0 para None, 1 para Sub, 2 para Up, 3 para Average, 4 para Paeth— y esa etiqueta, no el valor declarado de /Predictor, decide cómo se reconstruye esa fila específica. Un /Predictor de 12 en realidad es solo la pista del codificador de que favoreció el filtro Up, donde cada byte se restaura sumando el byte directamente arriba de él en la fila anterior, pero un decodificador correcto todavía tiene que leer la etiqueta en cada fila en lugar de asumir Up en todas partes
¿Por qué los flujos de objeto vuelven invisible un Predictor omitido?
Los flujos de objeto agravan el problema en lugar de simplemente repetirlo. ISO 32000-1 §7.5.7 permite que un escritor de PDF 1.5+ empaquete múltiples objetos indirectos en un solo contenedor comprimido, un /ObjStm, y es común que exactamente los objetos que más necesita un validador —el catálogo, /OutputIntents, o un flujo XMP /Metadata— viajen a través de ese contenedor con /Predictor 12 adjunto, porque esos objetos son lo bastante cortos y repetitivos como para beneficiarse de la diferenciación por fila. Cuando falta el paso del predictor, expandir el flujo de objeto no lanza un error: produce una secuencia de bytes que se ve superficialmente plausible pero que no se tokeniza en los objetos esperados, así que lo que fuera que estuviera empaquetado dentro simplemente no aparece. El renderizado rara vez lo nota, porque un motor de renderizado conforme ya reconstruye los datos diferenciados por predictor antes de que lleguen a la maquetación; el código que lo nota es exactamente del tipo en el que este bug se escondía: un validador, firmador, o comprobador de versión que recorre él mismo los bytes crudos del PDF para responder una pregunta estructural, sin ningún respaldo una vez que su propia vista del flujo de objeto vuelve equivocada
PDFiumPas se topó con exactamente este fallo antes de v2.16.0. Los flujos de objeto construidos con /Predictor 12, el caso común para los escritores de PDF 1.5+, se expandían a través de PdfExpandObjectStreams a bytes diferenciados que el escáner estructural no podía analizar, así que el catálogo, /OutputIntents, y los objetos /Metadata empaquetados dentro eran efectivamente invisibles para los escaneos de conformidad —sin excepción, sin advertencia, solo un escaneo que silenciosamente se comportaba como si esos objetos estuvieran ausentes. La mecánica más profunda de cómo PDFiumPas resuelve un flujo de objeto contra la tabla de referencia cruzada activa, incluidos los casos híbridos y de flujo xref puro, se cubre por separado en el artículo sobre validación de flujos de objetos y xref con PDFiumPas; el paso de predictor descrito aquí se ejecuta después de esa resolución, sobre los bytes que realmente contiene cada objeto comprimido
Revertir filas de Predictor PNG y TIFF en Pascal
PDFiumPas revierte la diferenciación en una única rutina, PdfApplyPredictor, y su matemática de geometría vale la pena conocerla ya sea que se la llame o se reimplemente la idea en su propio código Delphi. El ancho de fila en bytes es ceil(Columns × Colors × BitsPerComponent ÷ 8) y el ancho de byte por píxel que usan ambos algoritmos es ceil(Colors × BitsPerComponent ÷ 8) —equivoque cualquiera de los dos redondeos y la reconstrucción lee a través de un límite de fila en lugar de dentro de uno. Un /Predictor por debajo de 2 se deja intacto, ya que 1 significa que el codificador no aplicó ninguna transformación en absoluto; 2 selecciona la rama TIFF mostrada abajo, y cualquier cosa desde 10 hacia arriba cae en la reconstrucción de filtro de fila PNG, donde el byte de etiqueta al inicio de cada fila —no el valor declarado de /Predictor— decide cómo se deshace esa fila específica
function PdfApplyPredictor(const Src: TBytes;
Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
RowLen, Bpp, R, I: Integer;
begin
Result:= Src;
if Predictor< 2 then
Exit; // 1 = no prediction, nothing to undo
if Colors<= 0 then Colors:= 1;
if Bpc<= 0 then Bpc:= 8;
if Columns<= 0 then Columns:= 1;
if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
Exit; // reject hostile row geometries
RowLen:= (Columns* Colors* Bpc+ 7) div 8; // ceil(), per ISO 32000-1 Table 8
Bpp:= (Colors* Bpc+ 7) div 8;
if Predictor= 2 then
begin
if Bpc<> 8 then
Exit; // only the 8-bit layout is reconstructed
Result:= Copy(Src, 0, Length(Src));
R:= 0;
while R+ RowLen<= Length(Result) do
begin
for I:= R+ Bpp to R+ RowLen- 1 do
Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
Inc(R, RowLen);
end;
Exit;
end;
// Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
SrcOfs:= R* (RowLen+ 1);
DstOfs:= R* RowLen;
Tag:= Src[SrcOfs];
Inc(SrcOfs);
for I:= 0 to RowLen- 1 do
begin
if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0; // byte to the left
if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0; // byte above
case Tag of
1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A); // Sub
2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B); // Up
3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
// Paeth (tag 4) adds whichever of A, B or the byte above-left sits
// closest to the linear predictor A+ B- C; tag 0 (None) copies the
// filtered byte through unchanged
else Result[DstOfs+ I]:= Src[SrcOfs+ I];
end;
end;
end;
Qué cambió PDFiumPas en v2.16.0
La corrección que se incorporó en PDFiumPas v2.16.0 se ubica dentro de PdfReadAndDecodeStream, la rutina que lee los bytes crudos de un flujo y los decodifica para cada llamador que necesita inspeccionar la estructura de PDF a nivel de byte, incluida la expansión de flujo de objeto; solo intenta la reconstrucción después de confirmar que /Filter es un FlateDecode puro, nunca una cascada, porque un filtro encadenado no se puede corregir con seguridad por predictor en esta capa. Leer /Predictor, /Colors, /BitsPerComponent, y /Columns de vuelta del diccionario del flujo tampoco necesita un analizador de diccionario general: PdfDictRefNum encuentra cada clave mediante búsqueda directa de token de nombre dentro del rango de bytes de ese único diccionario, lo cual es seguro aquí precisamente porque esas cuatro claves no pueden repetirse ni anidarse dentro de un solo diccionario de flujo. Esa misma búsqueda de token de nombre es mucho más riesgosa en cuanto se apunta a una región más grande o menos acotada de un archivo PDF, que es el tema de el artículo complementario sobre analizar diccionarios PDF con seguridad
// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
Inflated:= PdfInflate(Raw);
Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
if Predictor>= 2 then
begin
PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
end
else
Result:= Inflated;
end;
Antes de v2.16.0, un flujo de objeto construido con /Predictor 12 se expandía a bytes diferenciados sin lanzar ningún error, así que cualquier objeto de catálogo, /OutputIntents, o /Metadata empaquetado dentro desaparecía de los escaneos estructurales de PDFiumPas sin ninguna advertencia. Después de la corrección, el mismo flujo de objeto se infla y luego se reconstruye correctamente, y los objetos empaquetados dentro vuelven a ser visibles para esos escaneos. Límites defensivos viajaron junto con la corrección: PdfApplyPredictor ahora rechaza directamente /Colors por encima de 64, /BitsPerComponent por encima de 32, y /Columns por encima de 2^24, porque esas combinaciones describen geometrías de fila que ningún productor de PDF real necesita y existen principalmente para hacer que un decodificador asigne mucha más memoria de la que justifican los bytes de entrada
var
Pdf: TPdf;
Report: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'incoming.pdf';
Pdf.Active := True;
Report := Pdf.ValidatePdfA;
if not Report.IsCompliant then
LogNonCompliance(Report); // caller-supplied handler
finally
Pdf.Free;
end;
end;
Límites que vale la pena conocer
La reconstrucción de predictor de PDFiumPas tiene dos bordes que vale la pena conocer antes de confiar en ella. La reconstrucción de TIFF Predictor 2 solo cubre el caso de 8 bits por componente; PDF permite empaquetados más estrechos, pero los datos diferenciados por TIFF de sub-byte pasan sin reconstruirse en lugar de intentar adivinarse, así que un flujo que declara /Predictor 2 con /BitsPerComponent 1, 2, o 4 hoy no se decodificará correctamente a través de esta ruta. La predicción PNG no tiene tal restricción —cada fila provee su propia etiqueta de filtro, y los cinco tipos definidos se reconstruyen sin importar cuál resulte ser el valor declarado de /Predictor entre 10 y 15, lo que coincide con cómo realmente funciona el filtrado al estilo PNG: el valor declarado se acerca más a una pista sobre lo que usó principalmente el codificador que a una promesa sobre cada fila
El motor de renderizado nativo de PDFium ya reconstruye correctamente los datos de imagen y flujo de contenido diferenciados por predictor, que es exactamente por qué un archivo puede renderizarse perfectamente en cualquier visor ordinario mientras que un validador, firmador, o comprobador de versión a nivel de byte construido encima de él lee mal los mismos bytes. La decodificación consciente de predictor descrita aquí respalda las funciones de validación PDF/A, escaneo estructural, y firma de PDFiumPas, el componente PDFium VCL nativo para Delphi y C++Builder