PDFlibPas versione 3.539.23 decodifica i file JBIG2 standalone che usano l'organizzazione random-access di ITU-T T.88 Allegato D.2, dove ogni header di segmento viene prima e i dati dei segmenti seguono nello stesso ordine. Il decoder Pascal nativo in PDFlibJBIG2.pas indicizza gli offset degli header fino all'obbligatorio header end-of-file, verifica che i numeri di segmento crescano e che le lunghezze dichiarate dei dati sommino esattamente ai byte che restano, e poi decodifica ogni corpo in ordine di header senza copiare o riordinare i dati compressi. Prima di questa release lo stesso file alzava un secco errore «random-access organisation is not supported» appena letti gli header flag
I file JBIG2 random-access sono rari, ed è esattamente per questo che fanno male quando si presentano. Escono da pipeline di archiviazione e sistemi di document imaging che vogliono far vedere al reader ogni header di segmento, e quindi ogni pagina e dipendenza di dizionario, prima di toccare un solo byte compresso. Un'applicazione Delphi che converte in batch archivi scansionati in PDF di solito ne incontra uno in mezzo a un lavoro, dopo che centinaia di file sequenziali sono passati lisci, e un decoder che si blocca su un file ben formato è solo marginalmente meglio di uno che renderizza spazzatura. La stessa linea di release aveva appena finito di insegnare al decoder le tabelle Huffman custom e i codici prefisso canonici di JBIG2, quindi il random access era l'ultimo buco di organizzazione rimasto nei limiti di capacità documentati del decoder
Che cos'è l'organizzazione random-access di JBIG2?
L'organizzazione random-access è uno dei tre modi in cui l'Allegato D di T.88 consente di disporre gli stessi segmenti: sequential (D.1) intreccia ogni header con i suoi dati, random-access (D.2) mette tutti gli header prima e tutti i dati dopo, ed embedded (D.3) è la forma senza header usata dentro altri container come il PDF. Un file .jb2 standalone inizia con l'identificatore di otto byte 97 4A 42 32 0D 0A 1A 0A, seguito da un byte di flag e, quando il numero di pagine è noto, da un conteggio pagine di quattro byte. Il bit 0 del byte di flag sceglie l'organizzazione, con 1 che significa sequential e 0 che significa random-access; il bit 1 a 1 significa che il numero di pagine è sconosciuto e il conteggio a quattro byte è assente. PDFlibPas li legge in checkHeader e setFileHeaderFlags, e i bit riservati da 2 a 7 vengono tollerati anziché rifiutati
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = numero di pagine omesso
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// stream PDF: nessun header di file, organizzazione embedded, una pagina
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
Il PDF stesso non porta mai questo layout. Uno stream immagine JBIG2Decode, descritto in ISO 32000-1 §7.4.7, contiene solo i segmenti di pagina in organizzazione embedded, con i dizionari di simboli condivisi spostati in uno stream JBIG2Globals separato e niente header di file, segmenti end-of-page o end-of-file. Quando decodeJBIG2 non trova l'identificatore di otto byte presume esattamente questo e forza una decodifica sequenziale a pagina singola. L'export di immagini JBIG2 nativo va nella direzione opposta e avvolge i segmenti PDF in un file standalone il cui byte di flag è $03, sequential con numero di pagine sconosciuto, seguito da un header end-of-file accodato. Quindi il lavoro sul random access tocca un solo percorso: i file standalone consegnati direttamente a TPLJBIG2Decoder, tipicamente prima che vengano convertiti o ricompressi per il PDF, il lavoro di cui si occupano sul lato output i backend encoder JBIG2 in PDFlibPas
Perché un file random-access non si può leggere in ordine di file?
Un file random-access non si può leggere in ordine di file perché nulla nel flusso di byte segna dove il blocco degli header finisce e inizia quello dei dati, tranne l'header del segmento end-of-file stesso. Gli header dei segmenti JBIG2 hanno lunghezza variabile: il conteggio dei segmenti referenziati può essere una forma corta a tre bit o una forma lunga con una retention bitmap, i numeri dei segmenti referenziati occupano uno, due o quattro byte a seconda del numero del segmento stesso, e il campo di associazione pagina è di uno o quattro byte. Un lettore sequenziale ingenuo parsa il primo header, legge la lunghezza dei suoi dati, e poi prende i primi byte del secondo header come dati di quel segmento. Il decoder non può accorgersi di essere sbagliato se non molto dopo, ed è per questo che il vecchio codice rifiutava l'organizzazione in blocco invece di tentarla
Come indicizza PDFlibPas gli header dei segmenti random-access?
PDFlibPas indicizza gli header random-access in una sola pre-scansione, IndexRandomHeaders, che parsifica ogni header, registra soltanto il suo offset in byte, e si ferma al primo header end-of-file (tipo di segmento 51). Ogni header viene parsato per intero e scartato, così l'indice è un array di interi anziché una lista di oggetti, e la pre-scansione accumula le lunghezze dichiarate dei dati mentre procede. Quando la scansione finisce, il reader sta sul primo byte dei dati del primo segmento, e quella posizione diventa NextBodyOffset
// IndexRandomHeaders, locale 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); // accumulatore Int64
HeaderOffsets[HeaderCount] := Offset; // cresce a blocchi
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;
Ogni controllo in quel loop esiste perché un file random-access ha meno ridondanza di uno sequenziale. I numeri di segmento devono crescere in modo strict, confrontati come valori unsigned, perché due header che dichiarano lo stesso numero rendono ambiguo quale corpo significhi la lista dei referenziati di una regione successiva. Il campo di lunghezza dati viene letto da handleSegmentDataLength, che mappa qualunque valore col bit alto a 1, incluso il marker di «lunghezza sconosciuta» 0xFFFFFFFF, a -1; nel layout random-access non c'è altro modo per trovare dove inizia il corpo successivo, quindi PDFlibPas rifiuta quella lunghezza subito invece di cercare un marker di fine. Il totale deve combaciare con i byte residui esattamente in entrambe le direzioni, e un solo byte in più dopo l'ultimo corpo fallisce con «trailing random-access data». Quella severità è voluta: in questo layout uno squilibrio di lunghezza significa che ogni corpo dopo il punto dell'errore è spostato, e un decoder che scrolla le spalle davanti a un byte fuori posto non ha modo di sapere se è padding innocuo o il primo sintomo di dati disallineati
Perché l'ultimo segmento end-of-page spariva?
L'ultimo segmento end-of-page spariva perché la prima versione del loop di decodifica teneva il test di terminazione sequenziale, while not reader.isFinished, e nel layout random-access il flusso dei dati finisce prima dell'indice degli header. I segmenti end-of-page (tipo 49) ed end-of-file portano zero byte di dati, e di solito sono gli ultimi header del file. Dopo che l'ultimo corpo di regione è consumato il reader sta esattamente alla fine del buffer, quindi il loop esce e quei segmenti a lunghezza zero non vengono mai dispatchati, lasciando la pagina incompleta. La correzione fa contare gli header al loop random-access anziché i byte. Ogni iterazione sposta il reader al prossimo header indicizzato, riporta bitPointer a 7 perché il corpo precedente può essere finito a metà byte, re-parsa quell'header, poi sposta bytePointer a NextBodyOffset e lo avanza oltre il corpo. I gestori di segmenti esistenti, i controlli sui segmenti referenziati e la diagnostica del Context girano invariati, e un messaggio di errore riporta comunque l'offset in byte originale dell'header, non la posizione del corpo
// TJBIG2StreamDecoder.readSegments, loop principale
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; // riallinea dopo un byte parziale
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // usato nel contesto di errore
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // salta ai dati di questo segmento
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... dispatch al gestore di segmenti esistente, poi seek a DataEnd
end;
Che cosa prova davvero la validazione random-access?
La validazione prova che i byte riorganizzati decodificano negli stessi pixel dei loro originali sequenziali, e prova che l'input random-access malformato fallisce pulito; non prova copertura dei file random-access provenienti da encoder arbitrari. La regression Pascal condivisa usa un file sintetico di 235 byte costruito su una fixture a tabella custom che deve decodificare in una riga di 7 per 1 pixel neri, sia con numero di pagine noto sia con il campo del conteggio rimosso, e poi dà al decoder ogni prefisso troncato di quel file, un numero di segmento duplicato, un byte di coda e una lunghezza dati sconosciuta, asserendo ogni volta che LoadFromByteArray restituisce False e lascia Width e Height a zero. Il caso con immagine vera è un'immagine di refinement a tabella custom di 500 per 473 i cui segmenti sono stati riorganizzati in layout random-access con ogni header originale e ogni byte compresso preservati; il suo SHA-256 combacia esattamente con il baseline sequenziale riesaminato. Quel file è un derivato prodotto da una trasformazione di organizzazione, non un documento random-access naturale trovato in giro, e non era disponibile alcun campione naturale del genere. Le suite sono passate a 1.598 test per Delphi Win32, 42 per la suite immagini Delphi Win64, 48 per FPC Win32 e 46 per FPC Win64, accanto ai tre casi pixel sequenziali esistenti
Caricare un file .jb2 random-access e i suoi limiti
Il codice applicativo non cambia: TPLJBIG2Decoder.LoadFromByteArray rileva da solo l'header del file e l'organizzazione, restituisce False su qualunque input rifiutato con la motivazione in LastError, ed espone la pagina decodificata attraverso Width, Height e GetScanline, che restituisce un byte per pixel
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
// I file standalone sequential e random-access usano la stessa chiamata
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;
I confini valgono la pena dirli chiari. Il supporto random-access è una funzionalità di organizzazione del file, non una API di pagine casuali: TPLJBIG2Decoder restituisce ancora la bitmap della prima pagina, e non c'è nessuna chiamata per pescare la pagina 7 da un file da 40 pagine o per decodificare le pagine in modo lazy. I segmenti con lunghezza dati sconosciuta vengono rifiutati nei file random-access, e i limiti esistenti sulle lunghezze dei prefissi Huffman custom e sui conteggi delle voci di tabella restano invariati. Quei limiti sono abbastanza stretti che un'applicazione Delphi può deviare altrove i casi rifiutati guardando LastError, e il resto della pipeline di immagini, dall'estrazione di immagini PDF alla codifica JBIG2, è coperto sulla pagina di prodotto della PDF library Delphi PDFlibPas