Techninis straipsnis

JBIG2 random-access failai: jų dekodavimas Delphi

PDFlibPas 3.539.23 versija dekoduoja atskirus JBIG2 failus, kurie naudoja random-access organizaciją iš ITU-T T.88 D priedo (Annex D.2), kur visos segmento antraštės ateina pirmiausia, o segmento duomenys seka ta pačia tvarka. Natyvus Pascal dekoderis faile PDFlibJBIG2.pas indeksuoja antraščių poslinkius iki privalomos end-of-file antraštės, patikrina, kad segmentų numeriai didėja ir kad deklaruoti duomenų ilgiai sudeda į lygiai tuos baitus, kurie lieka, o tada dekoduoja kiekvieną korpusą antraščių tvarka nekopijuodamas ir neperstatydamas suspaustų duomenų. Iki šios laidos tas pats failas vos perskaičius antraštės vėliavėles metė platų pranešimą „random-access organisation is not supported“

Random-access JBIG2 failai reti – būtent todėl jie ir skauda, kai vis dėlto atsiranda. Jie išeina iš archyvinių konvejerių ir dokumentų skenavimo sistemų, kurios nori, kad skaitytuvas pamatytų kiekvieną segmento antraštę, o kartu kiekvieną puslapio ir žodynų priklausomybę, dar nelietęs nė vieno suspausto baito. Delphi programa, kuri paketais konvertuoja nuskaitytus archyvus į PDF, tokio failo paprastai sutinka darbo viduryje, pro švariai praėjus šimtams nuosekliųjų failų, o dekoderis, kuris sustoja kietai ant gerai suformuoto failo, vos kiek geresnis už tą, kuris atvaizduoja šiukšles. Ta pati laidų linija ką tik baigė išmokyti dekoderį JBIG2 custom Huffman lentelių ir kanoninių prefiksinių kodų, tad random access buvo paskutinė organizacijos spraga dekoderio dokumentuotuose gebėjimo limituose

Kas yra JBIG2 random-access organizacija?

Random-access organizacija yra vienas iš trijų būdų, kuriais T.88 D priedas leidžia išdėstyti tuos pačius segmentus: sequential (D.1) pina kiekvieną antraštę kartu su jos duomenimis, random-access (D.2) deda visas antraštes priekyje ir visus duomenis po jų, o embedded (D.3) yra be antraštės forma, naudojama kituose konteineriuose, tokiuose kaip PDF. Atskiras .jb2 failas prasideda aštuonių baitų identifikatoriumi 97 4A 42 32 0D 0A 1A 0A, po kurio eina vienas vėliavėlių baitas ir, kai puslapių skaičius žinomas, keturių baitų puslapių skaičius. Vėliavėlių baito bitas 0 pasirenka organizaciją, kur 1 reiškia sequential, o 0 – random-access; bitas 1 reiškia, kad puslapių skaičius nežinomas ir keturių baitų skaitiklis nėra. PDFlibPas tai skaito checkHeader ir setFileHeaderFlags, o rezervuoti bitai nuo 2 iki 7 toleruojami, o ne atmetami

JBIG2 failų organizacijos PDFlibPas: vėliavėlių baitas, kurį skaito setFileHeaderFlags, pasirenka sequential D.1 su įpintomis antraštėmis, random-access D.2 su kiekviena antrašte prieš duomenų bloką arba embedded D.3 – be antraštės formą, kurią JBIG2Decode srautas naudoja su žodynais JBIG2Globals
Trys išdėstymai neša tuos pačius segmentus, bet tik random access priverčia skaitytuvą pamatyti kiekvieną puslapio ir žodynų priklausomybę dar nelietus suspausto baito – todėl to ir prašė archyviniai konvejeriai
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1;          // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2;                // 1 = puslapių skaičius praleistas
noOfPagesKnown := pagesKnown = 0;

// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader;                       // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
  // PDF srautas: be failo antraštės, embedded organizacija, vienas puslapis
  noOfPagesKnown := True;
  randomAccessOrganisation := False;
  noOfPages := 1;
end
else
begin
  setFileHeaderFlags;
  if noOfPagesKnown then
    noOfPages := getNoOfPages;
end;

Pats PDF šio išdėstymo niekada neneša. JBIG2Decode paveikslėlio srautas, aprašytas ISO 32000-1 §7.4.7, laiko tik puslapio segmentus embedded organizacijoje, su bendromis simbolių žodynais, perkeltais į atskirą JBIG2Globals srautą, ir be failo antraštės, end-of-page ar end-of-file segmentų. Kai decodeJBIG2 nesuranda aštuonių baitų identifikatoriaus, jis būtent tai ir asumuoja bei priverčia sequential, vieno puslapio dekodavimą. Natyvus JBIG2 paveikslėlio eksportas eina priešinga kryptimi ir apvynioja PDF segmentus į atskirą failą, kurio vėliavėlių baitas yra $03 – sequential su nežinomu puslapių skaičiumi, o pabaigoje prikabinta end-of-file antraštė. Taigi random-access darbas liečia tik vieną kelią: atskirus failus, paduodamus TPLJBIG2Decoder tiesiogiai, paprastai dar prieš juos konvertuojant ar perspaudžiant PDF – tą darbą, kurį išvedimo pusėje atlieka JBIG2 enkoderių backendai PDFlibPas

Kodėl random-access failo negalima skaityti failo tvarka?

Random-access failo negalima skaityti failo tvarka, nes baitų sraute niekas nežymi, kur baigiasi antraščių blokas ir prasideda duomenų blokas, – išskyrus pačią end-of-file segmento antraštę. JBIG2 segmento antraštės yra kintamo ilgio: referred-to segmentų skaitiklis gali būti trijų bitų trumpoji forma arba ilgoji su retention bitmap, referred-to segmentų numeriai užima vieną, du ar keturis baitus, priklausomai nuo paties segmento numerio, o puslapio asociacijos laukas yra vienas ar keturi baitai. Naivus sequential skaitytuvas išanalizuoja pirmąją antraštę, perskaito jos duomenų ilgį ir tada pirmuosius antrosios antraštės baitus laiko to segmento duomenimis. Dekoderis negali pasakyti, kad ėmė klaidinti, iki pat vėliausio momento – todėl senasis kodas organizacijos atsisakydavo iš karto, vietoj to, kad bandytų

Kaip PDFlibPas indeksuoja random-access segmentų antraštes?

PDFlibPas indeksuoja random-access antraštes vienu išankstine peržiūra IndexRandomHeaders, kuri išanalizuoja kiekvieną antraštę, užsirašo tik jos baito poslinkį ir sustoja ties pirmąja end-of-file antrašte (segmento tipas 51). Kiekviena antraštė išanalizuojama iki galo ir išmetama, tad indeksas yra sveikųjų skaičių masyvas, o ne objektų sąrašas, ir išankstinė peržiūra einant kaupia deklaruotus duomenų ilgius. Kai peržiūra baigiasi, skaitytuvas sėdi ant pirmojo pirmojo segmento duomenų baito, ir ta pozicija tampa NextBodyOffset

IndexRandomHeaders išankstinė peržiūra PDFlibPas: kiekviena segmento antraštė išanalizuojama ir išmetama, o laikomas tik jos baito poslinkis, segmentų numeriai turi griežtai didėti, 0xFFFFFFFF nežinomas ilgis atmetamas, peržiūra sustoja ties tipo 51 end-of-file antrašte, o deklaruoti ilgiai turi lygiai sutapti su likusiais baitais
Griežtumas sąmoningas: išdėstyne, kuriame antraštės yra vienintelis duomenų žemėlapis, vienas atsitiktinis baitas reiškia, kad kiekvienas vėlesnis korpusas gali būti pasislinkęs, tad dekoderis, kuris to toleruoja, negali atskirti užpildo nuo nesulygiuotų duomenų
// IndexRandomHeaders, lokali TJBIG2StreamDecoder.readSegments viduje
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 kaupiklis
    HeaderOffsets[HeaderCount] := Offset;      // auginamas gabalais
    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;

Kiekvienas tos kilpos patikrinimas egzistuoja todėl, kad random-access failas turi mažiau pertekliaus nei sequential. Segmentų numeriai turi griežtai didėti, lyginami kaip be ženklo reikšmės, nes dvi antraštės, tvirtinančios tą patį numerį, padaro nevienareikšmiška, kurį korpusą vėlesnės srities referred-to sąrašas turi omeny. Duomenų ilgio lauką skaito handleSegmentDataLength, kuri bet kokią reikšmę su uždėtu vyresniuoju bitu, įskaitant 0xFFFFFFFF „nežinomo ilgio“ žymeklį, susieja su -1; random-access išdėstyne nėra kito būdo rasti, kur prasideda kitas korpusas, tad PDFlibPas tą ilgį atmeta iš karto, vietoj to, kad ieškotų pabaigos žymos. Suma turi tiksliai sutapti su likusiais baitais abiem kryptimis, ir vienas papildomas baitas po paskutinio korpuso žlunga su „trailing random-access data“. Tas griežtumas sąmoningas: šiame išdėstyne ilgio nesutapimas reiškia, kad kiekvienas korpusas po klaidos taško yra pasislinkęs, o dekoderis, kuris nusispjautų vienam atsitiktiniam baitui, neturi kaip sužinoti, ar tai nepavojingas užpildas, ar pirmasis nesulygiuotų duomenų simptomas

Kodėl paskutinė end-of-page segmento antraštė dingo?

Paskutinė end-of-page segmento antraštė dingo todėl, kad pirmoji dekodavimo kilpos versija paliko sequential pabaigos testą while not reader.isFinished, o random-access išdėstyne duomenų srautas pasibaigia anksčiau nei antraščių indeksas. End-of-page (tipas 49) ir end-of-file segmentai neša nulį baitų duomenų, ir jie paprastai yra paskutinės failo antraštės. Suvartojus paskutinės srities korpusą skaitytuvas sėdi lygiai buferio gale, tad kilpa išeina, ir tos nulinio ilgio antraštės niekada neišsiunčiamos apdoroti – puslapis lieka neužbaigtas. Pataisa priverčia random-access kilpą skaičiuoti antraštes, o ne baitus. Kiekvienas ciklas perkelia skaitytuvą prie sekančios indeksuotos antraštės, atstato bitPointer į 7, nes ankstesnis korpusas galėjo baigtis baito viduryje, iš naujo išanalizuoja tą antraštę, tada perkelia bytePointer į NextBodyOffset ir pajuda pro korpusą pirmyn. Egzistuojantys segmentų apdorojimo mechanizmai, referred-to segmentų patikros ir Context diagnostika veikia nepakitę, o klaidos pranešimas vis tiek nurodo antraštės originalų baito poslinkį, o ne korpuso poziciją

Random-access dekodavimo kilpa PDFlibPas: kiekvienas ciklas nukeliauja prie dabartinės antraštės HeaderOffsets, atstato bitPointer į 7, kad panaikintų baito vidurio uodegas, šokinėja į NextBodyOffset korpusui ir skaičiuoja antraštes, o ne baitus, kad nulinio ilgio end-of-page segmentai būtų išsiųsti apdoroti, kol kilpa nesibaigė
Kadangi end-of-page ir end-of-file segmentai neša nulį baitų duomenų, duomenų srautas pasibaigia anksčiau nei antraščių indeksas, ir tik antraštes skaičiuojanti kilpa gali tiems galutiniams segmentams suteikti jų eilę
// TJBIG2StreamDecoder.readSegments, pagrindinė kilpa
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;                    // susilygiuoti po dalinio baito
    Inc(HeaderIndex);
  end;
  SegmentOffset := reader.bytePointer;         // naudojamas klaidos kontekste
  readSegmentHeader(segmentHeader);
  if randomAccessOrganisation then
    reader.bytePointer := NextBodyOffset;      // šuolis į šio segmento duomenis
  DataLength := segmentHeader.getSegmentDataLength;
  DataEnd := reader.bytePointer + DataLength;
  NextBodyOffset := DataEnd;
  // ... perduoti egzistuojančiam segmento apdorojimui, tada persikelti į DataEnd
end;

Ką random-access patikra iš tikrųjų įrodo?

Patikra įrodo, kad pertvarkyti baitai dekoduojasi į tuos pačius pikselius kaip jų sequential originalai, ir įrodo, kad netinkamai suformuota random-access įvestis žlunga švariai; ji neįrodo random-access failų iš atsitiktinių enkoderių aprėpties. Bendroji Pascal regresija naudoja 235 baitų sintetinį failą, pastatytą ant custom-table fiksuoto pavyzdžio, kuris turi dekoduotis į 7 į 1 juodų pikselių eilutę – tiek su žinomu puslapių skaičiumi, tiek su pašalintu skaitiklio lauku, – o tada duoda dekoderiui kiekvieną to failo apkerptą priešdėlį, pasikartojantį segmento numerį, vieną galinį baitą ir nežinomą duomenų ilgį, kiekvienąkart teisdamas, kad LoadFromByteArray grąžina False ir palieka Width bei Height nuliuose. Tikro paveikslėlio atvejis yra 500 į 473 custom-table patikslinimo paveikslėlis, kurio segmentai buvo pertvarkyti į random-access išdėstymą išsaugant kiekvieną originalią antraštę ir suspaustą baitą; jo SHA-256 tiksliai sutampa su peržiūrėtu sequential etalonu. Tas failas yra organizacijos transformacijos pagamintas darinys, o ne gamtoje rastas natūralus random-access dokumentas, ir tokio natūralaus pavyzdžio nebuvo. Rinkiniai praėjo: 1 598 testai Delphi Win32, 42 Delphi Win64 paveikslėlių rinkinyje, 48 FPC Win32 ir 46 FPC Win64, greta trijų egzistuojančių sequential pikselių atvejų

Random-access .jb2 failo įkėlimas ir jo ribos

Programos kodas nesikeičia: TPLJBIG2Decoder.LoadFromByteArray pats atpažįsta failo antraštę ir organizaciją, grąžina False bet kuriai atmestai įvesčiai su priežastimi LastError, o dekoduotą puslapį atiduoda per Width, Height ir GetScanline, kuri grąžina po vieną baitą pikseliui

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 ir random-access atskiri failai eina pro tą patį iškvietimą
    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;

Ribas verta išsakyti atvirai. Random-access palaikymas yra failo organizacijos funkcija, o ne atsitiktinių puslapių API: TPLJBIG2Decoder vis dar grąžina pirmojo puslapio bitmap, ir nėra iškvietimo, kuris iš 40 puslapių failo parinktų 7 puslapį ar dekoduotų puslapius tingiai. Segmentai su nežinomu duomenų ilgiu random-access failuose atmetami, o egzistuojantys limitai custom Huffman prefikso ilgiams ir lentelės įrašų skaičiams nesikeičia. Tie limitai pakankamai siauri, kad Delphi programa atmestuosius atvejus galėtų nukreipti kitur pagal LastError, o likusi paveikslėlio konvejerio dalis – nuo PDF paveikslėlių ištraukimo iki JBIG2 kodavimo – aprašyta PDFlibPas Delphi PDF bibliotekos produkto puslapyje