A PDFlibPas 3.539.23-as verziója dekódolja azokat az önálló JBIG2 fájlokat, amik az ITU-T T.88 D.2 melléklet szerinti random-access organizációt használják, ahol minden szegmensfejléc elöl van, és a szegmensadat ugyanabban a sorrendben következik. A PDFlibJBIG2.pas-ban lévő natív Pascal dekóder indexeli a fejléc offszeteket a kötelező end-of-file fejlécig, ellenőrzi, hogy a szegmensszámok nőnek-e, és hogy a deklarált adathosszak pontosan a megmaradó bájtokra adódnak-e fel, majd fejlécsorrendben dekódolja az egyes törzseket a tömörített adat másolása vagy átrendezése nélkül. A release előtt ugyanez a fájl lapos „random-access organisation is not supported" hibát dobott, abban a pillanatban, amikor a fejléc flageket kiolvasták
A random-access JBIG2 fájlok ritkák, pontosan ezért fájdalmasak, amikor mégis felbukkannak. Archiváló pipeline-okból és dokumentum-képalkotó rendszerekből jönnek, amik azt akarják, hogy az olvasó minden szegmensfejlécet lásson, vagyis minden oldalt és szótárfüggőséget, mielőtt egyetlen tömörített bájthoz is hozzányúlna. Egy Delphi alkalmazás, ami szkennelt archívumokat kötegelve konvertál PDF-be, jellemzően egy feladat közepén találkozik ilyennel, miután több száz szekvenciális fájl simán átment, és egy dekóder, amelyik egy jól formált fájlon nyomban leáll, csak kicsit jobb annál, amelyik szemetet renderel. Ugyanez a release-vonal épp most fejezte be a dekóder megtanítását a JBIG2 custom Huffman táblákra és kanonikus prefix kódokra, így a random access volt az utolsó organizációs rés a dekóder dokumentált képességkorlátjaiban
Mi az a JBIG2 random-access organizáció?
A random-access organizáció a T.88 D. melléklet által megengedett három elrendezés egyike ugyanarra a szegmenskészletre: a szekvenciális (D.1) minden fejlécet összefűz a saját adatával, a random-access (D.2) az összes fejlécet elöl tartja és az összes adatot utána, az embedded (D.3) pedig a fejléc nélküli forma, amit más konténerekben, például a PDF-ben használnak. Egy önálló .jb2 fájl a nyolcbájtos 97 4A 42 32 0D 0A 1A 0A azonosítóval indul, egy flags bájt következik, és ha az oldalszám ismert, egy négybájtos oldalszám. A flags bájt 0. bitje választja ki az organizációt, az 1 szekvenciálist, a 0 random-accesst jelent; az 1-es bit beállítottsága azt jelenti, hogy az oldalszám ismeretlen, és a négybájtos számláló hiányzik. A PDFlibPas ezeket a checkHeader-ben és a setFileHeaderFlags-ben olvassa, a 2-től 7-ig terjedő rezervált biteket pedig tűri, nem utasítja el
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = az oldalszám elhagyva
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF stream: nincs fájlfejléc, embedded organizáció, egy oldal
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
A PDF maga soha nem hordozza ezt az elrendezést. Egy JBIG2Decode image stream, ahogy az ISO 32000-1 §7.4.7 leírja, csak az oldal szegmenseit tartja embedded organizációban, a megosztott szimbólumszótárak külön JBIG2Globals streambe költöznek, és nincs se fájlfejléc, se end-of-page, se end-of-file szegmens. Amikor a decodeJBIG2 nem találja a nyolcbájtos azonosítót, pontosan ezt feltételezi, és szekvenciális, egyoldalas dekódolást kényszerít. A natív JBIG2 image export fordítva megy, és a PDF szegmenseket egy önálló fájlba csomagolja, aminek a flags bájtja $03, azaz szekvenciális ismeretlen oldalszámmal, majd egy hozzáfűzött end-of-file fejléc következik. A random-access munka tehát csak egyetlen útvonalat érint: azokat az önálló fájlokat, amiket közvetlenül a TPLJBIG2Decoder-nek adnak át, jellemzően még azelőtt, hogy PDF-hez konvertálnák vagy átömörítenék őket — ezt a munkát végzi a kimeneti oldalon a JBIG2 encoder backend a PDFlibPas-ben
Miért nem olvasható fáilsorrendben egy random-access fájl?
Egy random-access fájl nem olvasható fáilsorrendben, mert a bájtfolyamban semmi nem jelzi, hol ér véget a fejlécblokk és hol kezdődik az adatblokk, kivéve magát az end-of-file szegmensfejlécet. A JBIG2 szegmensfejlécek változó hosszúak: a hivatkozott szegmensek száma lehet hárombites rövid forma vagy retention bitképes hosszú forma, a hivatkozott szegmensszámok a szegmens saját száma szerint egy, kettő vagy négy bájtot foglalnak, az oldal asszociációs mezője pedig egy vagy négy bájt. Egy naiv szekvenciális olvasó feldolgozza az első fejlécet, kiolvassa az adathosszát, majd a második fejléc első bájtjait annak a szegmensnek az adatának tekinti. A dekóder csak jóval később veszi észre, hogy elromlott, ezért a régi kód egyenesen elutasította az organizációt ahelyett, hogy megkísérelte volna
Hogyan indexeli a PDFlibPas a random-access szegmensfejléceket?
A PDFlibPas a random-access fejléceket egyetlen elővizsgálatban, az IndexRandomHeaders-ben indexeli, ami minden fejlécet feldolgoz, csak a bájt offszetjét jegyzi fel, és az első end-of-file fejlécnél (51-es szegmenstípus) áll meg. Minden fejlécet teljesen feldolgoznak és eldobnak, így az index egész számok tömbje, nem objektumlista, az elővizsgálat pedig menet közben halmozza a deklarált adathosszakat. Amikor a vizsgálat befejeződik, az olvasó az első szegmens adatának első bájtján ül, és ez a pozíció válik NextBodyOffset-tá
// IndexRandomHeaders, a TJBIG2StreamDecoder.readSegments lokális rutinja
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 akkumulátor
HeaderOffsets[HeaderCount] := Offset; // darabokban nő
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;
Minden ellenőrzés ebben a ciklusban azért létezik, mert egy random-access fájl kevesebb redundanciát hordoz, mint egy szekvenciális. A szegmensszámoknak szigorúan növekedniük kell, előjel nélküli értékként hasonlítva, mert két, ugyanazt a számot jelentő fejléc kétértelművé teszi, hogy egy későbbi régió hivatkozási listája melyik törzset jelenti. Az adathossz mezőt a handleSegmentDataLength olvassa, ami bármelyik, felső bittel rendelkező értéket, benne a 0xFFFFFFFF „ismeretlen hossz" jelzővel, -1-re képez le; random-access elrendezésben nincs más mód megtalálni, hol kezdődik a következő törzs, ezért a PDFlibPas azonnal elutasítja azt a hosszt ahelyett, hogy végjelért pásztázna. Az összegnek mindkét irányban pontosan egyeznie kell a megmaradó bájtokkal, és egyetlen fölösleges bájt az utolsó törzs után „trailing random-access data" hibával bukik meg. Ez a szigorúság szándékos: ebben az elrendezésben egy hosszeltérés azt jelenti, hogy a hibapont utáni minden törzs eltolódott, és egy dekóder, ami egy kóbor bájtot legyint, nem tudhatja, hogy az ártalmatlan padding vagy az elcsúszott adat első tünete
Miért tűnt el az utolsó end-of-page szegmens?
Az utolsó end-of-page szegmens azért tűnt el, mert a dekódolási ciklus első változata megtartotta a szekvenciális leállási feltételt, a while not reader.isFinished-t, random-access elrendezésben pedig az adatfolyam kifogy, mielőtt a fejlécindex kifogyna. Az end-of-page (49-es típus) és end-of-file szegmensek nulla bájt adatot hordoznak, és jellemzően ők az utolsó fejlécek a fájlban. Miután az utolsó régió törzse elfogyott, az olvasó pontosan a buffer végén ül, így a ciklus kilép, és azok a nulla hosszú szegmensek soha nem kerülnek kiosztásra, az oldal befejezetlen marad. A javítás úgy oldja meg, hogy a random-access ciklus fejléceket számol, nem bájtokat. Minden iteráció az olvasót a következő indexelt fejléchez ugrik, a bitPointer-t 7-re állítja vissza, mert az előző törzs bájt közepén ért véget, újrafeldolgozza azt a fejlécet, majd a bytePointer-t a NextBodyOffset-ra teszi, és átlépteti a törzsön. A meglévő szegmenskezelők, a hivatkozott szegmensek ellenőrzései és a Context diagnosztika változatlanul futnak, és egy hibaüzenet továbbra is a fejléc eredeti bájt offszetjét jelenti, nem a törzs pozícióját
// TJBIG2StreamDecoder.readSegments, fő ciklus
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; // újraigazítás részbájt után
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // a hibakontextusban használt
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // ugrás ennek a szegmensnek az adatára
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... kiosztás a meglévő szegmenskezelőnek, majd ugrás a DataEnd-re
end;
Mit bizonyít valójában a random-access validáció?
A validáció bizonyítja, hogy az átrendezett bájtok ugyanazokra a pixelekre dekódolódnak, mint a szekvenciális eredetijük, és azt is, hogy a rosszul formált random-access bemenet tisztán bukik meg; nem bizonyítja viszont a tetszőleges encoderből származó random-access fájlok lefedettségét. A közös Pascal regresszió egy 235 bájtos szintetikus fájlt használ, amit egy custom-table fixture-re építettek, és ami 7-szer 1-es fekete pixelsorra dekódolódik, ismert oldalszámmal és a számláló mező eltávolításával egyaránt, majd a dekódert eteti a fájl minden csonkolt prefixével, egy duplikált szegmensszámmal, egy záró bájttal és egy ismeretlen adathosszal, minden alkalommal azt állítva, hogy a LoadFromByteArray False-t ad vissza, és a Width és a Height nullán marad. A valós képes eset egy 500-szor 473-as custom-table refinement kép, aminek szegmenseit random-access elrendezésbe rendezték át, minden eredeti fejléccel és tömörített bájttal együtt megőrizve; a SHA-256-a pontosan egyezik a felülvizsgált szekvenciális baseline-nal. Az a fájl egy organizáció-transzformációval előállított származék, nem vadon talált természetes random-access dokumentum, és ilyen természetes minta nem állt rendelkezésre. A suite-ok átmentek 1 598 teszttel Delphi Win32-n, 42-vel a Delphi Win64 image suite-ban, 48-cal FPC Win32-n és 46-tal FPC Win64-en, a három meglévő szekvenciális pixel esettel együtt
Egy random-access .jb2 fájl betöltése és korlátai
Az alkalmazáskód nem változik: a TPLJBIG2Decoder.LoadFromByteArray magától felismeri a fájlfejlécet és az organizációt, bármilyen elutasított bemenetre False-t ad vissza az indokkal a LastError-ben, és a dekódolt oldalt a Width, a Height és a GetScanline közvetíti, ami pixelenként egy bájtot ad vissza
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
// Szekvenciális és random-access önálló fájlok ugyanazt a hívást kapják
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;
A határokat érdemes tisztán kimondani. A random-access támogatás egy fájlorganizációs funkció, nem random oldal API: a TPLJBIG2Decoder továbbra is az első oldal bitmapjét adja vissza, és nincs hívás, amivel egy 40 oldalas fájlból ki lehetne választani a 7. oldalt, vagy lustán lehetne dekódolni az oldalakat. Az ismeretlen adathosszú szegmensek elutasításra kerülnek random-access fájlokban, és a custom Huffman prefix hosszakra és táblabejegyzés-számokra vonatkozó meglévő korlátok változatlanok. Ezek a korlátok elég szűkek ahhoz, hogy egy Delphi alkalmazás az elutasított eseteket a LastError alapján máshova irányítsa, az image pipeline többi része pedig, a PDF image kinyeréstől a JBIG2 kódolásig, a PDFlibPas Delphi PDF library termékoldalon találja meg a lefedettségét