PDFlibPas versjon 3.539.23 dekoder frittstående JBIG2-filer som bruker random-access-organisasjonen fra ITU-T T.88 vedlegg D.2, der hver segmentheader kommer først og segmentdataene følger i samme rekkefølge. Den native Pascal-dekoderen i PDFlibJBIG2.pas indekserer header-offsettene frem til den obligatoriske end-of-file-headeren, sjekker at segmentnumrene øker og at de deklarerte datalengdene summerer seg opp til nøyaktig bytene som gjenstår, og dekoder deretter hver kropp i header-rekkefølge uten å kopiere eller omstokke de komprimerte dataene. Før denne utgivelsen reiste samme fil en flat feilmelding «random-access organisation is not supported» i det øyeblikket header-flaggene ble lest
Random-access JBIG2-filer er sjeldne, noe som er nøyaktig grunnen til at de gjør vondt når de dukker opp. De kommer ut av arkiveringspipelines og dokumentavbildningssystemer som vil at en leser skal se hver segmentheader, og dermed hver side- og ordboksavhengighet, før den rører en eneste komprimert byte. En Delphi-applikasjon som batchkonverterer skannede arkiver til PDF, treffer som regel en av disse midt i en jobb, etter at hundrevis av sekvensielle filer har gått rent gjennom, og en dekoder som stopper fullstendig på en velformet fil er bare marginalt bedre enn en som render søppel. Samme utgivelseslinje hadde nettopp lært dekoderen JBIG2 custom Huffman-tabeller og kanoniske prefikskoder, så random access var det siste organisasjonsgapet som sto igjen i dekoderens dokumenterte kapasitetsgrenser
Hva er JBIG2 random-access-organisasjonen?
Random-access-organisasjonen er én av tre måter vedlegg D i T.88 tillater de samme segmentene lagt ut på: sequential (D.1) fletter hver header inn med dataene sine, random-access (D.2) legger alle headere først og alle data etterpå, og embedded (D.3) er den headerløse formen som brukes inne i andre beholdere, som PDF. En frittstående .jb2-fil begynner med den åttabyte identifikatoren 97 4A 42 32 0D 0A 1A 0A, fulgt av én flaggbyte og, når sideantallet er kjent, et sideantall på fire byte. Bit 0 i flaggbyten velger organisasjonen, der 1 betyr sequential og 0 betyr random-access; bit 1 satt betyr at sideantallet er ukjent og firebyte-tallet mangler. PDFlibPas leser disse i checkHeader og setFileHeaderFlags, og de reserverte bitene 2 til 7 tolereres i stedet for å avvises
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = sideantall utelatt
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF-strøm: ingen filheader, embedded-organisasjon, én side
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF selv bærer aldri denne oppstillingen. En JBIG2Decode-bildestrøm, beskrevet i ISO 32000-1 §7.4.7, holder bare sidesegmentene i embedded-organisasjon, med delte symbolordbøker flyttet inn i en egen JBIG2Globals-strøm og uten filheader eller end-of-page- og end-of-file-segmenter. Når decodeJBIG2 ikke finner den åttabyte identifikatoren, antar den nøyaktig det og tvinger frem sequential-dekoding med én side. Den native JBIG2-bildeksporten går den andre veien og pakker PDF-segmentene inn i en frittstående fil hvis flaggbyte er $03, sequential med ukjent sideantall, fulgt av en tillagt end-of-file-header. Så random-access-arbeidet berører bare én sti: frittstående filer som gis rett til TPLJBIG2Decoder, typisk før de konverteres eller rekomprimeres for PDF, jobben som JBIG2-encoder-backendene i PDFlibPas håndterer på utdatasisden
Hvorfor kan en random-access-fil ikke leses i filrekkefølge?
En random-access-fil kan ikke leses i filrekkefølge fordi ingenting i bytestrømmen markerer hvor headerblokken slutter og datablokken begynner, bortsett fra end-of-file-segmentheaderen selv. JBIG2-segmentheadere har variabel lengde: antallet refererte segmenter kan være en trebits kortform eller en langform med et retention-bitmap, refererte segmentnumre tar én, to eller fire byte avhengig av segmentets eget nummer, og page association-feltet er én eller fire byte. En naiv sekvensiell leser parser den første headeren, leser datalengden dens, og behandler så de første bytene av den andre headeren som segmentets data. Dekoderen kan ikke vite at noe har gått galt før mye senere, og det er derfor den gamle koden nektet organisasjonen kontant i stedet for å forsøke den
Hvordan indekserer PDFlibPas random-access-segmentheadere?
PDFlibPas indekserer random-access-headere i ett forhåndssøk, IndexRandomHeaders, som parser hver header, registrerer bare byte-offsetten dens, og stopper ved den første end-of-file-headeren (segmenttype 51). Hver header parses fullt ut og forkastes, så indeksen er en array av heltall snarere enn en liste over objekter, og forhåndssøket akkumulerer de deklarerte datalengdene etter hvert. Når søket er ferdig, står leseren på den første byten av det første segmentets data, og den posisjonen blir NextBodyOffset
// IndexRandomHeaders, lokal i 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; // utvides i biter
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;
Hver sjekk i den løkken finnes fordi en random-access-fil har mindre redundans enn en sekvensiell. Segmentnumre må øke strengt, sammenlignet som usignerte verdier, for to headere som hevder samme nummer gjør det tvetydig hvilken kropp en senere regions referert-liste mener. Datalengdefeltet leses av handleSegmentDataLength, som mapper enhver verdi med toppbiten satt, inkludert markøren 0xFFFFFFFF for «ukjent lengde», til -1; i random-access-oppstilling er det ingen annen måte å finne hvor neste kropp begynner, så PDFlibPas avviser den lengden umiddelbart i stedet for å skanne etter et sluttpunkt. Totalen må matche de gjenværende bytene nøyaktig i begge retninger, og en eneste ekstra byte etter siste kropp feiler med «trailing random-access data». Strengheten er bevisst: i denne oppstillingen betyr et lengdemismatch at hver kropp etter feilpunktet er forskjøvet, og en dekoder som avfeier én feilplassert byte, har ingen måte å vite om den er ufarlig fyllbyte eller det første symptomet på feiljusterte data
Hvorfor forsvant det siste end-of-page-segmentet?
Det siste end-of-page-segmentet forsvant fordi den første versjonen av dekode-løkken beholdt den sekvensielle avslutningstesten, while not reader.isFinished, og i random-access-oppstilling tar datastrømmen slutt før headerindeksen gjør det. End-of-page- (type 49) og end-of-file-segmenter bærer null byte med data, og de er normalt de siste headerne i filen. Etter at siste regionkropp er konsumert, står leseren nøyaktig på slutten av bufferen, så løkken avslutter, og de null-lengde-segmentene blir aldri dispatchet, noe som lar siden uferdig igjen. Fiksen får random-access-løkken til å telle headere i stedet for byte. Hver iterasjon hopper leseren til neste indekserte header, nullstiller bitPointer til 7 fordi forrige kropp kan ha sluttet midt i en byte, reparsérer den headeren, flytter så bytePointer til NextBodyOffset og flytter den forbi kroppen. De eksisterende segmenthåndtererne, sjekkene av refererte segmenter og Context-diagnostikken kjører uendret, og en feilmelding rapporterer fortsatt headerens opprinnelige byte-offset, ikke kroppsposisjonen
// 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; // rejuster etter en delbyte
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // brukes i feilkontekst
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // hopp til dette segmentets data
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... dispatch til eksisterende segmenthåndterer, og søk deretter til DataEnd
end;
Hva beviser random-access-valideringen egentlig?
Valideringen beviser at de omorganiserte bytene dekoder til de samme pikslene som sine sekvensielle originaler, og den beviser at misdannet random-access-input feiler rent; den beviser ikke dekning av random-access-filer fra vilkårlige encodere. Den delte Pascal-regresjonen bruker en syntetisk fil på 235 byte bygget på et custom-table-fixtur som må dekode til en rad på 7 ganger 1 svarte piksler, både med kjent sideantall og med tellingfeltet fjernet, og mater deretter dekoderen hvert avkortet prefiks av den filen, et duplisert segmentnummer, én avsluttende byte og en ukjent datalengde, og påstår hver gang at LoadFromByteArray returnerer False og lar Width og Height stå på null. Tilfellet med ekte bilde er et custom-table refinement-bilde på 500 ganger 473 hvis segmenter ble omorganisert til random-access-oppstilling med hver original header og hver komprimert byte bevart; SHA-256-en matcher den gjennomgåtte sekvensielle baselinjen eksakt. Den filen er en avledning produsert av en organisasjonstransform, ikke et naturlig random-access-dokument funnet i naturen, og intet slikt naturlig utvalg var tilgjengelig. Suitene besto ved 1 598 tester for Delphi Win32, 42 for Delphi Win64-bildesuiten, 48 for FPC Win32 og 46 for FPC Win64, ved siden av de tre eksisterende sekvensielle pikseltilfellene
Å laste en random-access .jb2-fil og grensene dens
Applikasjonskoden endres ikke: TPLJBIG2Decoder.LoadFromByteArray oppdager filheaderen og organisasjonen på egen hånd, returnerer False på enhver avvist input med begrunnelsen i LastError, og eksponerer den dekodede siden gjennom Width, Height og GetScanline, som returnerer én byte per piksel
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
// Sekvensielle og random-access frittstående filer tar samme kall
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;
Grensene er verdt å si rett frem. Random-access-støtte er en filorganisasjonsfunksjon, ikke et tilfeldig side-API: TPLJBIG2Decoder returnerer fortsatt første sides bitmap, og det finnes ingen kall for å plukke side 7 ut av en 40-siders fil eller å dekode sider lat. Segmenter med ukjent datalengde avvises i random-access-filer, og de eksisterende grensene for custom Huffman-prefikslengder og tabelloppslagstall er uendret. De grensene er trange nok til at en Delphi-applikasjon kan rute de avviste tilfellene et annet sted via LastError, og resten av bildepipelinen, fra PDF-bildeekstraksjon til JBIG2-enkoding, er dekket på produktsiden for PDFlibPas Delphi PDF library