PDFlibPas verze 3.539.23 dekóduje samostatné JBIG2 soubory, které používají random-access organizaci z ITU-T T.88 Annex D.2, kde nejdřív přijdou všechny hlavičky segmentů a data segmentů následují ve stejném pořadí. Nativní Pascal dekodér v PDFlibJBIG2.pas zaindexuje offsety hlaviček až po povinnou end-of-file hlavičku, zkontroluje, že čísla segmentů rostou a že deklarované délky dat se sečtou přesně na bajty, které zbývají, a pak dekóduje každé tělo v pořadí hlaviček, aniž by kopíroval nebo přeuspořádával komprimovaná data. Před tímto vydáním stejný soubor vyhodil plochou chybu „random-access organisation is not supported“ v momentě, kdy se přečetly příznaky hlavičky
Random-access JBIG2 soubory jsou vzácné, a přesně proto bolí, když se přece jen objeví. Vycházejí z archivních pipeline a dokumentových imaging systémů, které chtějí, aby čtečka viděla každou hlavičku segmentu, a tedy každou závislost stránek a slovníků, než sáhne na jediný komprimovaný bajt. Delphi aplikace, která hromadně konvertuje skenované archivy do PDF, na nějaký takový typicky narazí uprostřed zakázky, poté, co stovky sekvenčních souborů prošly čistě, a dekodér, který se na well-formed souboru zastaví na suchu, je jen o chlup lepší než ten, který renderuje nesmysly. Stejná vývojová řada dekodéru právě předala custom Huffman tabulky JBIG2 a kanonické prefixové kódy, takže random access byla poslední organizační mezera v dokumentovaných limitech schopností dekodéru
Co je random-access organizace JBIG2?
Random-access organizace je jeden ze tří způsobů, jakými T.88 Annex D dovoluje tytéž segmenty rozložit: sequential (D.1) prokládá každou hlavičku jejími daty, random-access (D.2) dá všechny hlavičky dopředu a všechna data za ně a embedded (D.3) je bezhlavičková forma používaná uvnitř jiných kontejnerů, jako je PDF. Samostatný soubor .jb2 začíná osmibajtovým identifikátorem 97 4A 42 32 0D 0A 1A 0A, za ním jde jeden bajt příznaků a když je počet stránek znám, čtyřbajtový počet stránek. Bit 0 bajtu příznaků vybírá organizaci, kde 1 znamená sequential a 0 znamená random-access; nastavený bit 1 znamená, že počet stránek není znám a čtyřbajtový počet chybí. PDFlibPas tohle čte v checkHeader a setFileHeaderFlags a rezervované bity 2 až 7 toleruje místo odmítnutí
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = počet stránek vynechán
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF stream: žádná hlavička souboru, embedded organizace, jedna stránka
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF samo si tenhle layout nikdy neveze. Image stream JBIG2Decode, popsaný v ISO 32000-1 §7.4.7, drží jen page segmenty v embedded organizaci, se sdílenými symbolovými slovníky přesunutými do samostatného streamu JBIG2Globals a bez hlavičky souboru a bez segmentů end-of-page nebo end-of-file. Když decodeJBIG2 nenajde osmibajtový identifikátor, předpokládá přesně tohle a vynutí sekvenční jednostránkové dekódování. Nativní JBIG2 export obrázků jde opačným směrem a zabalí PDF segmenty do samostatného souboru, jehož bajt příznaků je $03, sequential s neznámým počtem stránek, následovaný přilepenou end-of-file hlavičkou. Random-access práce se tedy dotýká jen jediné cesty: samostatných souborů předaných TPLJBIG2Decoder přímo, typicky dřív, než se konvertují nebo rekompimují pro PDF, což na výstupní straně obstarávají JBIG2 encoder backendy v PDFlibPas
Proč se random-access soubor nedá číst v pořadí souboru?
Random-access soubor se nedá číst v pořadí souboru, protože nic v bajtovém proudu neznačí, kde blok hlaviček končí a blok dat začíná, vyjma samotné hlavičky end-of-file segmentu. Hlavičky JBIG2 segmentů mají proměnlivou délku: počet referred-to segmentů může být tříbitová krátká forma nebo dlouhá forma s retention bitmapou, čísla referred-to segmentů berou jeden, dva nebo čtyři bajty podle čísla samotného segmentu a pole page association má jeden nebo čtyři bajty. Naivní sekvenční čtečka rozparsuje první hlavičku, přečte její délku dat a pak vezme první bajty druhé hlavičky jako data toho segmentu. Dekodér se dozví, že se to pokazilo, až mnohem později, a proto starý kód organizaci odmítl rovnou, místo aby se ji pokusil zvládnout
Jak PDFlibPas indexuje hlavičky random-access segmentů?
PDFlibPas indexuje random-access hlavičky jedním pre-scanem IndexRandomHeaders, který rozparsuje každou hlavičku, zaznamená jen její bajtový offset a zastaví se na první end-of-file hlavičce (typ segmentu 51). Každá hlavička se rozparsuje celá a zahodí, takže index je pole integerů místo seznamu objektů a pre-scan průběžně sčítá deklarované délky dat. Když sken doběhne, čtečka sedí na prvním bajtu dat prvního segmentu a ta pozice se stane NextBodyOffset
// IndexRandomHeaders, lokální v 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 akumulátor
HeaderOffsets[HeaderCount] := Offset; // roste po blocích
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;
Každá kontrola v té smyčce existuje proto, že random-access soubor má méně redundance než sekvenční. Čísla segmentů musí přísně růst, porovnávaná jako unsigned hodnoty, protože dvě hlavičky si nárokující stejné číslo dělají nejednoznačným, které tělo referred-to seznam pozdějšího regionu myslí. Pole délky dat čte handleSegmentDataLength, které jakoukoli hodnotu s nastaveným nejvyšším bitem, včetně markeru „unknown length“ 0xFFFFFFFF, mapuje na -1; v random-access layoutu neexistuje jiný způsob, jak najít, kde začíná další tělo, takže PDFlibPas tu délku odmítne okamžitě, místo aby skenovala end marker. Celkové součty se musí v obou směrech přesně schodovat se zbývajícími bajty a jediný navíc bajt po posledním těle selže s „trailing random-access data“. Ta přísnost je záměrná: v tomhle layoutu znamená nesouhlas délek, že každé tělo za bodem chyby je posunuté, a dekodér, který jednoho cizího bajta okecá, nemá jak poznat, jestli je to neškodný padding nebo první symptom špatně zarovnaných dat
Proč se ztratil poslední end-of-page segment?
Poslední end-of-page segment se ztratil proto, že první verze decode smyčky si nechala sekvenční test ukončení while not reader.isFinished a v random-access layoutu dojdou data dřív než index hlaviček. Segmenty end-of-page (typ 49) a end-of-file nesou nula bajtů dat a normálně jsou posledními hlavičkami v souboru. Po spotřebování posledního těla regionu sedí čtečka přesně na konci bufferu, takže smyčka skončí a ty nulové segmenty se už nikdy nedispatchují, čímž stránka zůstane nedokončená. Oprava přepíná random-access smyčku na počítání hlaviček místo bajtů. Každá iterace skočí s čtečkou na další indexovanou hlavičku, resetuje bitPointer na 7, protože předchozí tělo mohlo skončit uprostřed bajtu, znovu rozparsuje tu hlavičku, pak přesune bytePointer na NextBodyOffset a posune ho za tělo. Stávající handlery segmentů, kontroly referred-to segmentů a Context diagnostika běží nezměněné a chybová hláška pořád hlásí původní bajtový offset hlavičky, ne pozici těla
// 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; // znovuzarovnání po částečném bajtu
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // použito v chybovém kontextu
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // skok na data tohoto segmentu
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... dispatch ke stávajícímu handleru segmentu, pak seek na DataEnd
end;
Co random-access validace doopravdy dokazuje?
Validace dokazuje, že přeuspořádané bajty se dekódují na tytéž pixely jako jejich sekvenční originály, a že deformovaný random-access vstup selže čistě; nedokazuje pokrytí random-access souborů od libovolných encodérů. Sdílená Pascal regrese používá 235bajtový syntetický soubor postavený na custom-table fixtuře, který se musí dekódovat na řádek černých pixelů 7 × 1, jak se známým počtem stránek, tak s odstraněným polem počtu, a pak dekodéru podstrčí každý useknutý prefix toho souboru, duplicitní číslo segmentu, jeden trailing bajt a neznámou délku dat, a pokaždé assertuje, že LoadFromByteArray vrátí False a nechá Width a Height na nule. Případ reálného obrázku je refinement obrázek z custom tabulky 500 × 473, jehož segmenty se přeuspořádaly do random-access layoutu se všemi původními hlavičkami a komprimovanými bajty zachovánými; jeho SHA-256 se přesně schoduje s prověřeným sekvenčním baseline. Ten soubor je derivát vyrobený organizační transformací, ne přirozený random-access dokument nalezený ve volné přírodě, a žádný takový přirozený vzorek k dispozici nebyl. Sady prošly na 1 598 testů pro Delphi Win32, 42 pro Delphi Win64 image sadu, 48 pro FPC Win32 a 46 pro FPC Win64, vedle tří existujících sekvenčních pixelových případů
Načtení random-access souboru .jb2 a jeho limity
Aplikační kód se nemění: TPLJBIG2Decoder.LoadFromByteArray detekuje hlavičku souboru a organizaci sám, vrátí False na jakýkoli odmítnutý vstup s důvodem v LastError a vystavuje dekódovanou stránku přes Width, Height a GetScanline, která vrací jeden bajt na 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
// Sekvenční i random-access samostatné soubory vezme stejné volání
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;
Hranice stojí za to vyslovit nahlas. Random-access podpora je vlastnost organizace souboru, ne API pro náhodné stránky: TPLJBIG2Decoder pořád vrací bitmapu první stránky a neexistuje volání, které by vytáhlo stránku 7 ze 40stránkového souboru nebo dekódovalo stránky líně. Segmenty s neznámou délkou dat se v random-access souborech odmítají a stávající limity na délky custom Huffman prefixů a počty položek tabulek zůstávají nezměněné. Ty limity jsou úzké dost na to, aby si Delphi aplikace odmítnuté případy přesměrovala jinam podle LastError, a zbytek image pipeline, od extrakce PDF obrázků po JBIG2 encoding, pokrývá stránka produktu PDFlibPas Delphi PDF library