PDFlibPas version 3.539.23 decoder selvstændige JBIG2-filer, der bruger random-access-organisationen fra ITU-T T.88 Annex D.2, hvor alle segment-headers kommer først, og segmentdataene følger i samme rækkefølge. Den native Pascal-decoder i PDFlibJBIG2.pas indekserer header-offsetsene frem til den obligatoriske end-of-file-header, tjekker, at segmentnumrene stiger, og at de deklarerede datalængder lægger op til præcis de bytes, der er tilbage, og decoder derefter hver body i header-rækkefølge uden at kopiere eller sortere de komprimerede data om. Før denne release rejste samme fil en flad "random-access organisation is not supported"-fejl i det øjeblik, header-flagene blev læst
Random-access JBIG2-filer er sjældne, hvilket præcis er grunden til, at de gør ondt, når de dukker op. De kommer fra arkiveringspipelines og document-imaging-systemer, der vil have en reader til at se alle segment-headers, og dermed alle side- og dictionary-afhængigheder, før der røres ved en eneste komprimeret byte. En Delphi-applikation, der batch-konverterer scannede arkiver til PDF, møder typisk en af dem midt i et job, efter hundredvis af sekventielle filer er gået rent igennem, og en decoder, der stopper stenhårdt på en velformet fil, er kun marginalt bedre end en, der renderer skrald. Samme release-linje havde netop lært decoderen JBIG2 custom Huffman-tabeller og canoniske prefix-koder, så random access var det sidste organisationshul tilbage i decoderens dokumenterede kapabilitetsgrænser
Hvad er JBIG2 random-access-organisationen?
Random-access-organisationen er én af tre måder, T.88 Annex D tillader de samme segmenter lagt ud på: sequential (D.1) fletter hver header sammen med sine data, random-access (D.2) lægger alle headers først og alle data bagefter, og embedded (D.3) er den headerløse form, der bruges inde i andre containere som PDF. En selvstændig .jb2-fil starter med den otte-byte identifier 97 4A 42 32 0D 0A 1A 0A, efterfulgt af én flags-byte og, når sideantallet er kendt, et fire-byte sideantal. Bit 0 i flags-byten vælger organisationen, med 1 som sequential og 0 som random-access; bit 1 sat betyder, at sideantallet er ukendt, og at fire-byte-tallet mangler. PDFlibPas læser dem i checkHeader og setFileHeaderFlags, og de reserverede bits 2 til 7 tolereres i stedet for at blive afvist
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = sideantal udeladt
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF-stream: ingen file header, embedded organisation, én side
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF selv bærer aldrig dette layout. En JBIG2Decode-imagestream, beskrevet i ISO 32000-1 §7.4.7, indeholder kun pagesegmenterne i embedded organisation, med delte symbol-dictionaries flyttet ud i en separat JBIG2Globals-stream og ingen file header- eller end-of-page-/end-of-file-segmenter. Når decodeJBIG2 ikke kan finde den otte-byte identifier, antager den præcis det og tvinger sekventiel, enkeltsides-dekodning. Den native JBIG2-imageeksport går den anden vej og pakker PDF-segmenterne ind i en selvstændig fil, hvis flags-byte er $03, sequential med ukendt sideantal, efterfulgt af en tilføjet end-of-file-header. Random-access-arbejdet rører derfor kun én sti: selvstændige filer, der gives direkte til TPLJBIG2Decoder, typisk før de konverteres eller rekomprimeres til PDF, det job, som JBIG2 encoder-backends i PDFlibPas varetager på outputsiden
Hvorfor kan en random-access-fil ikke læses i filrækkefølge?
En random-access-fil kan ikke læses i filrækkefølge, fordi intet i bytestreamen markerer, hvor headerblokken stopper, og datablokken begynder, bortset fra end-of-file-segment-headeren selv. JBIG2-segment-headers har variabel længde: referred-to segment-antallet kan være en tre-bit kort form eller en lang form med en retention-bitmap, referred-to segmentnumre tager én, to eller fire bytes afhængigt af segmentets eget nummer, og page association-feltet er én eller fire bytes. En naiv sekventiel reader parser den første header, læser dens datalængde og behandler derefter de første bytes af den anden header som det segments data. Decoderen kan ikke se, at den er kørt af sporet, før længe efter, og det er derfor, den gamle kode afviste organisationen blankt i stedet for at forsøge sig
Hvordan indekserer PDFlibPas random-access-segment-headers?
PDFlibPas indekserer random-access-headers i én pre-scan, IndexRandomHeaders, der parser hver header, kun registrerer dens byte-offset og stopper ved den første end-of-file-header (segmenttype 51). Hver header parses fuldt ud og smides væk, så indekset er et array af integers snarere end en liste af objekter, og pre-scannen akkumulerer de deklarerede datalængder undervejs. Når scannen slutter, sidder readeren på den første byte af det første segments data, og den position bliver NextBodyOffset
// IndexRandomHeaders, lokal til 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-akkumulator
HeaderOffsets[HeaderCount] := Offset; // vokser i bidder
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;
Hvert tjek i den løkke findes, fordi en random-access-fil har mindre redundans end en sekventiel. Segmentnumre skal strengt talt stige, sammenlignet som unsigned værdier, for to headers, der påstår det samme nummer, gør det tvetydigt, hvilken body en senere regions referred-to-liste mener. Datalængdefeltet læses af handleSegmentDataLength, som mapper enhver værdi med den øverste bit sat, inklusive 0xFFFFFFFF-"unknown length"-markøren, til -1; i random-access-layout er der ingen anden måde at finde ud af, hvor den næste body begynder, så PDFlibPas afviser den længde med det samme i stedet for at scanne efter en end-marker. Totalen skal matche de resterende bytes præcist i begge retninger, og én enkelt ekstra byte efter den sidste body fejler med "trailing random-access data". Strammeheden er bevidst: i dette layout betyder en længde-mismatch, at alle bodies efter fejlpunktet er forskudt, og en decoder, der trækker på skuldrene af én løs byte, har ingen måde at vide, om det er harmløs padding eller det første symptom på forskudte data
Hvorfor forsvandt det sidste end-of-page-segment?
Det sidste end-of-page-segment forsvandt, fordi den første version af decode-løkken beholdt den sekventielle afbrydelsestest, while not reader.isFinished, og i random-access-layout løber datastreamen tør, før header-indekset gør det. End-of-page- (type 49) og end-of-file-segmenter bærer nul bytes data, og de er normalt de sidste headers i filen. Efter den sidste regions body er konsumeret, sidder readeren præcis ved bufferens ende, så løkken afsluttes, og de nul-længde-segmenter bliver aldrig dispatchet, hvilket efterlader siden uafsluttet. Fixet gør, at random-access-løkken tæller headers i stedet for bytes. Hver iteration hopper readeren til den næste indekserede header, nulstiller bitPointer til 7, fordi den foregående body kan være endt midt i en byte, re-parser den header og flytter derefter bytePointer til NextBodyOffset og skrider den forbi bodyen. De eksisterende segment-handlers, referred-to segment-tjek og Context-diagnostikken kører uændret, og en fejlmeddelelse rapporterer stadig headerens oprindelige byte-offset, ikke body-positionen
// TJBIG2StreamDecoder.readSegments, hovedløkke
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; // genjuster efter en partiel byte
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // bruges i error context
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // hop til dette segments data
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... dispatch til den eksisterende segment-handler, seek derefter til DataEnd
end;
Hvad beviser random-access-valideringen egentlig?
Valideringen beviser, at de omorganiserede bytes decoder til de samme pixels som deres sekventielle originaler, og den beviser, at misdannet random-access-input fejler rent; den beviser ikke dækning af random-access-filer fra vilkårlige encodere. Det delte Pascal-regressionssæt bruger en syntetisk fil på 235 bytes bygget på et custom-table-fixture, der skal dekode til en række på 7 x 1 sorte pixels, både med et kendt sideantal og med talfeltet fjernet, og fodrer derefter decoderen med hver afkortet præfiks af den fil, et duplikeret segmentnummer, én trailing byte og en ukendt datalængde, og påstår hver gang, at LoadFromByteArray returnerer False og efterlader Width og Height på nul. Det rigtige billede-case er et custom-table refinement-billede på 500 x 473, hvis segmenter blev omorganiseret til random-access-layout med hver original header og komprimeret byte bevaret; dets SHA-256 matcher den gennemgåede sekventielle baseline præcist. Den fil er en afledt produceret af en organisationstransform, ikke et naturligt random-access-dokument fundet i naturen, og intet sådant naturligt eksemplar var tilgængeligt. Suites bestod med 1.598 tests for Delphi Win32, 42 for Delphi Win64-image-suitten, 48 for FPC Win32 og 46 for FPC Win64, sammen med de tre eksisterende sekventielle pixel-cases
Indlæsning af en random-access .jb2-fil og dens grænser
Applikationskoden ændres ikke: TPLJBIG2Decoder.LoadFromByteArray detekterer selv file header og organisation, returnerer False på ethvert afvist input med begrundelsen i LastError og eksponerer den decodede side gennem Width, Height og GetScanline, som returnerer én byte pr. 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
// Sekventielle og random-access selvstændige filer tager samme kald
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;
Grænserne er værd at sige klart. Random-access-support er en filorganisations-feature, ikke et random page-API: TPLJBIG2Decoder returnerer stadig første sides bitmap, og der findes intet kald til at plukke side 7 ud af en fil på 40 sider eller til at decode sider lazily. Segmenter med ukendt datalængde afvises i random-access-filer, og de eksisterende grænser for custom Huffman-præfikslængder og table-entry-antal er uændrede. Grænserne er snævre nok til, at en Delphi-applikation kan dirigere de afviste cases et andet sted hen via LastError, og resten af image-pipelinen, fra PDF-imageekstraktion til JBIG2-encoding, er dækket på produktsiden for PDFlibPas Delphi PDF-biblioteket