PDFlibPas versiunea 3.539.23 decodează fișierele JBIG2 standalone care folosesc organizația random-access din ITU-T T.88 Anexa D.2, în care toate header-ele de segment vin primele, iar datele de segment urmează în aceeași ordine. Decoder-ul Pascal nativ din PDFlibJBIG2.pas indexează offset-urile de header până la header-ul obligatoriu end-of-file, verifică faptul că numerele de segment cresc și că lungimile declarate de date se adună exact în octeții rămași, apoi decodează fiecare corp în ordinea header-elor, fără să copieze sau să reordoneze datele comprimate. Înainte de această versiune, același fișier ridica o eroare plată de genul „organizația random-access nu este suportată” imediat ce flag-urile de header erau citite
Fișierele JBIG2 random-access sunt rare, ceea ce e exact motivul pentru care lovesc când apar. Ele ies din pipeline-uri de arhivare și sisteme de document-imaging care vor ca un cititor să vadă fiecare header de segment, deci fiecare pagină și dependență de dicționar, înainte să atingă un singur octet comprimat. O aplicație Delphi care convertește în lot arhive scanate în PDF întâlnește de regulă unul dintre astea în mijlocul unui job, după ce sute de fișiere secvențiale au trecut curat, iar un decoder care se oprește brusc pe un fișier bine format e doar marginal mai bun decât unul care randează gunoi. Aceeași linie de versiuni tocmai terminase să îi predea decoder-ului tabelele Huffman custom JBIG2 și codurile canonice de prefix, deci random access era ultimul gol de organizare rămas în limitele documentate de capabilitate ale decoder-ului
Ce este organizația JBIG2 random-access?
Organizația random-access este una dintre cele trei moduri în care Anexa D din T.88 permite aranjarea acelorași segmente: secvențial (D.1) împletește fiecare header cu datele lui, random-access (D.2) pune toate header-ele primele și toate datele după, iar embedded (D.3) este forma fără header folosită în interiorul altor containere precum PDF. Un fișier standalone .jb2 începe cu identificatorul de opt octeți 97 4A 42 32 0D 0A 1A 0A, urmat de un octet de flag-uri și, când numărul de pagini e cunoscut, de o numărare de pagini pe patru octeți. Bitul 0 al octetului de flag-uri alege organizația, cu 1 însemnând secvențial și 0 însemnând random-access; bitul 1 setat înseamnă că numărul de pagini e necunoscut și numărarea pe patru octeți lipsește. PDFlibPas le citește în checkHeader și setFileHeaderFlags, iar biții rezervați 2 până la 7 sunt tolerați, nu respinși
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = număr de pagini omis
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// stream PDF: fără header de fișier, organizație embedded, o singură pagină
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF-ul însuși nu cară niciodată acest aranjament. Un stream de imagine JBIG2Decode, descris în ISO 32000-1 §7.4.7, ține doar segmentele de pagină în organizație embedded, cu dicționarele de simboluri partajate mutate într-un stream JBIG2Globals separat și fără header de fișier, segmente end-of-page sau end-of-file. Când decodeJBIG2 nu găsește identificatorul de opt octeți, presupune exact asta și forțează decodare secvențială, cu o singură pagină. Exportul nativ de imagini JBIG2 merge în sens invers și înfășoară segmentele PDF într-un fișier standalone al cărui octet de flag-uri este $03, secvențial cu număr de pagini necunoscut, urmat de un header end-of-file adăugat la coadă. Deci lucrul pe random-access atinge o singură cale: fișierele standalone pasate direct lui TPLJBIG2Decoder, de regulă înainte să fie convertite sau recompresate pentru PDF, job pe care backend-urile encoder JBIG2 din PDFlibPas îl fac pe partea de output
De ce nu poate fi citit un fișier random-access în ordinea fișierului?
Un fișier random-access nu poate fi citit în ordinea fișierului pentru că nimic din byte stream nu marchează unde se oprește blocul de header și unde începe blocul de date, în afara header-ului de segment end-of-file însuși. Header-ele de segment JBIG2 au lungime variabilă: numărul de segmente referred-to poate fi o formă scurtă pe trei biți sau o formă lungă cu o bitmap de retenție, numerele de segment referred-to ocupă unu, doi sau patru octeți în funcție de numărul propriu al segmentului, iar câmpul de asociere de pagină este de unu sau patru octeți. Un cititor secvențial naiv parsează primul header, citește lungimea datelor lui și apoi tratează primii octeți ai celui de-al doilea header ca datele acelui segment. Decoder-ul nu poate spune că a greșit decât mult mai târziu, motiv pentru care vechiul cod refuza organizația din start în loc să încerce
Cum indexează PDFlibPas header-ele de segment random-access?
PDFlibPas indexează header-ele random-access într-un singur pre-scan, IndexRandomHeaders, care parsează fiecare header, înregistrează doar offset-ul lui în octeți și se oprește la primul header end-of-file (tipul de segment 51). Fiecare header e parseat complet și apoi aruncat, deci indexul e un tablou de întregi, nu o listă de obiecte, iar pre-scan-ul acumulează lungimile declarate de date pe măsură ce avansează. Când scanul se termină, cititorul stă pe primul octet al datelor primului segment, iar poziția aceea devine NextBodyOffset
// IndexRandomHeaders, local în 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); // acumulator Int64
HeaderOffsets[HeaderCount] := Offset; // extins în tranșe
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;
Fiecare verificare din bucla aceea există pentru că un fișier random-access are mai puțină redundanță decât unul secvențial. Numerele de segment trebuie să crească strict, comparate ca valori fără semn, pentru că două header-e care pretind același număr fac ambiguu cărui corp îi se referă lista referred-to a unei regiuni ulterioare. Câmpul de lungime de date e citit de handleSegmentDataLength, care maphează orice valoare cu bitul de sus setat, inclusiv markerul de „lungime necunoscută” 0xFFFFFFFF, la -1; în aranjamentul random-access nu există alt mod de a găsi unde începe următorul corp, deci PDFlibPas respinge imediat acea lungime în loc să caute un marker de sfârșit. Totalul trebuie să se potrivească exact cu octeții rămași în ambele direcții, iar un singur octet în plus după ultimul corp eșuează cu „trailing random-access data”. Rigurozitatea aceea e deliberată: în acest aranjament o nepotrivire de lungime înseamnă că fiecare corp de după punctul de eroare e deplasat, iar un decoder care dă din umeri la un octet rătăcit nu are cum să știe dacă e padding inofensiv sau primul simptom al unor date dezaliniate
De ce a dispărut ultimul segment end-of-page?
Ultimul segment end-of-page dispăruse pentru că prima versiune a buclei de decodare păstra testul de terminare secvențial, while not reader.isFinished, iar în aranjamentul random-access stream-ul de date se termină înaintea indexului de header. Segmentele end-of-page (tipul 49) și end-of-file cară zero octeți de date și sunt de regulă ultimele header-e din fișier. După ce corpul ultimei regiuni e consumat, cititorul stă exact la sfârșitul buffer-ului, deci bucla iese și acele segmente de lungime zero nu sunt niciodată dispecerizate, lăsând pagina neterminată. Reparația face ca bucla random-access să numere header-e în loc de octeți. Fiecare iterație sare cu cititorul la următorul header indexat, resetează bitPointer la 7 pentru că corpul anterior s-ar fi putut termina la mijlocul unui octet, re-parsează acel header, apoi mută bytePointer la NextBodyOffset și îl avansează peste corp. Handler-ele de segment existente, verificările de segmente referred-to și diagnosticul Context rulează neschimbate, iar un mesaj de eroare raportează în continuare offset-ul original al header-ului, nu poziția corpului
// TJBIG2StreamDecoder.readSegments, bucla 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; // realiniere după un octet parțial
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // folosit în contextul de eroare
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // salt la datele acestui segment
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... dispecerizare către handler-ul existent de segment, apoi seek la DataEnd
end;
Ce dovedește de fapt validarea random-access?
Validarea dovedește că octeții reorganizați decodează în aceiași pixeli ca originalurile lor secvențiale și că input-ul random-access malformat eșuează curat; nu dovedește acoperire pentru fișiere random-access de la encoder-i arbitrari. Regresia Pascal partajată folosește un fișier sintetic de 235 de octeți construit pe un fixture cu tabelă custom care trebuie să decodifice într-un rând de 7 pe 1 de pixeli negri, atât cu număr de pagini cunoscut, cât și cu câmpul de numărare eliminat, apoi îi dă decoder-ului fiecare prefix trunchiat al fișierului, un număr de segment duplicat, un octet în coadă și o lungime de date necunoscută, afirmând de fiecare dată că LoadFromByteArray întoarce False și lasă Width și Height pe zero. Cazul cu imagine reală e o imagine de rafinare custom-table de 500 pe 473 ale cărei segmente au fost reorganizate în aranjament random-access cu fiecare header original și fiecare octet comprimat păstrate; SHA-256-ul ei se potrivește exact cu baseline-ul secvențial revisat. Acel fișier e un derivat produs de o transformare de organizare, nu un document natural random-access găsit în utilizare reală, și niciun asemenea eșantion natural nu a fost disponibil. Suitele au trecut cu 1.598 de teste pentru Delphi Win32, 42 pentru suita de imagini Delphi Win64, 48 pentru FPC Win32 și 46 pentru FPC Win64, alături de cele trei cazuri secvențiale de pixeli existente
Încărcarea unui fișier .jb2 random-access și limitele lui
Codul de aplicație nu se schimbă: TPLJBIG2Decoder.LoadFromByteArray detectează singur header-ul de fișier și organizația, întoarce False pe orice input respins, cu motivul în LastError, și expune pagina decodată prin Width, Height și GetScanline, care întoarce un octet 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
// Fișierele standalone secvențiale și random-access primesc același apel
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;
Granițele merită spuse pe față. Suportul random-access e o funcționalitate de organizare de fișier, nu un API de pagini random: TPLJBIG2Decoder întoarce în continuare bitmap-ul primei pagini și nu există niciun apel care să aleagă pagina 7 dintr-un fișier de 40 de pagini sau să decodifice pagini leneș. Segmentele cu lungime de date necunoscută sunt respinse în fișierele random-access, iar limitele existente pe lungimile de prefix Huffman custom și pe numărul de intrări de tabelă rămân neschimbate. Limitele astea sunt destul de strânse încât o aplicație Delphi să poată ruta cazurile respinse altundeva prin LastError, iar restul pipeline-ului de imagini, de la extragerea de imagini PDF la encoding JBIG2, e acoperit pe pagina de produs a bibliotecii PDFlibPas pentru Delphi