PDFlibPas versión 3.539.23 decodifica archivos JBIG2 independientes que usan la organización de acceso aleatorio del Anexo D.2 de ITU-T T.88, donde todas las cabeceras de segmento van primero y los datos de segmento llegan después en el mismo orden. El decodificador Pascal nativo de PDFlibJBIG2.pas indexa los offsets de cabecera hasta la obligatoria cabecera de fin de archivo, comprueba 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 cabecera sin copiar ni reordenar los datos comprimidos. Antes de esta release el mismo archivo soltaba un error plano de «random-access organisation is not supported» en cuanto se leían los flags de cabecera
Los archivos JBIG2 de acceso aleatorio son raros, que es justo la razón por la que duelen cuando aparecen. Salen de pipelines de archivo y sistemas de imaging documental que quieren que un lector vea cada cabecera de segmento, y por tanto cada dependencia de página y diccionario, antes de tocar un solo byte comprimido. Una aplicación Delphi que convierte por lotes archivos escaneados a PDF suele encontrarse uno de estos en mitad de un trabajo, después de que cientos de archivos secuenciales pasaran limpios, y un decodificador que se planta en seco ante un archivo bien formado es apenas mejor que uno que renderiza basura. La misma línea de releases acababa de enseñar al decodificador las tablas Huffman personalizadas de JBIG2 y los códigos prefijo canónicos, así que el acceso aleatorio era el último hueco de organización que quedaba en los límites de capacidad documentados del decodificador
¿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 cabecera con sus datos, acceso aleatorio (D.2) pone todas las cabeceras primero y todos los datos después, y embebida (D.3) es la forma sin cabecera 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 se conoce el número de páginas, un conteo de páginas de cuatro bytes. El bit 0 del byte de flags elige la organización, con 1 significando secuencial y 0 acceso aleatorio; el bit 1 puesto 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 vez 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 = número 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 cabecera 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 esta disposición. Un stream de imagen JBIG2Decode, descrito en ISO 32000-1 §7.4.7, guarda solo los segmentos de página en organización embebida, con los diccionarios de símbolos compartidos movidos a un stream JBIG2Globals aparte y sin cabecera de archivo ni segmentos de fin de página o de fin de archivo. Cuando decodeJBIG2 no encuentra el identificador de ocho bytes asume exactamente eso y fuerza decodificación secuencial de una página. El export nativo de imágenes JBIG2 va por el otro lado y envuelve los segmentos del PDF en un archivo independiente cuyo byte de flags es $03, secuencial con número de páginas desconocido, seguido de una cabecera de fin de archivo añadida al final. Así que el trabajo de acceso aleatorio toca un solo camino: los archivos independientes que se entregan directamente a TPLJBIG2Decoder, normalmente antes de convertirlos o recomprimirlos para PDF, el trabajo del que se encargan del lado de salida los backends de encoder JBIG2 en PDFlibPas
¿Por qué no se puede leer un archivo de acceso aleatorio en orden de archivo?
Un archivo de acceso aleatorio no se puede leer en orden de archivo porque nada en el stream de bytes marca dónde para el bloque de cabeceras y dónde empieza el de datos, salvo la propia cabecera del segmento de fin de archivo. Las cabeceras 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 ocupan 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 la primera cabecera, lee su longitud de datos, y luego trata los primeros bytes de la segunda cabecera como los datos de ese segmento. El decodificador no puede saber que se ha equivocado hasta mucho después, que es la razón por la que el código viejo rechazaba la organización de plano en lugar de intentarlo
¿Cómo indexa PDFlibPas las cabeceras de segmento de acceso aleatorio?
PDFlibPas indexa las cabeceras de acceso aleatorio en un único preescaneo, IndexRandomHeaders, que parsea cada cabecera, registra solo su offset en bytes, y se para en la primera cabecera de fin de archivo (tipo de segmento 51). Cada cabecera se parsea completa y se descarta, así que el índice es un array de enteros y no una lista de objetos, y el preescaneo va acumulando las longitudes de datos declaradas a medida que avanza. Cuando el escaneo termina, el lector está sobre el primer byte de los datos del primer segmento, y esa posición se convierte en NextBodyOffset
// IndexRandomHeaders, local de 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; // crecido 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 comprobación 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 cabeceras que reclamen 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 puesto, incluido el marcador de «longitud desconocida» 0xFFFFFFFF, a -1; en disposición de acceso aleatorio no hay otra forma de encontrar dónde empieza el siguiente cuerpo, así que PDFlibPas rechaza esa longitud de inmediato en vez de buscar 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 severidad es deliberada: en esta disposición un desajuste de longitud significa que todos los cuerpos posteriores al punto del error están desplazados, y un decodificador que margine un byte suelto no tiene forma de saber si es padding inofensivo o el primer síntoma de datos desalineados
¿Por qué se perdía el último segmento de fin de página?
El último segmento de fin de página se perdía porque la primera versión del bucle de decodificación conservaba el test de terminación secuencial, while not reader.isFinished, y en disposición de acceso aleatorio el stream de datos se acaba antes que el índice de cabeceras. Los segmentos de fin de página (tipo 49) y de fin de archivo llevan cero bytes de datos, y normalmente son las últimas cabeceras del archivo. Tras consumir el cuerpo de la última región el lector está exactamente 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 cabeceras en lugar de bytes. Cada iteración salta el lector a la siguiente cabecera indexada, reajusta bitPointer a 7 porque el cuerpo anterior pudo acabar a mitad de byte, reparsea esa cabecera, y después mueve bytePointer a NextBodyOffset y lo avanza más allá del cuerpo. Los handlers de segmento existentes, las comprobaciones de segmentos referidos y los diagnósticos de Context corren sin cambios, y un mensaje de error sigue informando del offset en bytes original de la cabecera, no de 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 buscar hasta DataEnd
end;
¿Qué demuestra en realidad la validación de acceso aleatorio?
La validación demuestra que los bytes reorganizados decodifican a los mismos píxeles que sus originales secuenciales, y que la entrada de acceso aleatorio malformada falla limpio; no demuestra cobertura de archivos de acceso aleatorio de encoders arbitrarios. La regresión Pascal compartida usa un archivo sintético de 235 bytes montado sobre un fixture de tabla personalizada que debe decodificar a una fila de 7 por 1 píxeles negros, tanto con número de páginas conocido como con el campo de conteo eliminado, y luego alimenta al decodificador con cada prefijo truncado de ese archivo, un número de segmento duplicado, un byte final sobrante y una longitud de datos desconocida, asegurando cada vez que LoadFromByteArray devuelve False y deja Width y Height a cero. El caso de imagen real es una imagen de refinamiento de tabla personalizada de 500 por 473 cuyos segmentos se reorganizaron a disposición de acceso aleatorio conservando cada cabecera 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 natural de acceso aleatorio encontrado en la naturaleza, y no había disponible ninguna muestra natural semejante. Las suites pasaron con 1.598 tests en Delphi Win32, 42 en la suite de imágenes de Delphi Win64, 48 en FPC Win32 y 46 en FPC Win64, junto a los tres casos secuenciales de píxeles ya 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 la cabecera de archivo y la organización, devuelve False ante cualquier entrada rechazada con el motivo en LastError, y expone la página decodificada mediante 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 archivo, 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 de forma perezosa. Los segmentos con longitud de datos desconocida se rechazan en archivos de acceso aleatorio, y los límites existentes sobre longitudes de prefijo Huffman personalizadas y conteos de entradas de tabla no cambian. Esos límites son lo bastante estrechos para que una aplicación Delphi pueda derivar los casos rechazados a otro sitio vía LastError, y el resto del pipeline de imágenes, de la extracción de imágenes PDF al encoding JBIG2, está cubierto en la página de producto de la librería PDF PDFlibPas para Delphi