PDFlibPas verzija 3.539.23 dekoduje samostalne JBIG2 fajlove koji koriste random-access organizaciju iz ITU-T T.88 Aneksa D.2, gde svaki segment header dolazi prvi, a segment podaci slede istim redosledom. Nativni Pascal dekoder u PDFlibJBIG2.pas indeksira header ofsete do obaveznog end-of-file headera, proverava da se brojevi segmenata povećavaju i da se deklarisane dužine podataka sabiraju tačno u bajtove koji ostaju, a zatim dekoduje svako telo po redosledu headera bez kopiranja ili preuređivanja kompresovanih podataka. Pre ove verzije isti fajl je u trenutku čitanja header flagova podizao suvoparnu grešku „random-access organisation is not supported”
Random-access JBIG2 fajlovi su retki, pa baš zato i zabole kad se pojave. Dolaze iz arhivskih pipeline-ova i dokumenatsko-imaging sistema koji žele da čitač vidi svaki segment header, a samim tim i svaku zavisnost stranice i rečnika, pre nego što dodirne i jedan kompresovani bajt. Delphi aplikacija koja batch-konvertuje skenirane arhive u PDF obično naiđe na jedan takav usred posla, posle stotina sekvencijalnih fajlova koji su prošli čisto, i dekoder koji stane mrtav na dobro formiranom fajlu je tek malo bolji od onog koji renderuje smeće. Ista verzija je upravo završila podučavanje dekodera JBIG2 prilagođenim Huffman tabelama i kanonskim prefiks kodovima, pa je random access bila poslednja organizaciona rupa u dokumentovanim granicama mogućnosti dekodera
Šta je JBIG2 random-access organizacija?
Random-access organizacija je jedan od tri načina na koje T.88 Aneks D dozvoljava da se isti segmenti slože: sequential (D.1) ispliće svaki header sa njegovim podacima, random-access (D.2) stavlja sve headere napred a sve podatke posle, a embedded (D.3) je oblik bez headera koji se koristi unutar drugih kontejnera poput PDF-a. Samostalan .jb2 fajl počinje osmobajtnim identifikatorom 97 4A 42 32 0D 0A 1A 0A, za kojim slede jedan flags bajt i, kad je broj stranica poznat, četvorobajtni broj stranica. Bit 0 flags bajta bira organizaciju, gde 1 znači sequential a 0 random-access; postavljen bit 1 znači da broj stranica nije poznat i da četvorobajtni broj nedostaje. PDFlibPas ovo čita u checkHeader i setFileHeaderFlags, a rezervisani bitovi 2 do 7 se tolerišu umesto da se odbace
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = broj stranica izostavljen
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF stream: nema fajl headera, embedded organizacija, jedna stranica
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
Sam PDF nikada ne nosi ovaj raspored. JBIG2Decode image stream, opisan u ISO 32000-1 §7.4.7, drži samo segmente stranice u embedded organizaciji, sa deljenim simbol rečnicima premeštenim u odvojen JBIG2Globals stream i bez fajl headera, end-of-page ili end-of-file segmenata. Kada decodeJBIG2 ne nađe osmobajtni identifikator, tačno to pretpostavi i forsira sekvencijalno, jednostranično dekodovanje. Nativni JBIG2 image export ide obrnuto i umotava PDF segmente u samostalan fajl čiji je flags bajt $03, sequential sa nepoznatim brojem stranica, uz dodati end-of-file header na kraju. Dakle, random-access rad dodiruje samo jednu putanju: samostalne fajlove koji se direktno predaju TPLJBIG2Decoder-u, tipično pre nego što se konvertuju ili rekompresuju za PDF, što je posao koji JBIG2 encoder backendovi u PDFlibPas-u rade na izlaznoj strani
Zašto se random-access fajl ne može čitati redom kako je upisan?
Random-access fajl se ne može čitati redom kako je upisan jer ništa u bajt streamu ne označava gde blok headera staje a blok podataka počinje, osim samog end-of-file segment headera. JBIG2 segment headeri su promenljive dužine: referred-to segment brojač može biti trobitna kratka forma ili duga forma sa retention bitmapom, referred-to segment brojevi zauzimaju jedan, dva ili četiri bajta zavisno od sopstvenog broja segmenta, a page association polje je jedan ili četiri bajta. Naivan sekvencijalni čitač parsira prvi header, pročita njegovu dužinu podataka, a onda prve bajtove drugog headera tretira kao podatke tog segmenta. Dekoder ne može da zna da je zabrljao dok ne bude mnogo kasnije, što je i razlog što je stari kod organizaciju odbijao iz prve, umesto da je pokuša
Kako PDFlibPas indeksira random-access segment headere?
PDFlibPas indeksira random-access headere u jednom pre-scanu, IndexRandomHeaders, koji parsira svaki header, beleži samo njegov bajt ofset i staje na prvom end-of-file headeru (segment tip 51). Svaki header se potpuno parsira i baca, pa je indeks niz celih brojeva a ne lista objekata, i pre-scan usput sabira deklarisane dužine podataka. Kada skeniranje završi, čitač stoji na prvom bajtu podataka prvog segmenta, i ta pozicija postaje NextBodyOffset
// IndexRandomHeaders, lokalno u 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); // Int64 akumulator
HeaderOffsets[HeaderCount] := Offset; // raste u blokovima
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;
Svaka provera u toj petlji postoji jer random-access fajl ima manje redundancije od sekvencijalnog. Brojevi segmenata moraju strogo da rastu, poređeni kao nepredznačene vrednosti, jer dva headera sa istim brojem čine dvosmislenim koje telo kasnija referred-to lista regiona znači. Polje dužine podataka čita handleSegmentDataLength, koji svaku vrednost sa postavljenim najvišim bitom, uključujući 0xFFFFFFFF marker „nepoznate dužine”, mapira u -1; u random-access rasporedu nema drugog načina da se nađe gde počinje sledeće telo, pa PDFlibPas tu dužinu odbija odmah umesto da traži end marker. Ukupnost se mora poklapati sa preostalim bajtovima tačno u oba smera, i jedan jedini višak bajta posle poslednjeg tela pada sa „trailing random-access data”. Ta strogost je namerna: u ovom rasporedu nepoklapanje dužine znači da je svako telo posle tačke greške pomereno, i dekoder koji pređe preko jednog viška bajta nema načina da zna da li je to bezopasan padding ili prvi simptom toga da su podaci pomereni
Zašto je poslednji end-of-page segment nestao?
Poslednji end-of-page segment je nestao jer je prva verzija decode petlje zadržala sekvencijalni test završetka, while not reader.isFinished, a u random-access rasporedu stream podataka ponestane pre nego što ponestane header indeks. End-of-page (tip 49) i end-of-file segmenti nose nula bajtova podataka, i obično su poslednji headeri u fajlu. Kada se poslednje telo regiona potroši, čitač stoji tačno na kraju bafera, pa petlja izađe i ti segmenti nulte dužine se nikada ne isporuče, ostavljajući stranicu nedovršenom. Popravka čini da random-access petlja broji headere umesto bajtova. Svaka iteracija prebaci čitač na sledeći indeksirani header, resetuje bitPointer na 7 jer se prethodno telo možda završilo usred bajta, ponovo parsira taj header, pa prebaci bytePointer na NextBodyOffset i pomera ga preko tela. Postojeći segment handleri, referred-to provere i Context dijagnostika rade nepromenjeni, a poruka o grešci i dalje prijavljuje originalni bajt ofset headera, a ne poziciju tela
// TJBIG2StreamDecoder.readSegments, glavna petlja
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; // ponovno poravnanje posle parcijalnog bajta
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // koristi se u kontekstu greške
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // skok na podatke ovog segmenta
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... isporuka postojećem segment handleru, pa skok na DataEnd
end;
Šta random-access validacija zapravo dokazuje?
Validacija dokazuje da preuređeni bajtovi dekoduju u iste piksele kao njihovi sekvencijalni originali, i dokazuje da neispravan random-access ulaz pada čisto; ne dokazuje pokrivenost random-access fajlova od proizvoljnih enkodera. Zajednička Pascal regresija koristi sintetički fajl od 235 bajtova izgrađen na custom-table fixture-u koji se mora dekodovati u red crnih piksela 7 puta 1, i sa poznatim brojem stranica i sa uklonjenim poljem broja, a zatim dekoderu servira svaki skraćeni prefiks tog fajla, duplirani broj segmenta, jedan višak bajta i nepoznatu dužinu podataka, tvrdeći svaki put da LoadFromByteArray vraća False i ostavlja Width i Height na nuli. Slučaj sa pravom slikom je refinement slika 500 puta 473 sa custom tabelama čiji su segmenti preuređeni u random-access raspored uz sačuvan svaki originalni header i kompresovani bajt; njen SHA-256 se tačno poklapa sa pregledanim sekvencijalnim baseline-om. Taj fajl je derivat proizveden transformacijom organizacije, a nije prirodni random-access dokument nađen u divljini, i takav prirodni uzorak nije bio dostupan. Suite-ovi su prošli sa 1.598 testova za Delphi Win32, 42 za Delphi Win64 image suite, 48 za FPC Win32 i 46 za FPC Win64, uz tri postojeća sekvencijalna pixel slučaja
Učitavanje random-access .jb2 fajla i njegove granice
Kod aplikacije se ne menja: TPLJBIG2Decoder.LoadFromByteArray sam detektuje fajl header i organizaciju, vraća False na svaki odbijen ulaz sa razlogom u LastError, a dekodovanu stranicu izlaže kroz Width, Height i GetScanline, koji vraća jedan bajt po pikselu
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
// Sekvencijalni i random-access samostalni fajlovi idu preko istog poziva
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;
Granice vredi izreći mirno. Random-access podrška je mogućnost organizacije fajla, a ne random page API: TPLJBIG2Decoder i dalje vraća bitmapu prve stranice, i nema poziva kojim biste izvukli stranicu 7 iz fajla od 40 stranica ili dekodovali stranice lenjo. Segmenti sa nepoznatom dužinom podataka se u random-access fajlovima odbijaju, a postojeća ograničenja na dužine custom Huffman prefiksa i brojeve unosa u tabelama ostaju ista. Ta ograničenja su dovoljno uska da Delphi aplikacija odbijene slučajeve može da usmeri negde drugde preko LastError, a ostatak image pipeline-a, od PDF image ekstrakcije do JBIG2 enkodovanja, pokriven je na stranici proizvoda PDFlibPas Delphi PDF library