PDFlibPas v3.539.23 декодира самостоятелни JBIG2 файлове, ползващи random-access организацията от ITU-T T.88 Annex D.2, при която всеки segment header идва първо, а данните на сегментите следват в същия ред. Нативният Pascal decoder в PDFlibJBIG2.pas индексира отместванията на header-ите до задължителния end-of-file header, проверява, че номерата на сегментите растат и че обявените дължини на данните се събират точно до останалите байтове, и после декодира всяко тяло в реда на header-ите, без да копира или пренарежда компресираните данни. Преди този release същият файл вдигаше плоска грешка „random-access организацията не се поддържа" още при четенето на header флаговете
Random-access JBIG2 файлове са рядкост, което е точно причината да боли, когато все пак се появят. Излизат от архивни pipeline-и и document-imaging системи, които искат четецът да види всеки segment header, а оттам всяка страница и dictionary зависимост, преди да е пипнал един-единствен компресиран байт. Delphi приложение, което пакетно конвертира сканирани архиви в PDF, обикновено се сблъсква с такъв по средата на работа, след стотици sequential файлове, минали чисто, а decoder, който умира на коректен файл, е само мъничко по-добър от един, който рендира боклук. Същата release линия току-що беше научила decoder-а на JBIG2 custom Huffman таблици и канонични prefix кодове, така че random access беше последната организационна дупка в документираните граници на възможностите на decoder-а
Какво е JBIG2 random-access организацията?
Random-access организацията е един от трите начина, по които T.88 Annex D позволява същите сегменти да се подредят: sequential (D.1) преплита всеки header с данните му, random-access (D.2) слага всички header-и най-отпред и цялата след тях, а embedded (D.3) е формата без header, ползвана в други контейнери като PDF. Самостоятелен .jb2 файл започва с осембайтовия идентификатор 97 4A 42 32 0D 0A 1A 0A, следван от един flags байт и, когато броят страници е известен, четирибайтов брой страници. Бит 0 на flags байта избира организацията, като 1 значи sequential, а 0 — random-access; вдигнат бит 1 значи, че броят страници е неизвестен и четирибайтовото число липсва. PDFlibPas ги чете в checkHeader и setFileHeaderFlags, а резервираните битове 2 до 7 се толерират, вместо да се отхвърлят
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = броят страници е пропуснат
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF stream: без file header, embedded организация, една страница
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF сам по себе си никога не носи тази подредба. JBIG2Decode image stream, описан в ISO 32000-1 §7.4.7, държи само page сегментите в embedded организация, като споделените symbol dictionary-та са изнесени в отделен JBIG2Globals stream, без file header, без end-of-page или end-of-file сегменти. Когато decodeJBIG2 не намери осембайтовия идентификатор, допуска точно това и налага sequential, едностранично декодиране. Нативният JBIG2 image export върви в обратната посока и опакова PDF сегментите в самостоятелен файл с flags байт $03 — sequential с неизвестен брой страници — следван от приложен end-of-file header. Така че работата по random access пипа само един път: самостоятелни файлове, подадени директно на TPLJBIG2Decoder, обикновено преди да бъдат конвертирани или прекодирани за PDF — работата, която JBIG2 encoder backend-ите в PDFlibPas вършат на изходната страна
Защо random-access файл не може да се чете в реда на файла?
Random-access файл не може да се чете в реда на файла, защото нищо в байтовия поток не отбелязва къде свършва блокът с header-и и започва блокът с данни — освен самия end-of-file segment header. JBIG2 segment header-ите са с променлива дължина: броят referred-to сегменти може да е трибитова кратка форма или дълга форма с retention bitmap, referred-to номерата заемат един, два или четири байта според собствения номер на сегмента, а полето за page association е с един или четири байта. Наивен sequential четец парсва първия header, чете дължината на данните му и после приема първите байтове на втория header за данни на този сегмент. Decoder-ът не може да разбере, че е сбъркал, чак много по-късно — затова старият код отказваше организацията изцяло, вместо да се опитва
Как PDFlibPas индексира random-access segment header-ите?
PDFlibPas индексира random-access header-ите в едно pre-scan обхождане, IndexRandomHeaders, което парсва всеки header, записва само байтовото му отместване и спира на първия end-of-file header (segment тип 51). Всеки header се парсва изцяло и се изхвърля, така че индексът е масив от числа, а не списък от обекти, а pre-scan-ът натрупва обявените дължини на данните в движение. Когато сканирането свърши, четецът стои на първия байт от данните на първия сегмент, а тази позиция става NextBodyOffset
// IndexRandomHeaders, локално в 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 акумулатор
HeaderOffsets[HeaderCount] := Offset; // расте на парчета
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;
Всяка проверка в този цикъл съществува, защото random-access файл има по-малко резервираност от sequential. Номерата на сегментите трябва строго да растат, сравнявани като unsigned, защото два header-а с един и същ номер правят неясно кое тяло значи referred-to списъкът на по-късен регион. Полето за дължина на данните се чете от handleSegmentDataLength, който мапва всяка стойност с вдигнат горен бит, включително маркера за „неизвестна дължина" 0xFFFFFFFF, на -1; при random-access подредба няма друг начин да се намери къде започва следващото тяло, затова PDFlibPas отхвърля тази дължина веднага, вместо да сканира за краен маркер. Общото трябва да съвпада с останалите байтове и в двете посоки, а един-единствен излишен байт след последното тяло пада с „trailing random-access data". Тази строгост е нарочна: при тази подредба разминаване в дължината значи, че всяко тяло след точката на грешката е изместено, а decoder, който да се отърва от един излишен байт, няма как да знае дали той е безобиден padding или първият симптом на разместени данни
Защо последният end-of-page сегмент изчезваше?
Последният end-of-page сегмент изчезваше, защото първата версия на decode цикъла пазеше sequential теста за край, while not reader.isFinished, а при random-access подредба потокът с данни свършва, преди да свърши индексът с header-ите. End-of-page (тип 49) и end-of-file сегментите носят нула байта данни и обикновено са последните header-и във файла. След като последното региона тяло е изядено, четецът стои точно на края на буфера, цикълът излиза и тези сегменти с нулева дължина никога не се изпращат, оставяйки страницата недовършена. Поправката кара random-access цикъла да брой header-и вместо байтове. Всяка итерация скача с четеца на следващия индексиран header, нулира bitPointer на 7, защото предишното тяло може да е свършило по средата на байт, препарсва този header, после мести bytePointer на NextBodyOffset и го напредва отвъд тялото. Съществуващите segment handler-и, проверките на referred-to сегменти и Context диагностиката работят непроменени, а съобщение за грешка и нататък докладва първоначалното байтово отместване на header-а, не позицията на тялото
// TJBIG2StreamDecoder.readSegments, главен цикъл
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; // подравняване след частичен байт
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // ползва се в error контекста
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // скачане към данните на сегмента
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... изпращане към съществуващия segment handler, после придвижване до DataEnd
end;
Какво всъщност доказва random-access валидацията?
Валидацията доказва, че пренаредените байтове декодират до същите пиксели като своите sequential оригинали, и че зле оформен random-access вход се проваля чисто; тя не доказва покритие на random-access файлове от произволни encoder-и. Споделената Pascal регресия ползва синтетичен файл от 235 байта, изграден върху fixture с custom таблица, който трябва да декодира до ред от 7 на 1 черни пиксела, както с известен брой страници, така и с премахнато полето за брой, и после подава на decoder-а всеки съкратен префикс на файла, дублиран номер на сегмент, един излишен байт накрая и неизвестна дължина на данните, като всеки път изисква LoadFromByteArray да върне False и да остави Width и Height на нула. Случаят с истинско изображение е refinement image 500 на 473 с custom таблица, чиито сегменти са пренаредени в random-access подредба с всеки оригинален header и всеки компресиран байт запазени; неговият SHA-256 съвпада точно с прегледания sequential baseline. Този файл е производен, получен чрез организационна трансформация, а не естествен random-access документ, хванат в дивото, и такава естествена проба не е имало. Suites-ите минаха с 1 598 теста за Delphi Win32, 42 за Delphi Win64 image suite, 48 за FPC Win32 и 46 за FPC Win64, редом с трите съществуващи sequential пикселни случая
Зареждане на random-access .jb2 файл и границите му
Кодът на приложението не се мени: TPLJBIG2Decoder.LoadFromByteArray сам разпознава file header-а и организацията, връща False на всеки отхвърлен вход с причината в LastError, и излага декодираната страница чрез Width, Height и GetScanline, който връща по един байт на пиксел
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
// Sequential и random-access самостоятелни файлове минават през същото извикване
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;
Границите си струва да се кажат направо. Поддръжката на random access е функция за файлова организация, а не random page API: TPLJBIG2Decoder и нататък връща bitmap-а на първата страница, а няма извикване, което да вземе страница 7 от файл с 40 страници или да декодира страниците лениво. Сегментите с неизвестна дължина на данните се отхвърлят в random-access файлове, а съществуващите ограничения върху custom Huffman prefix дължини и брой записи в таблици са непроменени. Тези ограничения са достатъчно тесни, че Delphi приложение може да пренасочи отхвърлените случаи другаде по LastError, а останалата част от image pipeline-а, от извличане на PDF изображения до JBIG2 кодиране, е покрита на продуктовата страница на PDFlibPas Delphi PDF library