PDFlibPas verzija 3.539.23 dekodira samostalne JBIG2 datoteke koje koriste random-access organizaciju iz ITU-T T.88 Annex D.2, u kojoj sva zaglavlja segmenata dođu prva, a podaci segmenata slijede istim redoslijedom. Nativni Pascal dekoder u PDFlibJBIG2.pas indeksira offsete zaglavlja do obaveznog end-of-file zaglavlja, provjerava da brojevi segmenata rastu i da deklarirane duljine podataka zbrojene daju točno preostale bajtove, pa zatim dekodira svako tijelo u redoslijedu zaglavlja bez kopiranja ili preuređivanja komprimiranih podataka. Prije ovog izdanja ista je datoteka podizala bezizlaznu grešku "random-access organisation is not supported" onog trenutka kad bi se pročitali flagovi zaglavlja
Random-access JBIG2 datoteke su rijetke, i upravo zato zabole kad se ipak pojave. Dolaze iz arhivskih cjevovoda i sustava za document imaging koji žele da čitač vidi svako zaglavlje segmenta, a time i svaku ovisnost stranice i rječnika, prije nego dotakne ijedan komprimirani bajt. Delphi aplikacija koja skenirane arhive batch-pretvara u PDF obično naleti na jednu od njih usred posla, nakon što je stotine sekvencijalnih datoteka prošlo uredno, a dekoder koji stane mrtav na dobro oblikovanu datoteku jedva je mrvicu bolji od onog koji renderira smeće. Ista linija izdanja upravo je završila s učenjem dekodera prilagođenih JBIG2 Huffman tablica i kanonskih prefix kodova, pa je random access bila zadnja praznina u organizaciji unutar dokumentiranih ograničenja sposobnosti dekodera
Što je JBIG2 random-access organizacija?
Random-access organizacija jedan je od tri načina na koje T.88 Annex D dopušta da se isti segmenti slažu: sequential (D.1) isprepliće svako zaglavlje s njegovim podacima, random-access (D.2) stavlja sva zaglavlja prva a sve podatke poslije, a embedded (D.3) bezglavi je oblik koji se koristi unutar drugih kontejnera poput PDF-a. Samostalna .jb2 datoteka počinje osmobajtnim identifikatorom 97 4A 42 32 0D 0A 1A 0A, za kojim slijedi jedan flags bajt i, kad je broj stranica poznat, četverobajtni broj stranica. Bit 0 flags bajta bira organizaciju, s 1 koja znači sequential i 0 koja znači random-access; postavljen bit 1 znači da je broj stranica nepoznat i da četverobajtnog brojača nema. PDFlibPas ovo čita u checkHeader i setFileHeaderFlags, a rezervirani bitovi 2 do 7 toleriraju se umjesto da se odbiju
// 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 zaglavlja datoteke, embedded organizacija, jedna stranica
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
Sam PDF nikad ne nosi ovaj raspored. Slika-stream JBIG2Decode, opisan u ISO 32000-1 §7.4.7, drži samo segmente stranice u embedded organizaciji, sa zajedničkim rječnicima simbola preseljenima u zaseban JBIG2Globals stream i bez zaglavlja datoteke, end-of-page ili end-of-file segmenata. Kad decodeJBIG2 ne pronađe osmobajtni identifikator, pretpostavi točno to i forsira sekvencijalno dekodiranje jedne stranice. Nativni JBIG2 izvoz slika ide obrnutim smjerom i umota PDF segmente u samostalnu datoteku čiji je flags bajt $03, sequential s nepoznatim brojem stranica, iza kojeg slijedi dodano end-of-file zaglavlje. Dakle random-access rad dodiruje samo jednu putanju: samostalne datoteke predane izravno klasi TPLJBIG2Decoder, tipično prije nego se pretvore ili rekomprimiraju za PDF, posao o kojem se na izlaznoj strani brinu JBIG2 encoder backendovi u PDFlibPasu
Zašto se random-access datoteka ne može čitati po redu?
Random-access datoteku nije moguće čitati po redu jer ništa u bajt streamu ne označava gdje blok zaglavlja staje, a blok podataka počinje, osim samog end-of-file zaglavlja segmenta. JBIG2 zaglavlja segmenata imaju promjenjivu duljinu: broj referenciranih segmenata može biti trobitna kratka forma ili duga forma s retention bitmapom, referencirani brojevi segmenata uzimaju jedan, dva ili četiri bajta ovisno o vlastitom broju segmenta, a polje page association jedno je ili četiri bajta. Naivan sekvencijalni čitač parsira prvo zaglavlje, pročita njegovu duljinu podataka, pa zatim prve bajtove drugog zaglavlja tretira kao podatke tog segmenta. Dekoder ne može znati da je pošao krago sve do mnogo kasnije, i zato je stari kod organizaciju odbijao odmah, umjesto da je pokuša
Kako PDFlibPas indeksira random-access zaglavlja segmenata?
PDFlibPas indeksira random-access zaglavlja u jednom pre-scanu, IndexRandomHeaders, koji parsira svako zaglavlje, bilježi samo njegov bajtni offset i staje na prvom end-of-file zaglavlju (tip segmenta 51). Svako se zaglavlje do kraja parsira i baci, pa je indeks polje cijelih brojeva, a ne lista objekata, i pre-scan usput zbraja deklarirane duljine podataka. Kad sken 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; // proširuje se 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 provjera u toj petlji postoji jer random-access datoteka ima manje redundancije nego sekvencijalna. Brojevi segmenata moraju strogo rasti, uspoređivani kao nepredznačene vrijednosti, jer dva zaglavlja koja tvrde isti broj čine dvosmislenim koje tijelo referred-to lista kasnije regije misli. Polje duljine podataka čita handleSegmentDataLength, koji svaku vrijednost s postavljenim najvišim bitom, uključujući marker nepoznate duljine 0xFFFFFFFF, preslikava u -1; u random-access rasporedu nema drugog načina da se nađe gdje počinje sljedeće tijelo, pa PDFlibPas tu duljinu odbija odmah, umjesto da traži end marker. Ukupnost se mora u oba smjera točno poklapati s preostalim bajtovima, i jedan jedini višak bajta nakon zadnjeg tijela pada s "trailing random-access data". Ta je strogost namjerna: u ovom rasporedu nepoklapanje duljine znači da je svako tijelo iza točke greške pomaknuto, a dekoder koji preko jednog viška bajta pređe rukom nema načina znati je li to bezopasan padding ili prvi simptom pomaknutih podataka
Zašto je zadnji end-of-page segment nestao?
Zadnji end-of-page segment nestajao je jer je prva verzija decode petlje zadržala sekvencijalni uvjet završetka, while not reader.isFinished, a u random-access rasporedu stream podataka presuši prije nego indeks zaglavlja. End-of-page (tip 49) i end-of-file segmenti nose nula bajtova podataka, i normalno su zadnja zaglavlja u datoteci. Nakon što se potroši tijelo zadnje regije, čitač stoji točno na kraju buffera, pa petlja izađe i ti segmenti nulte duljine nikad se ne isporuče, ostavivši stranicu nedovršenom. Popravak tjera random-access petlju da broji zaglavlja umjesto bajtova. Svaka iteracija skoči s čitačem na sljedeće indeksirano zaglavlje, resetira bitPointer na 7 jer je prethodno tijelo moglo završiti usred bajta, ponovno parsira to zaglavlje, pa pomakne bytePointer na NextBodyOffset i pomakne ga preko tijela. Postojeći handleri segmenata, provjere referenciranih segmenata i Context dijagnostika rade nepromijenjeni, a greška i dalje javlja izvorni bajtni offset zaglavlja, ne poziciju tijela
// TJBIG2StreamDecoder.readSegments, main loop
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 poravnaj nakon djelomičnog bajta
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // ide u kontekst greške
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // skoči na podatke ovog segmenta
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... isporuči postojećem handleru segmenta, pa skoči na DataEnd
end;
Što random-access validacija zapravo dokazuje?
Validacija dokazuje da reorganizirani bajtovi dekodiraju u iste piksele kao njihovi sekvencijalni izvornici, i dokazuje da neispravan random-access ulaz pada čisto; ne dokazuje pokrivenost random-access datoteka od proizvoljnih enkodera. Zajednička Pascal regresija koristi sintetsku datoteku od 235 bajtova izgrađenu na custom-table fiksuri koja se mora dekodirati u red crnih piksela 7 puta 1, i s poznatim brojem stranica i s maknutim poljem brojača, pa zatim dekoderu servira svaki skraćeni prefiks te datoteke, duplicirani broj segmenta, jedan višak bajta i nepoznatu duljinu podataka, svaki put tvrdeći da LoadFromByteArray vrati False i ostavi Width i Height na nuli. Slučaj stvarne slike je custom-table refinement slika 500 puta 473 čiji su segmenti reorganizirani u random-access raspored uz sačuvana sva izvorna zaglavlja i komprimirane bajtove; njezin SHA-256 poklapa se s pregledanim sekvencijalnim baselineom do posljednjeg bajta. Ta je datoteka derivat nastao organizacijskom transformacijom, a ne prirodni random-access dokument nađen u divljini, i takav prirodni uzorak nije bio dostupan. Suiteovi su prošli na 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 piksel slučaja
Učitavanje random-access .jb2 datoteke i njezine granice
Aplikacijski se kod ne mijenja: TPLJBIG2Decoder.LoadFromByteArray sam prepozna zaglavlje datoteke i organizaciju, vraća False na svaki odbijen ulaz s razlogom u LastError, a dekodiranu 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
// Sekvencijalne i random-access samostalne datoteke 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 vrijedi reći ravno. Podrška za random access značajka je organizacije datoteke, a ne API za nasumične stranice: TPLJBIG2Decoder i dalje vraća bitmapu prve stranice, i nema poziva kojim bi se iz datoteke od 40 stranica izvukla stranica 7 ili dekodirale stranice lijenim dohvatom. Segmenti s nepoznatom duljinom podataka odbijaju se u random-access datotekama, a postojeća ograničenja na duljine prilagođenih Huffman prefiksa i brojeve unosa tablica ostaju ista. Ta su ograničenja dovoljno uska da Delphi aplikacija odbijene slučajeve može poslati drugamo preko LastError, a ostatak image cjevovoda, od izvlačenja PDF slika do JBIG2 kodiranja, pokriven je na stranici proizvoda PDFlibPas Delphi PDF library