Este es un error que un lector JBIG2 escrito a mano comete con el primer archivo inusual que encuentra. Parsea el header del archivo, parsea el primer header de segmento, lee la longitud de datos de cuatro bytes y toma esa cantidad de bytes como cuerpo del segmento. En la mayoría de los archivos .jb2 eso es exactamente correcto. En un archivo cuyo byte de flags tiene el bit 0 en cero, esos bytes del "cuerpo" son en realidad el segundo, tercer y cuarto header de segmento, y el decoder aritmético los convierte en un bitmap de ruido o se sale del final del buffer. El archivo no está roto. Usa la organización de acceso aleatorio del Anexo D.2 de ITU-T T.88, donde todos los headers de segmento van primero y todos los datos de los segmentos siguen después, en el mismo orden
Los archivos de acceso aleatorio son raros. Salen de sistemas de archivo histórico y de digitalización de documentos que quieren que un lector vea cada dependencia de página y de diccionario antes de tocar un byte comprimido. Si un trabajo de conversión mezcla archivos de una fuente así con escaneos ordinarios, un archivo de acceso aleatorio puede quedar en medio de un lote que por lo demás es todo secuencial. El decoder que los lee es el mismo decoder Pascal nativo que maneja tablas Huffman custom de JBIG2 y códigos de prefijo canónicos, así que el lado de la codificación no cambia; solo difiere el orden en que se encuentran los segmentos
Tres layouts de archivo detrás de un byte de flags
PDF Library for Delphi lee archivos JBIG2 independientes a través de TPLJBIG2Decoder en la unidad PDFlibJBIG2, y el decoder deduce el layout por su cuenta a partir del header del archivo. El Anexo D de T.88 permite disponer los mismos segmentos de tres maneras: secuencial (D.1) pone cada header justo delante de sus datos, acceso aleatorio (D.2) pone todos los headers primero y todos los datos después, y embebida (D.3) es la forma sin headers que se usa dentro de otros contenedores. Un archivo independiente empieza con el identificador de ocho bytes 97 4A 42 32 0D 0A 1A 0A, luego un byte de flags y luego un conteo de páginas de cuatro bytes, a menos que los flags digan que el conteo es desconocido
El bit 0 de ese byte de flags elige la organización: 1 significa secuencial, 0 significa acceso aleatorio. El bit 1 en 1 significa que el número de páginas es desconocido y el conteo de cuatro bytes está ausente, así que el primer header de segmento arranca en el byte 9 en lugar del 13. Los bits 2 a 7 están reservados; el decoder acepta los archivos que los encienden en lugar de rechazarlos, porque un bit reservado suelto no dice nada sobre si los segmentos en sí son legibles
PDF en sí nunca lleva el layout de acceso aleatorio. Un stream de imagen JBIG2Decode, descrito en ISO 32000-1 §7.4.7, contiene solo segmentos de página en forma embebida, con los diccionarios de símbolos compartidos movidos a un stream JBIG2Globals separado y sin header de archivo ni segmentos de end-of-page o end-of-file. Cuando el decoder no encuentra el identificador de ocho bytes, trata la entrada exactamente como ese tipo de stream: secuencial, de una página. En la dirección contraria, cuando PDF Library for Delphi exporta una imagen JBIG2Decode como archivo .jb2 nativo, escribe el byte de flags $03 (secuencial, conteo de páginas desconocido) y agrega al final un header de segmento de end-of-file. Así que el acceso aleatorio solo llega desde afuera, como un archivo independiente entregado directamente a TPLJBIG2Decoder, normalmente camino a ser convertido o recomprimido para PDF con los backends de encoder JBIG2 en PDFlibPas
¿Por qué no se puede leer un archivo de acceso aleatorio en orden de archivo?
Porque nada en el stream de bytes marca dónde termina el bloque de headers y empieza el bloque de datos, salvo el propio header de segmento de end-of-file (tipo 51). Los headers de segmento JBIG2 no tienen tamaño fijo. El conteo de segmentos referidos es o una forma corta de tres bits o una forma larga seguida de un bitmap de retención; cada número de segmento referido toma uno, dos o cuatro bytes según qué tan grande sea el número del propio segmento actual; el campo de asociación de página es de uno o cuatro bytes. Un lector tiene que parsear cada campo de un header solo para saber dónde arranca el siguiente
Un lector secuencial que recorre el archivo en orden acierta el primer header y se equivoca en todo lo que sigue. Peor aún, puede tardar en notarlo: un decoder de región genérica alimentado con bytes de header produce píxeles, no un error, hasta que alguna comprobación posterior de longitud o referencia finalmente se dispara. Nuestra opinión es que un decoder que no indexa los headers primero debería rechazar de plano los archivos de acceso aleatorio. Un rechazo claro cuesta un archivo en un lote; un bitmap de aspecto plausible pero equivocado puede terminar en un archivo histórico y quedarse ahí
Indexar cada header antes del primer cuerpo
Cuando el byte de flags dice acceso aleatorio, PDF Library for Delphi hace una pasada sobre el bloque de headers antes de decodificar nada. Cada header se parsea por completo y luego se descarta; lo único que se conserva es su offset de byte en el archivo. Los datos comprimidos nunca se copian a un segundo buffer reordenado, lo que importa en escaneos de archivo grandes donde el bloque de datos puede ser la mayor parte del archivo. La pasada se detiene en el primer header de end-of-file, y en ese punto la posición de lectura queda sobre el primer byte de los datos del primer segmento
Un archivo de acceso aleatorio tiene menos redundancia que uno secuencial, así que el pre-scan es estricto. Rechaza el archivo cuando:
- un header queda cortado por el final del buffer, reportado como
JBIG2 truncated random-access header at byte Ncon el offset donde empezaba ese header - los números de segmento no crecen estrictamente (comparados como valores sin signo), porque dos headers que reclaman el mismo número vuelven ambigua la lista de referidos de una región posterior
- una longitud de datos tiene el bit más alto encendido, lo que incluye el marcador de "longitud desconocida"
0xFFFFFFFF; en este layout no hay otra forma de encontrar dónde arranca el siguiente cuerpo, así que el decoder lo rechaza de inmediato en lugar de escanear hacia adelante buscando un marcador de fin - el header de end-of-file declara datos, o nunca aparece (
JBIG2 random-access file is missing its end-of-file header) - las longitudes declaradas suman más que los bytes que quedan, o menos
La última regla es con la que la gente tropieza. Un solo byte extra después del último cuerpo falla con JBIG2 trailing random-access data. Eso parece duro al lado de la ruta secuencial, donde la decodificación simplemente se detiene en el segmento de end-of-file y cualquier cosa después se ignora. La asimetría tiene una razón. En el layout secuencial cada header está justo al lado de sus propios datos, así que la basura al final no puede haber corrido nada. En el layout de acceso aleatorio los headers son el único mapa del bloque de datos, y si las longitudes no cuadran, el decoder no puede distinguir un relleno inofensivo de un byte perdido o agregado mucho antes que corrió todos los cuerpos siguientes
¿Por qué un bucle que cuenta bytes pierde el último segmento de end-of-page?
Un bucle que corre "mientras queden bytes" termina demasiado pronto en un archivo de acceso aleatorio, porque los datos se agotan antes que el índice de headers. Los segmentos de end-of-page (tipo 49) y end-of-file llevan cero bytes de datos y normalmente van al final. Una vez consumido el cuerpo de la última región, la posición de lectura queda exactamente al final del buffer; un bucle que cuenta bytes sale ahí, y el segmento de end-of-page de longitud cero nunca se despacha, lo que deja la página sin terminar
Así que para los archivos de acceso aleatorio el decoder cuenta headers, no bytes. Cada paso salta de vuelta al siguiente header indexado, se realinea a un límite de byte (un cuerpo codificado aritméticamente puede terminar a mitad de byte), parsea el header de nuevo y luego salta hacia adelante hasta donde arranca el siguiente cuerpo y lo pasa de largo según la longitud declarada. Un cuerpo cuyo decoder termina antes se salta hasta su fin declarado; uno que lee más allá se rechaza. Desde ahí el segmento pasa por los mismos handlers y comprobaciones de referencias que en un archivo secuencial. Los errores lanzados al decodificar un cuerpo llevan la posición original del header, en la forma JBIG2 segment N (type T) at byte B: ..., así que el offset en LastError apunta a algo que usted puede encontrar en un editor hexadecimal
Cargar un archivo .jb2 de acceso aleatorio en Delphi
El código de aplicación no cambia con la organización. TPLJBIG2Decoder.LoadFromByteArray detecta el header del archivo y el layout, devuelve False ante cualquier entrada rechazada con el motivo en LastError, y expone la página decodificada a través de Width, Height y GetScanline, que llena un byte por píxel con 1 para negro
uses
SysUtils, Classes, PDFlibJBIG2;
function ReadJb2(const FileName: string): TJBIG2ByteArray;
var
FS: TFileStream;
begin
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
SetLength(Result, FS.Size);
if Length(Result) > 0 then
FS.ReadBuffer(Result[0], Length(Result));
finally
FS.Free;
end;
end;
function CountBlackPixels(const FileName: string): Integer;
var
Decoder: TPLJBIG2Decoder;
Row: TJBIG2ByteArray;
X, Y: Integer;
begin
Result := 0;
Decoder := TPLJBIG2Decoder.Create;
try
// Los archivos independientes secuenciales y de acceso aleatorio usan la misma llamada
if not Decoder.LoadFromByteArray(ReadJb2(FileName)) then
raise Exception.Create('JBIG2 rejected: ' + Decoder.LastError);
for Y := 0 to Decoder.Height - 1 do
if Decoder.GetScanline(Y, Row) then
for X := 0 to Decoder.Width - 1 do
if Row[X] = 1 then
Inc(Result);
finally
Decoder.Free;
end;
end;
Si quiere saber qué layout usa un archivo, para registrarlo o para ordenar un corpus antes de una corrida de conversión, lea el byte de flags usted mismo. La biblioteca no expone la organización que detectó, y nueve bytes son todo lo que necesita. El helper de abajo es algo para agregar a su propio código; simplemente repite las reglas de header de T.88:
type
TJb2Layout = (jlNoFileHeader, jlSequential, jlRandomAccess);
// Helper propio, no es parte de PDFlibJBIG2: clasifica un buffer por su header de archivo T.88
function DetectJb2Layout(const Data: TJBIG2ByteArray;
out PageCountKnown: Boolean): TJb2Layout;
const
Signature: array[0..7] of Byte = ($97, $4A, $42, $32, $0D, $0A, $1A, $0A);
var
I: Integer;
begin
Result := jlNoFileHeader; // p. ej. un stream embebido sacado de un PDF
PageCountKnown := False;
if Length(Data) < 9 then
Exit;
for I := 0 to 7 do
if Data[I] <> Signature[I] then
Exit;
PageCountKnown := (Data[8] and 2) = 0; // bit 1 en 1: sin conteo de páginas de cuatro bytes
if (Data[8] and 1) = 0 then
Result := jlRandomAccess
else
Result := jlSequential;
end;
Trabajos por lotes: buffers con relleno y archivos rechazados
La regla de los datos finales es donde el código por lotes tiende a equivocarse, y cuando un archivo que abre en otro lado se rechaza así, mire el buffer antes que el archivo. Un .jb2 sacado de una columna BLOB de base de datos, una lectura de red de tamaño fijo o un formato contenedor con relleno de alineación puede llegar con unos cuantos bytes cero después del final real. Un archivo secuencial se encoge de hombros. Un archivo de acceso aleatorio con el mismo relleno falla con JBIG2 trailing random-access data, y lo mismo pasa con un buffer cortado unos bytes antes, que falla como truncado. Pase exactamente los bytes del archivo, dimensionados según la longitud real de la fuente, nunca según un tamaño de chunk o una capacidad
Un archivo rechazado tampoco deja nada atrás. LoadFromByteArray reinicia Width, Height y las scanlines antes de empezar a decodificar, así que una llamada fallida reporta dimensiones cero en lugar de la página del archivo anterior, y la misma instancia del decoder puede seguir directo con el siguiente archivo:
procedure DecodeBatch(const Files, Log, Quarantine: TStrings);
var
Decoder: TPLJBIG2Decoder;
I: Integer;
begin
Decoder := TPLJBIG2Decoder.Create;
try
for I := 0 to Files.Count - 1 do
if Decoder.LoadFromByteArray(ReadJb2(Files[I])) then
Log.Add(Format('%s: %d x %d', [Files[I], Decoder.Width, Decoder.Height]))
else
begin
// Width y Height son 0 aquí; nada del archivo anterior sobrevive
Log.Add(Files[I] + ': ' + Decoder.LastError);
if Pos('trailing random-access data', Decoder.LastError) > 0 then
Quarantine.Add(Files[I]); // revise cómo se almacenaron los bytes antes de culpar al archivo
end;
finally
Decoder.Free;
end;
end;
Comprobar esto en su propio pipeline no necesita archivos de muestra raros. Reorganice un .jb2 secuencial al layout de acceso aleatorio, conservando cada header y cada byte comprimido, mueva los headers de end-of-page y end-of-file al final del bloque de headers, y los píxeles decodificados deben coincidir exactamente con el original secuencial, tanto con el campo de conteo de páginas como sin él. Luego aliméntele cada prefijo truncado, un número de segmento duplicado, un byte extra al final y una longitud 0xFFFFFFFF; cada uno debería devolver False con Width y Height en cero
Qué devuelve el decoder, y qué espera
El acceso aleatorio aquí es una propiedad del layout del archivo. TPLJBIG2Decoder devuelve el bitmap de la primera página sea cual sea la organización que use el archivo, y Width y Height describen esa página. Cada segmento debe declarar su longitud de datos; T.88 permite que una región genérica inmediata use el marcador 0xFFFFFFFF, y el decoder rechaza ese marcador en cualquiera de los dos layouts con unknown or oversized segment length is not supported. Las longitudes de prefijo Huffman custom y los conteos de entradas de tabla se limitan de la misma manera que en los archivos secuenciales
Decodificar JBIG2 es una etapa de una ruta de imágenes más larga que también cubre la extracción de imágenes PDF, la recompresión y la codificación JBIG2; la página de producto de PDF Library for Delphi cubre el resto de ese pipeline y los formatos de imagen soportados