Vierta byte por byte un segmento Tables de JBIG2 (tipo 53) y la primera sorpresa es lo que deja afuera: los propios códigos Huffman. Usted recibe un byte de flags, dos cotas y una seguidilla de números pequeños, y esos números son longitudes de código. Encoder y decoder reconstruyen ambos los patrones de bits reales desde las longitudes con el procedimiento del Anexo B.3 de ITU-T T.88. Reconstrúyalo en un orden levemente distinto y obtiene igualmente un código de prefijo válido, todo patrón sigue decodificando, y cada valor sale pegado al patrón equivocado
En una página codificada con IDs de símbolo Huffman y tablas provistas por el encoder, ese desajuste se ve como un escaneo malo: glifos desplazados unos píxeles, caracteres armados con los símbolos equivocados, o números plausibles hasta que un ancho se sale de rango varios segmentos después. La mayoría del JBIG2 en archivos PDF va codificado aritméticamente, y la mayor parte del JBIG2 Huffman se atiene a las tablas estándar B.1 a B.15. Los archivos que traen sus propias tablas son raros, y tienden a ejercitar de golpe el parser de tablas, el refinement Huffman y los bitmaps colectivos sin comprimir
Qué carga realmente un segmento Tables
Un segmento Tables describe exactamente una tabla Huffman. T.88 §7.4.13 y el Anexo B.2 lo presentan como un byte de flags, un HTLOW con signo de 32 bits, un HTHIGH con signo de 32 bits, y luego pares (longitud de prefijo, longitud de rango) que cubren el intervalo de HTLOW a HTHIGH por tramos. El bit 0 de los flags es HTOOB, que dice si la tabla tiene un código fuera de banda. Los bits 1 a 3 más uno dan HTPS, el ancho de cada campo de longitud de prefijo. Los bits 4 a 6 más uno dan HTRS, el ancho de cada campo de longitud de rango. El bit 7 está reservado
Las cotas son donde un parser escrito a mano suele tropezar primero. Lea HTLOW como sin signo y una tabla que arranca en -2, algo ordinario para anchos y altos de símbolos delta-codificados, de repente declara arrancar apenas por debajo de 4.3 mil millones. PDF Library for Delphi lee ambas cotas como valores con signo, rechaza una tabla cuyo HTLOW no esté por debajo de HTHIGH o cuya longitud de rango exceda 32, y contrasta cada campo leído contra el bit donde termina el dato declarado del segmento. Esa última revisión importa más de lo que suena: una tabla que lee más allá de su segmento consume el header del siguiente segmento como longitudes de prefijo y parsea hasta un sinsentido completo. Un bit 7 asignado falla con reserved custom Huffman table flag. Preferimos ese rechazo a una adivinanza, porque una tabla adivinada produce píxeles equivocados que nadie puede rastrear hasta su causa
Después de las líneas regulares vienen las líneas de escape del Anexo B.2. La línea de rango inferior arranca en HTLOW menos uno y cuenta hacia abajo con un offset de 32 bits, la línea de rango superior arranca en HTHIGH con un rango de 32 bits, y la línea fuera de banda existe solo cuando HTOOB está asignado. Estas líneas llevan los mismos marcadores que usan las tablas estándar integradas, así que una tabla propia y la Table B.6 pasan por un solo bucle de decodificación. Una tabla propia puede tener hasta 65,536 líneas
¿Por qué hay que asignar los códigos de prefijo en el orden de las líneas de la tabla?
Porque el orden de declaración es lo único que ata un patrón de bits a un rango de valores. El Anexo B.3 cuenta cuántas líneas usan cada longitud de prefijo, reparte primero los códigos de longitud 1, desplaza a la izquierda para pasar a longitud 2, y así; dentro de una misma longitud, los códigos van a las líneas en el orden en que aparecen en el segmento. Una tabla chica lo hace concreto. Seis líneas, llamadas A a F en orden de archivo, con estas longitudes de prefijo:
| Línea | Longitud de prefijo | Código (orden de archivo respetado) | Código si B y D se intercambian |
|---|---|---|---|
| A | 2 | 00 | 00 |
| B | 3 | 100 | 101 |
| C | 2 | 01 | 01 |
| D | 3 | 101 | 100 |
| E | 3 | 110 | 110 |
| F | 0 | sin uso | sin uso |
Ambas columnas son códigos de prefijo válidos. Solo la izquierda es lo que el encoder quiso decir: con B y D intercambiadas, el patrón 100 produce el rango de valores de D y el decoder sigue con un número del intervalo equivocado. La línea F tiene longitud 0, que B.3 define como sin uso; dele un código de un bit y toda asignación posterior se desplaza
La manera fácil de terminar en la columna derecha es un ordenamiento de propósito general por longitud de prefijo. El TArray.Sort y el TList<T>.Sort de Delphi no son estables, así que longitudes iguales pueden regresar en cualquier orden. Para códigos canónicos en su propia herramienta, recorra las líneas en orden de archivo una vez por longitud:
uses
System.SysUtils;
type
TCodeLine = record
PrefixLen: Integer; // tal como se lee del segmento Tables
Code: Cardinal; // asignado abajo
end;
// Asignación canónica del Anexo B.3, escrita para su propia herramienta.
// Las longitudes iguales conservan su orden de archivo; longitud 0 no recibe código.
procedure AssignCanonicalCodes(var Lines: array of TCodeLine);
var
LenCount: array[0..32] of UInt64;
FirstCode: array[0..32] of UInt64;
NextCode: UInt64;
CurLen, I: Integer;
begin
FillChar(LenCount, SizeOf(LenCount), 0);
for I := 0 to High(Lines) do
begin
if (Lines[I].PrefixLen < 0) or (Lines[I].PrefixLen > 32) then
raise Exception.Create('prefix length out of range');
Inc(LenCount[Lines[I].PrefixLen]);
end;
LenCount[0] := 0;
FirstCode[0] := 0;
for CurLen := 1 to 32 do
begin
FirstCode[CurLen] := (FirstCode[CurLen - 1] + LenCount[CurLen - 1]) shl 1;
// Chequeo de Kraft: más códigos de los que esta longitud puede contener
if FirstCode[CurLen] + LenCount[CurLen] > (UInt64(1) shl CurLen) then
raise Exception.Create('oversubscribed prefix lengths');
NextCode := FirstCode[CurLen];
for I := 0 to High(Lines) do // el orden de archivo desempata
if Lines[I].PrefixLen = CurLen then
begin
Lines[I].Code := Cardinal(NextCode);
Inc(NextCode);
end;
end;
end;
Aliméntelo con las seis líneas de arriba y A recibe 00, C recibe 01, B recibe 100, D recibe 101 y E recibe 110; F se queda sin código. PDF Library for Delphi hace el mismo trabajo con una pasada de conteo, estable por construcción y sin reescanear la tabla 32 veces, y aplica las mismas dos guardas. Longitudes sobreasignadas fallan con oversubscribed Huffman prefix codes; déjelas pasar y un patrón puede corresponder a dos líneas. Los prefijos de más de 32 bits también fallan por nombre, aunque T.88 los permite en el papel. Nos parece que ese techo es el trade-off correcto: cada código cabe en una palabra de máquina, y un archivo que necesita más es mucho más probable que esté dañado que ser ingenioso
Los valores merecen el mismo cuidado. La línea de rango inferior se decodifica como HTLOW menos uno menos un offset sin signo de 32 bits, y con un intermedio de Integer de 32 bits esa resta puede envolverse en un ancho positivo de apariencia ordinaria. El decoder lo hace en 64 bits y lanza Huffman value exceeds signed integer range cuando el resultado real no cabe
Orden de selectores, y dos trampas que se cubren entre sí
Una región de texto o un diccionario de símbolos no nombra sus tablas. Carga selectores de dos bits, y cada selector asignado a custom toma el siguiente segmento Tables de la lista referida, en orden de referencia. T.88 §7.4.3.1.2 ordena los selectores de región de texto como FS, DS, DT, RDW, RDH, RDX, RDY y RSIZE; §7.4.2.1.1 ordena los selectores del diccionario de símbolos como DH, DW, BMSIZE y AGGINST. Los valores 0 y 1 eligen tablas estándar, 3 elige una propia, y 2 está reservado en los campos con solo dos tablas estándar. PDF Library for Delphi rechaza los valores de selector reservados, y falla con missing custom Huffman table reference cuando los selectores quieren más tablas propias de las que la región refiere
Si escribe su propio decoder, aquí hay dos errores fáciles de cometer, y cada uno esconde al otro. El primero muerde en Delphi, o en FPC en modo Delphi (el modo objfpc rechaza el código como identificador duplicado): un setter de flags cuyo parámetro tiene el mismo nombre que el campo. El parámetro hace sombra al campo, Flags := Flags se asigna a sí mismo, y el campo queda en cero. Todos los selectores leen entonces cero, una región de texto usa las tablas estándar B.6, B.8 y B.11 para FS, DS y DT sin importar lo que pidió el archivo, y el camino propio jamás corre
El segundo vive en el diccionario de símbolos. Un diccionario Huffman puede guardar sus símbolos como un bitmap colectivo sin comprimir (BMSIZE de cero), donde cada fila termina en un byte parcial. El valor de padding es la cantidad de bits válidos en ese byte, y es tentador leerlo como la posición del bit válido más bajo:
// Último byte de una fila de bitmap colectivo, bits leídos MSB primero.
// Padding := TotalWidth mod 8 -> cantidad de bits VÁLIDOS en este byte
// Mal: trata Padding como el índice del bit más bajo a copiar
for BitPos := 7 downto Padding do
CopyPixel((LastByte shr BitPos) and 1);
// Bien: el índice del bit válido más bajo es 8 - Padding (0 cuando Padding = 0)
for BitPos := 7 downto ((8 - Padding) and 7) do
CopyPixel((LastByte shr BitPos) and 1);
// CopyPixel representa lo que sea que escriba el píxel en su propio decoder
Revise los anchos de frontera. Una fila de 63 píxeles tiene padding de 7: el bucle malo copia un bit del último byte, el bueno siete. Una fila de 9 píxeles tiene padding de 1: el malo copia siete, el bueno copia uno. Con 64 píxeles ambos copian los ocho, así que los múltiplos de 8 jamás lo exponen. Bitmaps colectivos sintéticos de 7 y 9 píxeles de ancho fijan ambos lados de la frontera del byte. Y el daño viaja, porque el refinement usa estos símbolos como referencias
Si su propio decoder tiene ambas trampas, depurarlo se vuelve una miseria. Corrija solo el setter y el camino propio por fin corre, directo a los bitmaps colectivos rotos, así que la página sigue viéndose mal. Corrija solo el padding y nada visible cambia, porque el camino propio sigue inalcanzable. Pruebe ambos con entradas sintéticas antes de confiar en cualquiera de los dos cambios. Un detalle relacionado: un diccionario de símbolos Huffman cuya suma de símbolos de entrada y nuevos da uno recibe del log2 una longitud de código de símbolo 0, pero la variante Huffman escribe cada ID de símbolo con al menos un bit. PDF Library for Delphi sube esa longitud a 1 en modo Huffman
¿De dónde lee su tamaño de bitmap el refinement Huffman?
De un campo BMSIZE que solo existe en modo Huffman, decodificado antes del dato de refinement aritmético. Cuando una instancia de región de texto tiene refinement (RI distinto de cero) y SBHUFF está asignado, T.88 §6.4.11 hace que el decoder lea RDW, RDH, RDX y RDY con sus tablas seleccionadas, luego BMSIZE con la tabla RSIZE, luego salte a una frontera de byte, y solo entonces corra la decodificación de refinement genérica sobre exactamente BMSIZE bytes. Las regiones de texto en modo aritmético no tienen tal campo, así que un decoder que comparte un solo camino de código entre los modos lo salta, arranca el decoder aritmético antes de tiempo, y refina cada símbolo contra ruido. El camino del diccionario de símbolos con REFAGG y una sola instancia de refinement (§6.5.8.2.2) tiene el mismo campo
PDF Library for Delphi acota BMSIZE por ambos lados. El mínimo es 2, el par inicial de bytes que el decoder aritmético siempre consume. El máximo es lo que queda del segmento actual, deliberadamente no del stream completo: un BMSIZE que solo quepa pidiendo prestados bytes del siguiente segmento está malformado, y un chequeo contra la longitud del stream dejaría al decoder aritmético leer dentro del header siguiente. Los tamaños fuera de rango fallan con invalid refinement bitmap size. Después el lector Huffman se reanuda exactamente BMSIZE bytes más allá del campo, sin importar cuánto leyó de más el decoder aritmético
La longitud declarada del segmento es la frontera
Cada header de segmento declara cuántos bytes de dato siguen, y el decoder mantiene el segmento en ese número en ambas direcciones: leer más allá del final es error, terminar antes es normal, y el siguiente header siempre se lee desde donde el archivo dijo que empieza. Reglas menores siguen. La longitud de dato desconocida de T.88 §7.2.7 se rechaza de entrada en vez de escanear en busca de un terminador. Los segmentos referidos deben llevar números menores que el actual y ya existir. END_OF_PAGE y END_OF_FILE deben declarar cero bytes de dato. Un segmento Profiles (tipo 52) debe ser exactamente 4 más 4 veces su conteo de identificadores; se salta pero se conserva, así que segmentos posteriores aún pueden referirlo, y un identificador de perfil desconocido no detiene la decodificación
El caso clásico de terminar antes es MMR. Un decoder MMR termina cuando produce el último píxel de la última fila, lo que puede ocurrir antes del terminador EOFB que T.88 §6.2.5.7 pone al final del dato. Un lector que asume que ahora está en el siguiente header parsea los bytes EOFB sobrantes como un número de segmento y falla unos bytes más adelante, lejos de la causa real. Saltar al final declarado evita la pregunta. El mismo hábito corre por cómo PDF Library for Delphi parsea estructuras PDF no confiables en general
Decodificar un stream de imagen JBIG2 desde Delphi
El decoder vive en la unit PDFlibJBIG2 como TPLJBIG2Decoder. LoadFromByteArray toma el dato JBIG2 embebido de una imagen /JBIG2Decode y, opcionalmente, el dato de su stream /JBIG2Globals, que es donde suelen vivir las tablas propias y los diccionarios de símbolos compartidos. Devuelve False ante cualquier rechazo y deja la razón en LastError; la salida de una llamada anterior se limpia al inicio de cada llamada, así que una decodificación fallida nunca deja píxeles viejos para que el llamador los confunda con un resultado
uses
System.SysUtils, PDFlibJBIG2;
// ImageBytes: el dato del stream de imagen con cualquier filtro previo
// a JBIG2Decode ya removido
// GlobalBytes: el dato del stream /JBIG2Globals, o un array vacío
function DecodeAndCountBlack(const ImageBytes, GlobalBytes: TJBIG2ByteArray;
out BlackPixels: Int64): Boolean;
var
Decoder: TPLJBIG2Decoder;
Line: TJBIG2ByteArray;
Row, Col: Integer;
begin
BlackPixels := 0;
Decoder := TPLJBIG2Decoder.Create;
try
Result := Decoder.LoadFromByteArray(ImageBytes, GlobalBytes);
if not Result then
begin
// Formato: 'JBIG2 segment <n> (type <t>) at byte <offset>: <reason>'
Writeln('JBIG2 refused: ', Decoder.LastError);
Exit;
end;
for Row := 0 to Decoder.Height - 1 do
begin
Decoder.GetScanline(Row, Line); // un byte por píxel, 0 o 1
for Col := 0 to Decoder.Width - 1 do
Inc(BlackPixels, Line[Col]); // 1 = negro en JBIG2
end;
finally
Decoder.Free;
end;
end;
El conteo de píxeles negros es un chequeo suyo, una huella digital barata para comparar contra un segundo renderer. Las fallas a nivel de segmento llegan precedidas del número de segmento, el tipo y el offset en bytes, así que un (type 53) delante de oversubscribed Huffman prefix codes apunta directo a un segmento Tables. Mantenga separados los dos canales de error: TPLJBIG2Decoder.LastError es el texto de diagnóstico de este decoder, mientras que TPDFlib.LastErrorCode es el código numérico de la API a nivel de librería. La selección de página sigue al stream: el decoder devuelve el bitmap del primer segmento page-information que encuentra y no busca la asociación de página 0, porque el JBIG2 embebido en PDF carga una página y los encoders la numeran 1 por rutina
Comparar renderers sobre estos archivos exige cierta humildad, porque los decoders JBIG2 independientes no siempre concuerdan en archivos con tablas propias y refinement Huffman. Que dos decoders discrepen no decide nada. Los campos del formato sí: qué tablas eligieron los selectores, qué declaró BMSIZE, dónde termina cada segmento
La codificación está cubierta en el artículo sobre backends de encoder JBIG2 y cómo se enlazan. El camino de lectura tiene que aceptar lo que el encoder de otro emitiera, y comparte sus reglas con el decoder TIFF integrado y sus chequeos de BigTIFF y layout en tiles: rechazar por nombre, jamás pedir bytes prestados a través de una frontera declarada, mantener la aritmética intermedia ancha. Si una carpeta de PDFs escaneados sigue haciendo tropezar a otros toolkits, revise si las imágenes son JBIG2 Huffman con tablas propias, y pruébelas contra el camino de lectura nativo Delphi y C++Builder descrito en la página del producto PDF Library for Delphi