PDFlibPas versión 3.539.23 decodifica archivos JBIG2 independientes que usan la organización de acceso aleatorio de ITU-T T.88 Anexo D.2, donde todos los headers de segmento van primero y los datos de los segmentos siguen en el mismo orden. El decoder Pascal nativo en PDFlibJBIG2.pas indexa los offsets de los headers hasta el header obligatorio de end-of-file, chequea que los números de segmento crezcan y que las longitudes de datos declaradas sumen exactamente los bytes que quedan, y después decodifica cada cuerpo en orden de header sin copiar ni reordenar los datos comprimidos. Antes de esta release, el mismo archivo levantaba un seco "random-access organisation is not supported" apenas se leían los flags del header
Los archivos JBIG2 de acceso aleatorio son raros, que es exactamente por qué duelen cuando aparecen. Salen de pipelines de archivo histórico y sistemas de digitalización de documentos que quieren que un lector vea cada header de segmento, y por lo tanto cada dependencia de página y de diccionario, antes de tocar un solo byte comprimido. Una aplicación Delphi que convierte en lote archivos escaneados a PDF suele toparse con uno de estos en medio de un trabajo, después de que cientos de archivos secuenciales pasaron limpiamente, y un decoder que se planta en seco ante un archivo bien formado es apenas marginalmente mejor que uno que renderiza basura. La misma línea de release acababa de enseñarle al decoder tablas Huffman custom de JBIG2 y códigos de prefijo canónicos, así que el acceso aleatorio era el último hueco de organización que quedaba en los límites de capacidades documentados del decoder
¿Qué es la organización de acceso aleatorio de JBIG2?
La organización de acceso aleatorio es una de las tres formas en que el Anexo D de T.88 permite disponer los mismos segmentos: secuencial (D.1) intercala cada header con 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 como PDF. Un archivo .jb2 independiente empieza con el identificador de ocho bytes 97 4A 42 32 0D 0A 1A 0A, seguido de un byte de flags y, cuando el conteo de páginas es conocido, un conteo de páginas de cuatro bytes. El bit 0 del byte de flags selecciona la organización, con 1 significando secuencial y 0 significando 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. PDFlibPas los lee en checkHeader y setFileHeaderFlags, y los bits reservados 2 a 7 se toleran en lugar de rechazarse
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = acceso aleatorio (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = conteo de páginas omitido
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// stream PDF: sin header de archivo, organización embebida, una página
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF en sí nunca lleva este layout. Un stream de imagen JBIG2Decode, descrito en ISO 32000-1 §7.4.7, contiene solo los segmentos de página en organización 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 decodeJBIG2 no encuentra el identificador de ocho bytes, asume exactamente eso y fuerza una decodificación secuencial de una sola página. El export de imágenes JBIG2 nativo va en la dirección contraria y envuelve los segmentos del PDF en un archivo independiente cuyo byte de flags es $03, secuencial con conteo de páginas desconocido, seguido de un header de end-of-file agregado al final. Así que el trabajo de acceso aleatorio toca un solo camino: archivos independientes entregados directamente a TPLJBIG2Decoder, típicamente antes de convertirlos o recomprimirlos para PDF, el trabajo que los backends de encoder JBIG2 en PDFlibPas manejan del lado de salida
¿Por qué no se puede leer un archivo de acceso aleatorio en orden de archivo?
Un archivo de acceso aleatorio no puede leerse en orden de archivo porque nada en el stream de bytes marca dónde se detiene el bloque de headers y empieza el bloque de datos, salvo el propio header de segmento de end-of-file. Los headers de segmento JBIG2 tienen longitud variable: el conteo de segmentos referidos puede ser una forma corta de tres bits o una forma larga con un bitmap de retención, los números de segmentos referidos toman uno, dos o cuatro bytes según el número del propio segmento, y el campo de asociación de página es de uno o cuatro bytes. Un lector secuencial ingenuo parsea el primer header, lee su longitud de datos y después trata los primeros bytes del segundo header como los datos de ese segmento. El decoder no puede darse cuenta de que se equivocó hasta mucho después, que es por qué el código viejo rechazaba la organización de plano en lugar de intentarlo
¿Cómo indexa PDFlibPas los headers de segmentos de acceso aleatorio?
PDFlibPas indexa los headers de acceso aleatorio en un solo pre-scan, IndexRandomHeaders, que parsea cada header, registra únicamente su offset de byte y se detiene en el primer header de end-of-file (segmento tipo 51). Cada header se parsea por completo y se descarta, así que el índice es un array de enteros y no una lista de objetos, y el pre-scan acumula las longitudes de datos declaradas sobre la marcha. Cuando el scan termina, el lector queda parado en el primer byte de los datos del primer segmento, y esa posición se convierte en NextBodyOffset
// IndexRandomHeaders, local a TJBIG2StreamDecoder.readSegments
while not reader.isFinished do
begin
Offset := reader.bytePointer;
Header := TSegmentHeader.Create;
try
readSegmentHeader(Header);
if reader.BufferOverrun then
raise EJBIG2DecodeError.CreateFmt(
'JBIG2 truncated random-access header at byte %d', [Offset]);
if (HeaderCount > 0) and
(Cardinal(Header.getSegmentNumber) <= Cardinal(PreviousNumber)) then
raise EJBIG2DecodeError.Create('JBIG2 random-access segment numbers must increase');
PreviousNumber := Header.getSegmentNumber;
Count := Header.getSegmentDataLength;
if Count < 0 then
raise EJBIG2DecodeError.Create('JBIG2 unknown or oversized segment length is not supported');
Inc(TotalLength, Count); // acumulador Int64
HeaderOffsets[HeaderCount] := Offset; // crece por bloques
Inc(HeaderCount);
if Header.getSegmentType = JBIG2_END_OF_FILE then
begin
if Count <> 0 then
raise EJBIG2DecodeError.Create('JBIG2 invalid end segment length');
FoundEnd := True;
Break;
end;
finally
Header.Free;
end;
end;
if not FoundEnd then
raise EJBIG2DecodeError.Create('JBIG2 random-access file is missing its end-of-file header');
if TotalLength > Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 truncated random-access segment data');
if TotalLength < Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 trailing random-access data');
NextBodyOffset := reader.bytePointer;
Cada chequeo de ese bucle existe porque un archivo de acceso aleatorio tiene menos redundancia que uno secuencial. Los números de segmento deben crecer estrictamente, comparados como valores sin signo, porque dos headers que reclaman el mismo número dejan ambiguo a qué cuerpo se refiere la lista de referidos de una región posterior. El campo de longitud de datos lo lee handleSegmentDataLength, que mapea cualquier valor con el bit alto encendido, incluido el marcador de "longitud desconocida" 0xFFFFFFFF, a -1; en el layout de acceso aleatorio no hay otra forma de encontrar dónde empieza el siguiente cuerpo, así que PDFlibPas rechaza esa longitud de inmediato en lugar de escanear buscando un marcador de fin. El total debe coincidir con los bytes restantes exactamente en ambas direcciones, y un solo byte extra después del último cuerpo falla con "trailing random-access data". Esa rigidez es deliberada: en este layout un desajuste de longitud significa que todo cuerpo después del punto del error está corrido, y un decoder que ignora un byte suelto no tiene forma de saber si es padding inofensivo o el primer síntoma de datos desalineados
¿Por qué desaparecía el último segmento de end-of-page?
El último segmento de end-of-page desaparecía porque la primera versión del bucle de decodificación conservaba el test de terminación secuencial, while not reader.isFinished, y en el layout de acceso aleatorio el stream de datos se agota 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 son los últimos headers del archivo. Después de consumir el cuerpo de la última región, el lector queda justo al final del buffer, así que el bucle sale y esos segmentos de longitud cero nunca se despachan, dejando la página sin terminar. El fix hace que el bucle de acceso aleatorio cuente headers en lugar de bytes. Cada iteración salta el lector al siguiente header indexado, resetea bitPointer a 7 porque el cuerpo anterior puede haber terminado a mitad de byte, re-parsea ese header, y después mueve bytePointer a NextBodyOffset y lo avanza más allá del cuerpo. Los handlers de segmento existentes, los chequeos de segmentos referidos y los diagnósticos de Context corren sin cambios, y un mensaje de error sigue reportando el offset de byte original del header, no la posición del cuerpo
// TJBIG2StreamDecoder.readSegments, bucle principal
if randomAccessOrganisation then
IndexRandomHeaders;
while (randomAccessOrganisation and (HeaderIndex < HeaderCount)) or
((not randomAccessOrganisation) and (not reader.isFinished)) do
begin
if randomAccessOrganisation then
begin
reader.bytePointer := HeaderOffsets[HeaderIndex];
reader.bitPointer := 7; // realinear tras un byte parcial
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // usado en el contexto de error
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // saltar a los datos de este segmento
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... despachar al handler de segmento existente, y luego posicionar en DataEnd
end;
¿Qué prueba realmente la validación de acceso aleatorio?
La validación prueba que los bytes reorganizados decodifican a los mismos píxeles que sus originales secuenciales, y prueba que la entrada de acceso aleatorio malformada falla limpiamente; no prueba cobertura de archivos de acceso aleatorio venidos de encoders arbitrarios. La regresión Pascal compartida usa un archivo sintético de 235 bytes construido sobre un fixture de tabla custom que debe decodificar a una fila de 7 por 1 píxeles negros, tanto con un conteo de páginas conocido como con el campo de conteo removido, y después le alimenta al decoder cada prefijo truncado de ese archivo, un número de segmento duplicado, un byte final extra y una longitud de datos desconocida, afirmando cada vez que LoadFromByteArray devuelve False y deja Width y Height en cero. El caso de imagen real es una imagen de refinement de tabla custom de 500 por 473 cuyos segmentos se reorganizaron a layout de acceso aleatorio conservando cada header original y cada byte comprimido; su SHA-256 coincide exactamente con el baseline secuencial revisado. Ese archivo es un derivado producido por una transformación de organización, no un documento de acceso aleatorio natural encontrado en la naturaleza, y no había disponible una muestra natural así. Las suites pasaron con 1,598 tests para Delphi Win32, 42 para la suite de imágenes de Delphi Win64, 48 para FPC Win32 y 46 para FPC Win64, junto a los tres casos de píxeles secuenciales existentes
Cargar un archivo .jb2 de acceso aleatorio y sus límites
El código de aplicación no cambia: TPLJBIG2Decoder.LoadFromByteArray detecta por su cuenta el header de archivo y la organización, 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 devuelve un byte por píxel
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;
Los límites conviene enunciarlos sin rodeos. El soporte de acceso aleatorio es una función de organización de archivos, no una API de páginas aleatorias: TPLJBIG2Decoder sigue devolviendo el bitmap de la primera página, y no hay llamada para elegir la página 7 de un archivo de 40 páginas ni para decodificar páginas en forma lazy. Los segmentos con longitud de datos desconocida se rechazan en archivos de acceso aleatorio, y los límites existentes sobre longitudes de prefijo Huffman custom y conteos de entradas de tabla quedan igual. Esos límites son lo bastante angostos para que una aplicación Delphi pueda derivar los casos rechazados a otro lado vía LastError, y el resto del pipeline de imágenes, desde la extracción de imágenes PDF hasta la codificación JBIG2, está cubierto en la página de producto de la librería PDF PDFlibPas para Delphi