Teknisk artikkel

JBIG2 random-access-filer: å dekode dem i Delphi

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

JBIG2-filorganisasjoner i PDFlibPas: flaggbyten som setFileHeaderFlags leser, velger sequential D.1 med flettede headere, random-access D.2 med hver header foran datablokken, eller embedded D.3, den headerløse formen en JBIG2Decode-strøm bruker med ordbøker i JBIG2Globals
De tre oppstillingene bærer de samme segmentene, men bare random access får en leser til å se hver side- og ordboksavhengighet før den rører en komprimert byte, og det er derfor arkiveringspipelines etterspør den
// 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-forhåndssøk i PDFlibPas: hver segmentheader parses og forkastes mens bare byte-offsetten beholdes, segmentnumre må øke strengt, den ukjente lengden 0xFFFFFFFF avvises, søket stopper ved type 51 end-of-file-header, og deklarerte lengder må nøyaktig ligne de gjenværende bytene
Strengheten er bevisst: i en oppstilling der headere gir det eneste kartet over dataene, betyr én feilplassert byte at hver senere kropp kan være forskjøvet, så en dekoder som tolererer den, kan ikke skille fyllbyte fra feiljustering
// 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

Random-access dekode-løkke i PDFlibPas: hver iterasjon søker til HeaderOffsets for gjeldende header, nullstiller bitPointer til 7 for å angre midt-byte-haler, hopper til NextBodyOffset for kroppen, og teller headere i stedet for byte, så null-lengde end-of-page-segmenter blir dispatchet før løkken slutter
Fordi end-of-page- og end-of-file-segmenter bærer null byte med data, tar datastrømmen slutt før headerindeksen gjør det, og bare en løkke som teller headere kan gi de siste segmentene sin tur
// 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