Artículo técnico

Decodificación de Predictor y LZWDecode en Delphi con HotPDF

Un flujo marcado /Predictor 12 no significa que cada fila use el filtro PNG 2. HotPDF, el componente PDF VCL nativo para Delphi y C++Builder, trata los valores de predictor del 10 al 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 de casi todos los errores en este rincón de PDF, porque nada se dispara cuando lo haces mal. La cadena de filtros se ejecuta, el ráster tiene el tamaño que esperabas, y la imagen sale como estática diagonal o un degradado que se va desviando cada vez más con cada línea de escaneo. Los cinco números en /DecodeParms (ISO 32000-1 §7.4.4) sobre todo cambian el significado de los bytes en lugar de su longitud, así que uno equivocado produce basura verosímil en lugar de un error

Por qué /Predictor 12 no significa el 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 escaneo 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 diseño importa tanto como la semántica. Cada fila codificada mide 1 + RowBytes bytes, la entrada por lo tanto excede a la salida en exactamente la cantidad de filas, y un flujo cuya longitud no es 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 buffer de salida en lugar de materializar un arreglo bidimensional de filas. Los filtros 1 y 3 miran hacia atrás BytesPerPixel dentro de la fila actual, el filtro 2 lee directamente hacia arriba, el filtro 4 ejecuta la elección de Paeth sobre izquierda, arriba y arriba-izquierda, y los cuatro operan sobre salida ya reconstruida, que es por qué la fila de 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 en realidad una etapa de descompresión disfrazada, y un valor de /Columns hostil o simplemente roto convierte unos pocos kilobytes de entrada en una solicitud de asignación de varios gigabytes. HotPDF calcula primero los bits por fila, los bytes por fila y el tamaño total del ráster en Int64, rechaza geometrías que desbordan, y respeta el límite superior proporcionado por quien invoca. Pasa un límite real derivado del diccionario de la imagen y el modo de fallo se convierte en un mensaje registrado en lugar de un cuadro de 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 común suma el byte N-Colors al byte N, que resulta ser correcto a 8 bits por componente y está silenciosamente equivocado en cualquier otro caso. Un escaneo RGB de 8 bits se decodifica perfectamente, y luego el mismo código destruye una imagen indexada de 4 bits la primera vez que aparece una en producción

La aritmética correcta funciona 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 después de 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 envuelve en $FFFF a través del par en lugar de acarrear entre bytes de forma independiente; a 8 bits la recurrencia simple de byte es correcta, avanzando de a Colors para que el rojo se 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 ensancha su tamaño de código en un bit, y estar un código fuera de sincronía corrompe todo lo que sigue. HotPDF expresa la regla como un único invariante: después de agregar una entrada al diccionario, la siguiente lectura ensancha 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 archivos 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 de limpieza (clear code) reinicia juntos el tamaño de código, la máscara de bits, el próximo código libre y el almacenamiento de frases, 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 comienza en InitialCodeSize 9, limita el tamaño de código a 12 y el diccionario a 4096 entradas, y por defecto pone FillOrder en foTop porque PDF empaqueta los códigos con el bit de mayor orden primero; foBottom existe para flujos al 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 archivo se decodifica con la longitud correcta pero con los píxeles equivocados, PeakCodeSize y DictionaryAdds te dicen de inmediato si el lector alguna vez ensanchó donde el escritor lo hizo. Cambia EarlyChange, decodifica de nuevo, compara los dos: si los números se mueven, tienes tu respuesta en una sola ejecución en lugar de recorrer paso a paso un lector de bits

La rama KwKwK, y cuándo un flujo simplemente debería 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 sucede 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), agregarlo como la nueva entrada, y emitir la entrada que acaba de crear. HotPDF cuenta esos casos en KwKwKExpansions y verifica cruzadamente que el código que agregó sea el código que se le pidió. Todo lo que esté por encima de NextCode es corrupción, y ahí un decodificador debería 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 apagados por defecto, RequireInitialClear y RequireEndOfInformation, porque muchos PDF de producción omiten el código de limpieza inicial o se quedan sin datos sin un terminador. Actívalos al validar tu propia salida, déjalos apagados al consumir archivos del mundo real

Dónde se lee realmente /DecodeParms del lado del documento cargado

HotPDF resuelve /DecodeParms o su abreviatura /DP en el diccionario del flujo de imagen, acepta ya sea un diccionario o un arreglo y toma el último elemento cuando es un arreglo, y luego traslada Predictor, Colors, BitsPerComponent, Columns y EarlyChange a la ruta del ráster. El caso del arreglo es el que la gente olvida: un flujo filtrado por [/ASCII85Decode /FlateDecode] lleva un arreglo de parámetros paralelo, y la configuración del predictor pertenece 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;

Vale la pena nombrar un defecto histórico en esa ruta, porque esta clase de error se repite. La antigua rutina de Flate con parámetros creaba un flujo de descompresión y luego copiaba desde la entrada comprimida original, así que la etapa de predictor recibía bytes comprimidos y diligentemente les revertía la predicción: siempre equivocado, nunca se disparaba una excepción. El código actual lee únicamente desde el decodificador antes de entregar el resultado al predictor compartido, y rechaza un ráster más corto que el tamaño calculado en lugar de recurrir a los bytes que todavía están comprimidos, un mecanismo de respaldo que solía convertir un fallo de decodificación en un mapa de bits corrupto. Esa misma implementación de predictor ahora también sirve a los flujos de referencias cruzadas, lo cual es una consistencia útil si además trabajas con flujos de objetos y actualizaciones incrementales, y la maquinaria de extracción circundante se cubre en la nota complementaria sobre la 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

Reemplazar el diccionario de cadenas por entrada con un área de frases contigua midió alrededor de 1.61 veces más rápido en una entrada patológica: 1558 MiB/s contra 969 MiB/s en un benchmark cuya frase individual más larga alcanza 7,370,880 bytes. La forma de esa entrada explica la brecha, porque las implementaciones clásicas eligen uno de dos malos compromisos. Un diccionario de valores AnsiString asigna y copia una cadena nueva para cada una de hasta 4096 entradas, y cada entrada nueva copia entera a su padre; una pila de prefijo/sufijo evita por completo esa memoria pero reconstruye cada frase recorriendo la cadena hacia atrás un byte a la vez y luego invirtiéndola, lo cual está bien para texto ordinario y es doloroso cuando una frase llega a megabytes. HotPDF anexa cada frase de forma contigua a un área que crece geométricamente, indexa las entradas por desplazamiento y longitud, y emite una frase con un único Move hacia el buffer de salida. El costo honesto es la memoria: un área que contiene cada frase por completo está acotada por la suma de todas las longitudes de frase en lugar de por la cantidad de entradas, que es precisamente por qué existe MaxOutputBytes tanto en el descompresor como en el predictor. Deriva ese límite de lo que el diccionario de la imagen afirma que debería ser el ráster, y un flujo que miente 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 de producto