PDFlibPas v3.539.23 decodeert standalone JBIG2-bestanden die de random-access-organisatie uit ITU-T T.88 Annex D.2 gebruiken, waarbij eerst elke segmentheader komt en de segmentdata daarna in dezelfde volgorde volgt. De native Pascal-decoder in PDFlibJBIG2.pas indexeert de header-offsets tot aan de verplichte end-of-file-header, controleert dat segmentnummers oplopen en dat de opgegeven datalengtes precies optellen tot de bytes die overblijven, en decodeert daarna elke body in header-volgorde zonder de gecomprimeerde data te kopiëren of te herordenen. Vóór deze release gaf hetzelfde bestand al bij het lezen van de header-flags een botte fout "random-access organisation is not supported"
Random-access-JBIG2-bestanden zijn zeldzaam, en dat is precies waarom ze pijn doen wanneer ze wel eens opduiken. Ze komen uit archiveringspipelines en document-imaging-systemen die willen dat een reader eerst elke segmentheader ziet, en daarmee elke pagina- en dictionary-afhankelijkheid, voordat er ook maar één gecomprimeerde byte wordt aangeraakt. Een Delphi-applicatie die gescande archieven batchgewijs naar PDF converteert komt er doorgaans halverwege een job eentje tegen, nadat honderden sequentiële bestanden vlekkeloos doorkwamen, en een decoder die op een goed gevormd bestand stilvalt is nog maar nauwelijks beter dan eentje die rommel rendert. Dezelfde release-lijn had de decoder net JBIG2 custom Huffman-tabellen en canonieke prefixcodes bijgebracht, dus random access was het laatste organisatiegat in de gedocumenteerde capaciteitsgrenzen van de decoder
Wat is de JBIG2 random-access-organisatie?
De random-access-organisatie is één van de drie layouts waarmee T.88 Annex D dezelfde segmenten mag ordenen: sequentieel (D.1) vlecht elke header met zijn data door elkaar, random-access (D.2) zet eerst alle headers en daarna alle data, en embedded (D.3) is de vorm zonder headers die in andere containers zoals PDF gebruikt wordt. Een standalone .jb2-bestand begint met de achtbyte-identifier 97 4A 42 32 0D 0A 1A 0A, gevolgd door één flags-byte en, wanneer het paginanaantal bekend is, een paginanaantal van vier bytes. Bit 0 van de flags-byte kiest de organisatie, met 1 voor sequentieel en 0 voor random-access; bit 1 gezet betekent dat het aantal pagina's onbekend is en het vierbyte-aantal ontbreekt. PDFlibPas leest deze in checkHeader en setFileHeaderFlags, en de gereserveerde bits 2 tot en met 7 worden getolereerd in plaats van geweigerd
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = paginanaantal weggelaten
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF-stream: geen fileheader, embedded organisatie, één pagina
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF zelf draagt deze layout nooit. Een JBIG2Decode-imagestream, beschreven in ISO 32000-1 §7.4.7, bevat alleen de paginasegmenten in embedded-organisatie, met gedeelde symbolictionaries verplaatst naar een aparte JBIG2Globals-stream en zonder fileheader, end-of-page- of end-of-file-segmenten. Wanneer decodeJBIG2 de achtbyte-identifier niet vindt gaat hij precies daarvan uit en dwingt hij sequentiële single-page-decodering af. De native JBIG2-beeldexport gaat de andere kant op en verpakt de PDF-segmenten in een standalone bestand waarvan de flags-byte $03 is, sequentieel met een onbekend paginanaantal, gevolgd door een toegevoegde end-of-file-header. Het random-access-werk raakt dus maar één pad: standalone bestanden die rechtstreeks aan TPLJBIG2Decoder worden meegegeven, doorgaans voordat ze naar PDF worden geconverteerd of hercomprimeerd, de klus die de JBIG2-encoder-backends in PDFlibPas aan de uitvoerkant oppakken
Waarom is een random-access-bestand niet in bestandsvolgorde te lezen?
Een random-access-bestand is niet in bestandsvolgorde te lezen omdat niets in de bytestream markeert waar het headerblok ophoudt en het datablok begint, op de end-of-file-segmentheader zelf na. JBIG2-segmentheaders hebben een variabele lengte: het referred-to-segmentaantal kan een korte vorm van drie bits zijn of een lange vorm met een retention-bitmap, referred-to-segmentnummers nemen één, twee of vier bytes in, afhankelijk van het eigen nummer van het segment, en het page-association-veld is één of vier bytes. Een naïeve sequentiële reader parseert de eerste header, leest zijn datalengte, en behandelt daarna de eerste bytes van de tweede header als de data van dat segment. De decoder merkt pas veel later dat het mis is gegaan, en daarom weigerde de oude code de organisatie ronduit in plaats van het erop te wagen
Hoe indexeert PDFlibPas random-access-segmentheaders?
PDFlibPas indexeert random-access-headers in één voorafscan, IndexRandomHeaders, die elke header parseert, alleen zijn byte-offset vastlegt en stopt bij de eerste end-of-file-header (segmenttype 51). Elke header wordt volledig geparseerd en weer losgelaten, dus de index is een array van integers in plaats van een lijst met objecten, en de voorafscan telt de opgegeven datalengtes onderweg op. Als de scan klaar is zit de reader op de eerste byte van de data van het eerste segment, en die positie wordt NextBodyOffset
// IndexRandomHeaders, lokaal in 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-accumulator
HeaderOffsets[HeaderCount] := Offset; // in stappen vergroot
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;
Elke controle in die lus bestaat omdat een random-access-bestand minder redundantie heeft dan een sequentieel. Segmentnummers moeten strijd oplopen, als unsigned vergeleken, want twee headers die hetzelfde nummer claimen maken het ambigu naar welke body de referred-to-lijst van een latere regio verwijst. Het datalengte-veld wordt gelezen door handleSegmentDataLength, dat elke waarde met de topbit gezet, inclusief de 0xFFFFFFFF-marker voor "unknown length", naar -1 mapt; in random-access-layout is er geen andere manier om te vinden waar de volgende body begint, dus PDFlibPas wijst die lengte meteen af in plaats van naar een eindmarker te zoeken. Het totaal moet in beide richtingen exact met de overblijvende bytes matchen, en één extra byte na de laatste body faalt met "trailing random-access data". Die strengheid is opzettelijk: in deze layout betekent een lengtemismatch dat elke body na het foutpunt verschoven is, en een decoder die over één losse byte heen strijkt heeft geen manier om te weten of het onschadelijke opvulling is of het eerste symptoom van scheef liggende data
Waarom verdween het laatste end-of-page-segment?
Het laatste end-of-page-segment verdween omdat de eerste versie van de decodelus de sequentiële afbreekvoorwaarde hield, while not reader.isFinished, en in random-access-layout raakt de datastroom op vóór de headerindex dat doet. End-of-page- (type 49) en end-of-file-segmenten dragen nul bytes aan data, en het zijn normaal de laatste headers in het bestand. Nadat de laatste region-body verbruikt is zit de reader exact op het einde van de buffer, dus de lus stopt en die segmenten met lengte nul worden nooit doorgeschoven, met een onafgemaakte pagina tot gevolg. De fix zorgt dat de random-access-lus headers telt in plaats van bytes. Elke iteratie springt de reader naar de volgende geïndexeerde header, zet bitPointer terug op 7 omdat de vorige body midden in een byte kan zijn geëindigd, parseert die header opnieuw, en verplaatst daarna bytePointer naar NextBodyOffset en schuift die voorbij de body. De bestaande segmenthandlers, referred-to-controles en de Context-diagnose draaien onveranderd, en een foutmelding rapporteert nog steeds de oorspronkelijke byte-offset van de header, niet de bodypositie
// TJBIG2StreamDecoder.readSegments, hoofdlus
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; // opnieuw uitlijnen na een partiele byte
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // gebruikt in foutcontext
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // spring naar de data van dit segment
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... dispatch naar de bestaande segmenthandler, daarna seek naar DataEnd
end;
Wat bewijst de random-access-validatie eigenlijk?
De validatie bewijst dat de gehergroepeerde bytes naar dezelfde pixels decoderen als hun sequentiële originelen, en dat misvormde random-access-invoer schoon faalt; hij bewijst geen dekking van random-access-bestanden uit willekeurige encoders. De gedeelde Pascal-regressie gebruikt een synthetisch bestand van 235 bytes, gebouwd op een custom-table-fixture die moet decoderen tot een rij zwarte pixels van 7 bij 1, zowel met een bekend paginanaantal als met het count-veld verwijderd, en voert de decoder daarna elk afgekapte prefix van dat bestand, een dubbel segmentnummer, één losse afsluitende byte en een onbekende datalengte, met telkens de assertie dat LoadFromByteArray False teruggeeft en Width en Height op nul laat. Het echte-beeldgeval is een refinement-afbeelding van 500 bij 473 met custom tables waarvan de segmenten naar random-access-layout zijn gehergroepeerd met elke originele header en elke gecomprimeerde byte behouden; zijn SHA-256 matcht exact de gereviewde sequentiële baseline. Dat bestand is een afgeleide geproduceerd door een organisatietransformatie, geen natuurlijk random-access-document dat in het wild is gevonden, en zo'n natuurlijk voorbeeld was niet beschikbaar. De suites slaagden met 1.598 tests voor Delphi Win32, 42 voor de Delphi Win64-imagesuite, 48 voor FPC Win32 en 46 voor FPC Win64, naast de drie bestaande sequentiële pixelgevallen
Een random-access .jb2-bestand laden en zijn grenzen
Applicatiecode verandert niet: TPLJBIG2Decoder.LoadFromByteArray detecteert de fileheader en de organisatie zelf, geeft False terug bij elke geweigerde invoer met de reden in LastError, en stelt de gedecodeerde pagina beschikbaar via Width, Height en GetScanline, dat één byte per pixel teruggeeft
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
// Sequentiële en random-access standalone-bestanden nemen dezelfde aanroep
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;
De grenzen verdienen het ronduit genoemd te worden. Random-access-ondersteuning is een bestandsorganisatiefunctie, geen random page-API: TPLJBIG2Decoder geeft nog steeds de bitmap van de eerste pagina terug, en er is geen aanroep om pagina 7 uit een bestand van 40 pagina's te pakken of pagina's lazy te decoderen. Segmenten met onbekende datalengte worden in random-access-bestanden geweigerd, en de bestaande grenzen aan custom Huffman-prefixlengtes en table-entry-aantallen zijn onveranderd. Die grenzen zijn smal genoeg dat een Delphi-applicatie de geweigerde gevallen via LastError elders kan heen sturen, en de rest van de image-pipeline, van PDF-beeldextractie tot JBIG2-encoding, staat op de productpagina van PDFlibPas Delphi PDF library