Tehnični članak

Datoteke JBIG2 z naključnim dostopom: dekodiranje v Delphiju

PDFlibPas različice 3.539.23 dekodira samostojne datoteke JBIG2, ki uporabljajo organizacijo z naključnim dostopom iz dodatka D.2 ITU-T T.88, kjer vse glave segmentov pridejo na začetek in podatki segmentov sledijo v istem vrstnem redu. Izvorni pascalovski dekoder v PDFlibJBIG2.pas indeksira odmike glav do obvezne glave konca datoteke, preveri, da številke segmentov naraščajo in da deklarirane dolžine podatkov seštejo točno toliko bajtov, kolikor jih ostane, in nato dekodira vsako telo v vrstnem redu glav, brez kopiranja ali prerazporejanja stisnjenih podatkov. Pred to izdajo je ista datoteka v trenutku branja zastavic glave vrgla plosko napako »random-access organisation is not supported«

Datoteke JBIG2 z naključnim dostopom so redke, točno zato pa bolijo, kadar se le pojavijo. Prihajajo iz arhivskih cevovodov in sistemov za slikanje dokumentov, ki želijo, da bralnik vidi vsako glavo segmenta, torej vsako odvisnost strani in slovarja, preden se dotakne enega samega stisnjenega bajta. Aplikacija Delphi, ki paketno pretvarja skenirane arhive v PDF, se eno od teh navadno sreča na sredi posla, potem ko je na stotine zaporednih datotek šlo skozi čisto, dekoder, ki se na dobro oblikovani datoteki trdo ustavi, pa je le malenkostno boljši od tistega, ki upodablja smeti. Isti niz izdaj je pravkar končal poučevanje dekoderja tabel Huffman po meri in kanoničnih kod predpon v JBIG2, naključni dostop pa je bil zadnja vrzel v organizaciji, ki je ostala v dokumentiranih omejitvah zmožnosti dekoderja

Kaj je organizacija JBIG2 z naključnim dostopom?

Organizacija z naključnim dostopom je eden od treh načinov, kako dodatek T.88 D dovoljuje razporediti iste segmente: zaporedna (D.1) prepleta vsako glavo z njenimi podatki, naključni dostop (D.2) da vse glave na začetek in vse podatke nato, vdelana (D.3) pa je oblika brez glave, uporabljena znotraj drugih vsebnikov, kot je PDF. Samostojna datoteka .jb2 se začne z osmibajtnim identifikatorjem 97 4A 42 32 0D 0A 1A 0A, sledi en bajt zastavic in, kadar je število strani znano, štiribajtno število strani. Bit 0 bajta zastavic izbere organizacijo, 1 pomeni zaporedno in 0 naključni dostop; nastavljen bit 1 pomeni, da je število strani neznano in da štiribajtnega števca ni. PDFlibPas to prebere v checkHeader in setFileHeaderFlags, rezervirane bite od 2 do 7 pa prenese, namesto da bi jih zavrnil

Organizacije datotek JBIG2 v PDFlibPas: bajt zastavic, ki ga prebere setFileHeaderFlags, izbere zaporedno D.1 s prepletenimi glavami, naključni dostop D.2 z vsako glavo pred blokom podatkov ali vdelano D.3, obliko brez glave, ki jo tok JBIG2Decode uporablja s slovarji v JBIG2Globals
Vse tri postavitve nosijo iste segmente, le naključni dostop naredi, da bralnik vidi vsako odvisnost strani in slovarja, preden se dotakne stisnjenega bajta, zato so ga zahtevali arhivski cevovodi
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1;          // 0 = naključni dostop (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2;                // 1 = število strani izpuščeno
noOfPagesKnown := pagesKnown = 0;

// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader;                       // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
  // tok PDF: brez glave datoteke, vdelana organizacija, ena stran
  noOfPagesKnown := True;
  randomAccessOrganisation := False;
  noOfPages := 1;
end
else
begin
  setFileHeaderFlags;
  if noOfPagesKnown then
    noOfPages := getNoOfPages;
end;

Sam PDF te postavitve nikoli ne nosi. Slikovni tok JBIG2Decode, opisan v ISO 32000-1 §7.4.7, nosi le segmente strani v vdelani organizaciji, skupni slovarji simbolov pa so preseljeni v poseben tok JBIG2Globals, brez glave datoteke ter segmentov konca strani in konca datoteke. Ko decodeJBIG2 ne najde osmibajtnega identifikatorja, točno to privzame in vsili zaporedno, enostransko dekodiranje. Izvorni izvoz slik JBIG2 gre v nasprotno smer in ovije segmente PDF v samostojno datoteko, katere bajt zastavic je $03, zaporedna z neznanim številom strani, sledi pa pripeta glava konca datoteke. Delo na naključnem dostopu se torej dotakne le ene poti: samostojnih datotek, izročenih neposredno TPLJBIG2Decoder, navadno pred tem, ko so pretvorjene ali znova stisnjene za PDF, kar je delo, ki ga na izhodni strani opravljajo ozadja kodirnika JBIG2 v PDFlibPas

Zakaj datoteke z naključnim dostopom ni mogoče brati v vrstnem redu datoteke?

Datoteke z naključnim dostopom ni mogoče brati v vrstnem redu datoteke, ker nič v bajtnem toku ne označuje, kjer se blok glav ustavi in blok podatkov začne, razen sama glava segmenta konca datoteke. Glave segmentov JBIG2 imajo spremenljivo dolžino: števec navedenih segmentov je lahko tritbitna kratka oblika ali dolga oblika z bitno sliko ohranjanja, številke navedenih segmentov pa zavzamejo en, dva ali štiri bajte, odvisno od lastne številke segmenta, polje povezave strani pa en ali štiri bajte. Naiven zaporedni bralnik razčleni prvo glavo, prebere njeno dolžino podatkov in potem prve bajte druge glave obravnava kot podatke tega segmenta. Dekoder ne more vedeti, da je zašel, šele veliko pozneje, zato je stara koda organizacijo odklonila na mestu, namesto da bi jo poskusila

Kako PDFlibPas indeksira glave segmentov z naključnim dostopom?

PDFlibPas indeksira glave z naključnim dostopom v enem vnaprejšnjem preiskusu, IndexRandomHeaders, ki razčleni vsako glavo, zabeleži le njen bajtni odmik in se ustavi pri prvi glavi konca datoteke (tip segmenta 51). Vsaka glava se razčleni do konca in zavrže, zato je kazalo polje celih števil in ne seznam objektov, vnaprejšnji preiskus pa poteka in sešteva deklarirane dolžine podatkov. Ko se preiskus konča, bralnik stoji na prvem bajtu podatkov prvega segmenta in ta položaj postane NextBodyOffset

Vnaprejšnji preiskus IndexRandomHeaders v PDFlibPas: vsaka glava segmenta se razčleni in zavrže, obdrži pa se le njen bajtni odmik, številke segmentov se morajo strogo povečevati, neznana dolžina 0xFFFFFFFF je zavrnjena, preiskus se ustavi pri glavi konca datoteke tipa 51, deklarirane dolžine pa se morajo točno ujemati s preostalimi bajti
Strogost je namerna: v postavitvi, kjer glave dajejo edini zemljevid podatkov, en sam odvečni bajt pomeni, da je lahko vsako poznejše telo premaknjeno, dekoder, ki to prenese, pa ne more ločiti polnila od zamika podatkov
// IndexRandomHeaders, lokalno v 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 zbiralnik
    HeaderOffsets[HeaderCount] := Offset;      // raste v koščkih
    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;

Vsak preizkus v tej zanki obstaja, ker ima datoteka z naključnim dostopom manj redundance kot zaporedna. Številke segmentov se morajo strogo povečevati, primerjane kot nepredznačene vrednosti, ker dve glavi z isto številko naredita dvoumno, katero telo pomeni seznam navedb poznejšega območja. Polje dolžine podatkov prebere handleSegmentDataLength, ki vsako vrednost z nastavljenim vrhnjim bitom, vključno z markerjem »neznana dolžina« 0xFFFFFFFF, preslika v -1; v postavitvi z naključnim dostopom ni drugega načina, kako najti, kjer se začne naslednje telo, zato PDFlibPas to dolžino takoj zavrne, namesto da bi iskal zaključni marker. Vsota se mora v obeh smereh točno ujemati s preostalimi bajti, en sam odvečni bajt za zadnjim telesom pa odpove z »trailing random-access data«. Ta strogost je namerna: v tej postavitvi nesorazmerna dolžina pomeni, da je vsako telo za točko napake premaknjeno, dekoder, ki odmahuje enega odvečnega bajta, pa nima načina, da bi vedel, ali je to neškodljivo polnilo ali prvi simptom zamaknjenih podatkov

Zakaj je zadnji segment konca strani izginil?

Zadnji segment konca strani je izginil, ker je prva različica dekodirne zanke obdržala zaporedni preizkus zaključka, while not reader.isFinished, v postavitvi z naključnim dostopom pa podatkovni tok poide prej, kot se izprazni kazalo glav. Segmenti konca strani (tip 49) in konca datoteke nosijo nič bajtov podatkov in navadno so zadnje glave v datoteki. Ko je končno telo območja porabljeno, bralnik stoji točno na koncu medpomnilnika, zato se zanka zaključi in ti segmenti ničelne dolžine nikoli ne pridejo na vrsto, stran pa ostane nedokončana. Popravek naredi, da zanka za naključni dostop šteje glave namesto bajtov. Vsaka ponovitev premakne bralnik na naslednjo indeksirano glavo, ponastavi bitPointer na 7, ker se je prejšnje telo lahko končalo na sredini bajta, znova razčleni to glavo, nato premakne bytePointer na NextBodyOffset in ga pomakne čez telo. Obstoječi upravljalniki segmentov, preizkusi navedenih segmentov in diagnostika Context tečejo nespremenjeni, sporočilo o napaki pa še vedno poroča izvirni bajtni odmik glave in ne položaj telesa

Dekodirna zanka z naključnim dostopom v PDFlibPas: vsaka ponovitev poišče HeaderOffsets trenutne glave, ponastavi bitPointer na 7, da razveljavi repe na sredini bajta, skoči na NextBodyOffset po telo in šteje glave namesto bajtov, tako da segmenti konca strani ničelne dolžine pridejo na vrsto, preden se zanka konča
Ker segmenti konca strani in konca datoteke nosijo nič bajtov podatkov, podatkovni tok poide prej, kot se izprazni kazalo glav, le zanka, ki šteje glave, pa lahko tem zaključnim segmentom dade njihov vrst
// TJBIG2StreamDecoder.readSegments, main loop
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;                    // ponovna poravnava po delnem bajtu
    Inc(HeaderIndex);
  end;
  SegmentOffset := reader.bytePointer;         // uporabljeno v kontekstu napake
  readSegmentHeader(segmentHeader);
  if randomAccessOrganisation then
    reader.bytePointer := NextBodyOffset;      // skok na podatke tega segmenta
  DataLength := segmentHeader.getSegmentDataLength;
  DataEnd := reader.bytePointer + DataLength;
  NextBodyOffset := DataEnd;
  // ... izročitev obstoječemu upravljalniku segmentov, nato iskanje do DataEnd
end;

Kaj preverjanje z naključnim dostopom dejansko dokaže?

Preverjanje dokaže, da se preurejeni bajti dekodirajo v iste piksle kot njihovi zaporedni izvirniki, in dokaže, da deformiran vhod z naključnim dostopom odpove čisto; ne dokaže pokritosti datotek z naključnim dostopom od poljubnih kodirnikov. Skupna pascalovska regresija uporablja sintetično datoteko, dolgo 235 bajtov, zgrajeno na vtičnici tabele po meri, ki se mora dekodirati v vrstico črnih pikslov 7 krat 1, tako s znanim številom strani kot z odstranjenim poljem števca, in nato dekoderju podaja vsak okrnjen predpono te datoteke, podvojeno številko segmenta, en zaključni bajt in neznano dolžino podatkov, ter vsakič zahteva, da LoadFromByteArray vrne False in pusti Width ter Height na nič. Primer prave slike je prefinjena slika 500 krat 473 s tabelo po meri, katere segmenti so bili preurejeni v postavitev z naključnim dostopom, pri čemer sta bila ohranjena vsaka izvirna glava in vsak stisnjen bajt; njena SHA-256 se točno ujema s pregledano zaporedno izhodiščno vrednostjo. Ta datoteka je izpeljanka, ustvarjena s preoblikovanjem organizacije, in ne naravni dokument z naključnim dostopom, najden v divjini, takega naravnega vzorca pa ni bilo na voljo. Zbirke so uspele pri 1.598 preizkusih za Delphi Win32, 42 za slikovno zbirko Delphi Win64, 48 za FPC Win32 in 46 za FPC Win64, skupaj s tremi obstoječimi zaporednimi piksnimi primeri

Nalaganje datoteke .jb2 z naključnim dostopom in njene omejitve

Koda aplikacije se ne spremeni: TPLJBIG2Decoder.LoadFromByteArray sam zazna glavo datoteke in organizacijo, vrne False pri vsakem zavrnjenem vhodu z razlogom v LastError in izpostavi dekodirano stran prek Width, Height in GetScanline, ki vrne en bajt na 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
    // Zaporedne in samostojne datoteke z naključnim dostopom vzamejo isti klic
    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;

Meje so vredne odkritega izrekanja. Podpora za naključni dostop je zmožnost organizacije datotek in ne API za naključne strani: TPLJBIG2Decoder še vedno vrne bitno sliko prve strani, klica, ki bi izbral stran 7 iz datoteke s 40 stranmi ali lenobno dekodiral strani, pa ni. Segmenti z neznano dolžino podatkov so v datotekah z naključnim dostopom zavrnjeni, obstoječe omejitve dolžin predpon Huffman po meri in števil vnosov tabel pa so nespremenjene. Te omejitve so dovolj ozke, da lahko aplikacija Delphi zavrnjene primere usmeri drugam po LastError, preostanek slikovnega cevovoda, od izvleče slik PDF do kodiranja JBIG2, pa je pokrit na strani izdelka PDFlibPas Delphi PDF library