Artículo técnico

CCITTFaxDecode en Delphi: sin compresión y filas dañadas

HotPDF, el componente PDF nativo para Delphi, decodifica imágenes CCITTFaxDecode con soporte completo de la extensión de modo sin compresión de T.4/T.6 y de la recuperación de filas dañadas gobernada por DamagedRowsBeforeError. Al modo sin compresión solo se entra por el código de extensión terminado en 111, y las filas dañadas solo se reparan en datos Group 3 con EndOfLine activado. Todo lo demás falla limpio en lugar de producir una página torcida

Las imágenes codificadas a fax son la cola larga de la decodificación de imágenes PDF. La mayoría de los streams CCITT que te encuentras salen de un driver de escáner o de un pipeline de captura documental que solo emite Group 4 plano, y un decodificador que maneja el caso común supera todos los tests que le eches. Entonces llega un archivo de un servidor de fax de quince años, una página se renderiza como ruido diagonal desde la fila 400 hacia abajo, y descubres que las dos partes de las recomendaciones ITU-T que nadie implementa son exactamente las dos que usa ese archivo. Este artículo recorre esas dos partes tal como las maneja HotPDF, y los puntos donde un decodificador puede equivocarse y aun así parecer correcto

¿Qué parámetros de CCITTFaxDecode cambian de verdad el flujo de bits?

Cuatro entradas del diccionario DecodeParms cambian cómo el decodificador enmarca el flujo de bits: K, EndOfLine, EncodedByteAlign y DamagedRowsBeforeError. ISO 32000-1 §7.4.6 (Tabla 11) las define, y equivocarte con cualquiera desplaza cada fila posterior al primer desajuste. BlackIs1, Columns, Rows y EndOfBlock también importan, pero afectan a la polaridad, la geometría y dónde se detiene la decodificación, no a cómo se encuentra un límite de fila

  • K elige el esquema de codificación: negativo significa Group 4 puramente bidimensional (T.6), cero significa Modified Huffman unidimensional (Group 3 1D), y positivo significa Group 3 2D mixto, donde un bit de etiqueta tras cada EOL dice si la siguiente fila es 1D o 2D
  • EndOfLine dice que cada fila Group 3 va precedida de un código EOL, once bits cero seguidos de un uno
  • EncodedByteAlign dice que cada fila codificada empieza en un límite de byte, así que el decodificador debe descartar los bits de relleno antes de leerla
  • DamagedRowsBeforeError es cuántas filas corruptas puede tolerar el decodificador antes de rendirse, con valor por defecto 0

HotPDF lee todo esto del diccionario de la imagen cuando llamas a THotPDF.ExtractLoadedImage o THotPDF.DecodeLoadedImageStream. Una desviación deliberada conviene conocerla: cuando falta Columns, HotPDF recurre al Width de la imagen en lugar del valor por defecto de la norma, 1728, y cuando falta Rows usa Height. Un productor que omite Columns en un escaneo A4 de 2480 píxeles está equivocado, pero es tan común que confiar en la geometría de la imagen da una página utilizable donde el valor por defecto literal daría basura

¿Por qué 000000 no puede tratarse como fin de bloque?

En filas bidimensionales, seis bits cero iniciales son ambiguos, y un decodificador que los lea como el comienzo de un EOFB malinterpretará una entrada válida del modo sin compresión como el fin de la imagen. Group 4 no tiene EOL entre filas; los únicos códigos cargados de ceros son el EOFB, que son dos EOL seguidos, y los códigos de extensión de ITU-T T.4 y T.6. Ambos empiezan por 000000, y el séptimo bit es donde se separan

El decodificador CCITT de HotPDF resuelve el prefijo bit a bit. Un séptimo bit en 1 significa que los tres bits siguientes son un código de extensión, siendo el patrón completo 0000001xxx. Solo xxx = 111 entra al modo sin compresión; cualquier otro valor de extensión se trata como malformado, ya que un filtro fax de PDF no tiene uso para los demás. Un séptimo bit en 0 solo puede continuar un EOL, así que el decodificador exige entonces cuatro bits cero más y un 1 de cierre, once ceros y un uno en total, antes de reportar EOFB. Cualquier cosa más corta o más larga es un error, no un fin de imagen indulgente

La versión indulgente tienta porque hace que toda una clase de archivos rotos parezcan sanos: la decodificación simplemente se detiene antes y obtienes una imagen corta sin excepción. También es exactamente el bug que trunca faxes legítimos en el primer tramo de datos sin comprimir, que en un formulario escaneado suele ser una firma densa o un sello. HotPDF elige la lectura estricta, así que una imagen corta significa que el codificador de verdad terminó ahí

¿Cómo codifica píxeles el modo sin compresión?

El modo sin compresión sustituye la codificación por longitudes con patrones literales de píxeles, cada palabra de código llevando como máximo cinco píxeles, hasta que un código de salida regresa al modo normal. Las filas unidimensionales entran con el código de 12 bits 000000001111; las bidimensionales usan la extensión de 10 bits 0000001111 descrita antes. Los codificadores cambian a él en regiones tipo semitono donde longitudes de uno o dos píxeles costarían más que los propios píxeles

Dentro del modo, las palabras de código se cuentan por sus ceros iniciales. De cero a cuatro ceros seguidos de un 1 significan esos píxeles blancos y luego un píxel negro. Cinco ceros seguidos de un 1 significan cinco píxeles blancos y nada más. De seis a diez ceros seguidos de un 1 son códigos de salida: los ceros más allá de seis emiten hasta cuatro píxeles blancos más, y el único bit tras el código de salida, el bit de etiqueta, fija el color de la siguiente ronda cuando se reanuda la codificación por runs

{ Palabras de código del modo sin compresión (ITU-T T.4 / T.6)
  W = píxel blanco, B = píxel negro, T = bit de etiqueta

  1            B
  01           W B
  001          W W B
  0001         W W W B
  00001        W W W W B
  000001       W W W W W
  0000001T     exit, then resume runs with color T
  00000001T    W, exit
  000000001T   W W, exit
  0000000001T  W W W, exit
  00000000001T W W W W, exit
  más de diez ceros: malformado }

El bit de etiqueta es la parte sutil. La codificación por runs tiene estado: una fila 1D alterna rondas blancas y negras, y una fila 2D sigue el color del elemento de cambio actual a0. Tras el código de salida, el decodificador tiene que reanudar con el color que nombre la etiqueta, que puede coincidir o no con el último píxel literal. El decodificador de HotPDF registra un cambio de color en la posición actual cuando la etiqueta difiere del color que venía siguiendo, y así la siguiente ronda arranca del color correcto. Ignora la etiqueta y el resto de la fila sale con los colores invertidos, y en modo 2D cada fila posterior hereda el daño a través de su línea de referencia

¿Qué recupera DamagedRowsBeforeError y qué no?

DamagedRowsBeforeError permite a HotPDF sustituir una fila corrupta y continuar, pero solo cuando K es 0 o positivo y EndOfLine está a true. Esas dos condiciones no son arbitrarias. La recuperación necesita una forma de encontrar dónde empieza la siguiente fila cuando los bits dejan de tener sentido, y el único hito fiable en un stream CCITT es el código EOL. Los datos Group 4 no tienen EOL, y los datos Group 3 sin EOL obligatorios no garantizan que exista uno, así que en ambos casos una fila dañada termina la decodificación sin importar el valor del parámetro

Cuando una fila falla, el decodificador de HotPDF escanea hacia el siguiente EOL, una ronda de al menos once bits cero seguidos de un uno, y sustituye una fila de reemplazo. La primera fila dañada tras una buena se sustituye por una copia de la fila anterior, que en una página de texto escaneado suele quedar lo bastante cerca como para no verse. Una fila dañada que sigue directamente a otra fila dañada se sustituye por una fila toda blanca, porque copiar una copia convertiría una línea de texto en una barra negra alta. El sustituto pasa entonces a ser la línea de referencia de la siguiente fila 2D, que es lo que mantiene un stream de modo mixto decodificable tras la reparación

Dos límites se pasan por alto fácilmente. El presupuesto es acumulativo sobre toda la imagen, no por ronda de filas malas, así que DamagedRowsBeforeError 3 tolera tres filas dañadas en total y la cuarta hace fallar la decodificación. Y una decodificación fallida en HotPDF es todo o nada: THotPDF.ExtractLoadedImage devuelve nil en lugar de un bitmap con el borde inferior raído. THPDFLoadedImageInfo.Decodable reporta que el filtro está soportado, no que este stream concreto esté intacto, así que comprueba siempre si el bitmap es nil

Por qué el escaneo debe pararse al inicio del EOL

El escaneo de resincronización deja el lector en el primer bit cero del EOL, no después. El decodificador tiene un único bucle de enmarcado que consume EOL, comprueba el doble EOL que termina el bloque y lee el bit de etiqueta del modo mixto. Si el escaneo de recuperación se traga el propio EOL, ese bucle ve una fila sin EOL, la trata como dañada también y quema una segunda unidad del presupuesto mientras tira una fila buena. Peor aún, un bit de etiqueta saltado significa que una fila 2D se decodifica como 1D o al revés. La misma regla aplica cuando un decodificador de rondas 1D se topa con un EOL inesperado a mitad de fila: rebobina a donde empezó el código de la ronda, así el marcador sigue ahí para el escaneo de recuperación

Inventariar imágenes CCITT en un PDF cargado

Antes de fiarte de un lote de faxes archivados, lista qué imágenes usan CCITTFaxDecode y cuáles fallan al decodificar. THotPDF.GetLoadedImageInfo te da geometría y nombre de filtro sin decodificar nada, y THotPDF.ExtractLoadedImage te dice si el stream sobrevivió. Para el panorama más amplio de cómo el despachador de imágenes enruta cada filtro, véase la guía de extracción de imágenes PDF cargadas y sus filtros de decodificación

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('fax-archive.pdf', '') <= 0 then
      Exit;
    for I := 0 to Pdf.GetLoadedImageCount - 1 do
    begin
      if not Pdf.GetLoadedImageInfo(I, Info) then
        Continue;
      if Pos('CCITT', UpperCase(String(Info.Filter))) = 0 then
        Continue;
      Bmp := Pdf.ExtractLoadedImage(I);   // nil cuando el stream está demasiado dañado
      try
        if Bmp = nil then
          Writeln(Format('obj %d: %dx%d CCITT failed to decode',
            [Info.ObjectNumber, Info.Width, Info.Height]))
        else
          Bmp.SaveToFile(Format('fax_%d.bmp', [I]));
      finally
        Bmp.Free;                         // el bitmap es propiedad del caller
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Para los fallos, guarda la evidencia. THotPDF.ExtractLoadedImageToStream con el modo por defecto lismEncoded escribe los bytes del stream almacenado intactos, que es lo que quieres entregar a un segundo decodificador, envolver en un contenedor TIFF para inspección o adjuntar a un bug report. Recodificar un fax dañado solo esconde dónde estaba el daño

procedure SaveRawFax(Pdf: THotPDF; ImageIndex: Integer; const FileName: string);
var
  Fs: TFileStream;
begin
  Fs := TFileStream.Create(FileName, fmCreate);
  try
    if not Pdf.ExtractLoadedImageToStream(ImageIndex, Fs, lismEncoded) then
      raise Exception.CreateFmt('Image %d has no stream data', [ImageIndex]);
  finally
    Fs.Free;
  end;
end;

Si recorres los streams de imagen tú mismo mediante THotPDF.CollectLoadedImageStreams, pasa cada THPDFStreamObject a THotPDF.DecodeLoadedImageStream y obtendrás la misma ruta CCITT, los mismos parámetros y las mismas reglas de recuperación. Libera la lista devuelta pero no los objetos de stream que contiene, que siguen siendo propiedad del documento

¿Deberías subir DamagedRowsBeforeError en archivos que no controlas?

No, ni editando el PDF, y normalmente tampoco de ninguna otra forma. El parámetro pertenece al productor, que es la única parte que sabe si su canal de transmisión suelta bits. Un archivo que declara DamagedRowsBeforeError 0 y luego contiene una fila dañada te está diciendo que la copia del archivo está corrupta, y el resultado honesto es un fallo de decodificación que puedes registrar. Parchear filas en silencio en un contrato o una historia clínica convierte un defecto detectable en una imagen de aspecto plausible con una línea perdida, que es un desenlace peor para quien luego se fíe de ella

El patrón más seguro para archivos no confiables es mantener la decodificación estricta, registrar qué números de objeto fallaron y derivarlos a una persona o a un decodificador aparte. Si el archivo es grande y viene de muchas fuentes, ejecutar la decodificación de imágenes fuera de proceso limita lo que un stream hostil puede hacerle a la aplicación anfitriona; el artículo sobre aislar codecs de imagen al procesar PDF no confiables cubre ese montaje, y la guía de compresión bilevel JBIG2 compara el otro filtro bilevel que encontrarás en los mismos archivos escaneados

FAQ

¿Soporta HotPDF el modo sin compresión CCITT en imágenes Group 4?

Sí. HotPDF reconoce la extensión de 10 bits 0000001111 en filas bidimensionales y el código de entrada de 12 bits 000000001111 en filas unidimensionales, decodifica las palabras de código literales y respeta el bit de etiqueta de salida cuando se reanuda la codificación por runs

¿Por qué DamagedRowsBeforeError no tiene efecto en mi fax Group 4?

La recuperación exige marcadores EOL para resincronizar. HotPDF aplica DamagedRowsBeforeError solo cuando K es 0 o positivo y EndOfLine está a true; los datos Group 4 (K negativo) no tienen EOL, así que cualquier fila dañada termina la decodificación

¿Qué devuelve ExtractLoadedImage para una imagen CCITT dañada?

THotPDF.ExtractLoadedImage devuelve nil cuando el daño excede lo que DamagedRowsBeforeError permite o no tiene recuperación posible. Nunca devuelve un bitmap parcial, así que comprueba nil y usa ExtractLoadedImageToStream para conservar los bytes en bruto

HotPDF maneja imágenes CCITT, JBIG2, JPEG, JPEG 2000 y Flate en PDF cargados desde código Delphi y C++Builder puro, sin DLL externa para la ruta de fax. Detalles y ediciones en la página del componente HotPDF Delphi PDF