PDFlibPasin versio 3.539.23 purkaa erillisiä JBIG2-tiedostoja, jotka käyttävät ITU-T T.88:n liitteen D.2 random access -järjestelyä, jossa kaikki segmenttiotsikot tulevat ensin ja segmenttidata seuraa samassa järjestyksessä. Natiivi Pascal-dekooderi tiedostossa PDFlibJBIG2.pas indeksoi otsikoiden siirtymät pakolliseen end-of-file-otsikkoon asti, tarkistaa että segmenttinumerot kasvavat ja että ilmoitetut datapituudet lisääntyvät täsmälleen jäljellä olevien tavujen verran, ja purkaa sitten jokaisen rungon otsikkojärjestyksessä kopioimatta tai uudelleenjärjestämättä pakattua dataa. Ennen tätä julkaisua sama tiedosto nosti tasapaksun virheen ”random-access organisation is not supported” heti kun otsikon liput luettiin
Random access -JBIG2-tiedostot ovat harvinaisia, ja juuri siksi ne satuttavat silloin kun niitä sattuu eteen. Ne tulevat arkistointiputkista ja asiakirjankuvausjärjestelmistä, jotka haluavat lukijan näkevän jokaisen segmenttiotsikon, ja siten jokaisen sivun ja sanakirjariippuvuuden, ennen kuin yhtään pakattua tavua kosketaan. Delphi-sovellus, joka eräkääntää skannattuja arkistoja PDF:ksi, törmää yleensä tällaiseen työn keskellä, satojen sequential-tiedostojen menettyä siististi läpi, ja dekooderi, joka pysähtyy kuoliaaksi hyvin muodostuneen tiedoston kohdalla, on vain marginaalisesti parempi kuin sellainen, joka renderöi roskaa. Sama julkaisulinja oli juuri saanut opetettua dekooderille JBIG2:n omat Huffman-taulukot ja kanoniset etuliitekoodit, joten random access oli viimeinen järjestelyaukko dekooderin dokumentoiduissa kykyrajoissa
Mitä on JBIG2:n random access -järjestely?
Random access -järjestely on yksi kolmesta tavasta, joilla T.88:n liite D sallii samat segmentit asetella: sequential (D.1) lomittaa jokaisen otsikon sen datan kanssa, random access (D.2) laittaa kaikki otsikot ensin ja kaiken datan jälkeen, ja embedded (D.3) on otsikoton muoto, jota käytetään muiden konttien, kuten PDF:n, sisällä. Erillinen .jb2-tiedosto alkaa kahdeksantavuisella tunnisteella 97 4A 42 32 0D 0A 1A 0A, jota seuraa yksi lipputavu ja, kun sivumäärä on tiedossa, nelitavuinen sivumäärä. Lipputavun bitti 0 valitsee järjestelyn, 1 tarkoittaa sequentialia ja 0 random accessia; bitti 1 asetettuna tarkoittaa, että sivumäärä on tuntematon ja nelitavuista lukua ei ole. PDFlibPas lukee nämä funktioissa checkHeader ja setFileHeaderFlags, ja varatut bitit 2–7 siedetään sen sijaan että ne hylättäisiin
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = sivumäärä jätetty pois
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF-stream: ei tiedosto-otsikkoa, embedded-järjestely, yksi sivu
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF itse ei koskaan kanna tätä asettelua. JBIG2Decode-kuva-stream, jonka ISO 32000-1 §7.4.7 kuvaa, pitää sisällään vain sivusegmentit embedded-järjestelyssä, jaetut symbolisanakirjat on siirretty erilliseen JBIG2Globals-streamiin, eikä siinä ole tiedosto-otsikkoa eikä end-of-page- tai end-of-file-segmenttejä. Kun decodeJBIG2 ei löydä kahdeksantavuista tunnistetta, se olettaa täsmälleen tämän ja pakottaa sequential-yksisivuiseen purkuun. Natiivi JBIG2-kuvienvienti menee toiseen suuntaan ja käärii PDF:n segmentit erilliseen tiedostoon, jonka lipputavu on $03, sequential tuntemattomalla sivumäärällä, jota seuraa liitetty end-of-file-otsikko. Random access -työ koskee siis vain yhtä polkua: erilliset tiedostot, jotka annetaan suoraan TPLJBIG2Decoderille, tyypillisesti ennen niiden muuntamista tai uudelleenpakkaamista PDF:ksi — se on työtä, jonka PDFlibPasin JBIG2-enkooderitaustajärjestelmät hoitavat ulostulopuolella
Miksi random access -tiedostoa ei voi lukea tiedostojärjestyksessä?
Random access -tiedostoa ei voi lukea tiedostojärjestyksessä, koska mikään tavuvirrassa ei merkitse sitä, missä otsikkolohko loppuu ja datablokki alkaa, paitsi end-of-file-segmenttiotsikko itse. JBIG2:n segmenttiotsikot ovat muuttuvan pituisia: viitattujen segmenttien määrä voi olla kolmen bitin lyhyt muoto tai pitkä muoto retention-bittikarttoineen, viitattujen segmenttien numerot vievät yhden, kaksi tai neljä tavua segmentin oman numeron mukaan, ja sivukytkentäkenttä on yksi tai neljä tavua. Naiivi sequential-lukija jäsentää ensimmäisen otsikon, lukee sen datapituuden ja käsittelee sitten toisen otsikon ensimmäisiä tavuja kyseisen segmentin datana. Dekooderi ei voi tietää menneensä pieleen ennen paljon myöhemmin, mistä syystä vanha koodi kieltäytyi järjestelystä kokonaan sen yrittämisen sijaan
Miten PDFlibPas indeksoi random access -segmenttiotsikot?
PDFlibPas indeksoi random access -otsikot yhdellä esiskannauksella, IndexRandomHeadersilla, joka jäsentää jokaisen otsikon, kirjaa vain sen tavusiirtymän ja pysähtyy ensimmäiseen end-of-file-otsikkoon (segmenttityyppi 51). Jokainen otsikko jäsennetään kokonaan ja hylätään, joten indeksi on kokonaislukutaulukko eikä objektilista, ja esiskannaus kerryttää ilmoitettuja datapituuksia matkalla. Kun skannaus valmistuu, lukija istuu ensimmäisen segmentin datan ensimmäisellä tavulla, ja siitä sijainnista tulee NextBodyOffset
// IndexRandomHeaders, paikallinen kohteessa 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-kerrytin
HeaderOffsets[HeaderCount] := Offset; // kasvatetaan lohkoittain
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;
Jokainen tarkistus kyseisessä silmukassa on olemassa, koska random access -tiedostossa on vähemmän redundanssia kuin sequential-tiedostossa. Segmenttinumeroiden on kasvettava tiukasti, verrattuna etumerkittöminä arvoina, koska kaksi samaa numeroa vaativaa otsikkoa tekee epäselväksi, mitä runkoa myöhemmän region viitelistalla tarkoitetaan. Datapituuskentän lukee handleSegmentDataLength, joka kuvaa jokaisen ylin bitti asetetun arvon, mukaanlukien 0xFFFFFFFF ”tuntematon pituus” -merkinnän, arvoon -1; random access -asettelussa ei ole muuta tapaa löytää, mistä seuraava runko alkaa, joten PDFlibPas hylkää tuon pituuden välittömästi sen sijaan että skannaisi loppumerkkiä. Summan on täsmättävä jäljellä oleviin tavuihin täsmälleen molempiin suuntiin, ja yksi ylimääräinen tavu viimeisen rungon jälkeen kaatuu virheeseen ”trailing random-access data”. Tiukkuus on tahallista: tässä asettelussa pituusero tarkoittaa, että jokainen virhepisteen jälkeinen runko on siirtynyt, eikä dekooderilla, joka kohauttaa olkapäitään yhdelle harhaiselle tavulle, ole keinoa tietää, onko se vaaraton täyte vai kohdistaantuneen datan ensimmäinen oire
Miksi viimeinen end-of-page-segmentti katosi?
Viimeinen end-of-page-segmentti katosi, koska purkusilmukan ensimmäinen versio piti kiinni sequentialin pääte-ehtotestistä, while not reader.isFinished, ja random access -asettelussa datavirta loppuu ennen otsikkoindeksiä. End-of-page- (tyyppi 49) ja end-of-file-segmentit kantavat nollaa tavua dataa, ja ne ovat yleensä tiedoston viimeiset otsikot. Kun viimeisen region runko on kulutettu, lukija istuu täsmälleen puskurin lopussa, joten silmukka poistuu, eikä niitä nollapituisia segmenttejä koskaan lähetetä käsittelyyn, ja sivu jää kesken. Korjaus tekee random access -silmukasta otsikoiden laskijan tavujen sijaan. Jokainen kierros hyppäättää lukijan seuraavaan indeksoituun otsikkoon, nollaa bitPointerin arvoon 7, koska edellinen runko on voinut päättyä tavun keskelle, jäsentää kyseisen otsikon uudelleen, siirtää sitten bytePointerin NextBodyOffsetiin ja etenee sen ohi runkoon. Olemassa olevat segmenttikäsittelijät, viitattujen segmenttien tarkistukset ja Context-diagnostiikka toimivat muuttumattomina, ja virheilmoitus raportoi edelleen otsikon alkuperäisen tavusiirtymän, ei rungon sijaintia
// 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; // tasataan uudelleen osittaisen tavun jälkeen
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // käytetään virhekontekstissa
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // hyppy tämän segmentin dataan
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... lähetys olemassa olevalle segmenttikäsittelijälle, sitten haku DataEndiin
end;
Mitä random access -validointi oikeastaan todistaa?
Validointi todistaa, että uudelleenjärjestetyt tavut purkautuvat samoiksi pikseleiksi kuin niiden sequential-alkuperäiset, ja se todistaa, että epämuodostunut random access -syöte kaatuu siististi; se ei todista kattavuutta mielivaltaisten enkooderien random access -tiedostoista. Yhteinen Pascal-regressio käyttää 235-tavuista synteettistä tiedostoa, joka on rakennettu custom-taulukko-fixturen päälle ja jonka on purkauduttava 7 × 1 mustan pikselin riviksi, sekä tunnetulla sivumäärällä että määräkenttä poistettuna, ja syöttää sitten dekooderille jokaisen kyseisen tiedoston katkaistun etuliitteen, kaksoissegmenttinumeron, yhden loppuun jäävän tavun ja tuntemattoman datapituuden, vaatien joka kerta, että LoadFromByteArray palauttaa False:n ja jättää Widthin ja Heightin nolliin. Oikean kuvan tapaus on 500 × 473 custom-taulukko-hienosäätökuva, jonka segmentit järjestettiin uudelleen random access -asetteluun siten, että jokainen alkuperäinen otsikko ja pakattu tavu säilytettiin; sen SHA-256 täsmää katselmoituun sequential-perustaan täsmälleen. Kyseinen tiedosto on järjestelymuunnoksen tuottama johdannainen, ei luonnosta löytynyt random access -asiakirja, eikä sellaista luonnollista näytettä ollut saatavilla. Sarjat menivät läpi 1 598 testillä Delphi Win32:lla, 42 testillä Delphi Win64 -kuvasarjassa, 48 testillä FPC Win32:lla ja 46 testillä FPC Win64:llä, kolmen olemassa olevan sequential-pikselitapauksen ohella
Random access .jb2 -tiedoston lataus ja sen rajat
Sovelluskoodi ei muutu: TPLJBIG2Decoder.LoadFromByteArray havaitsee tiedosto-otsikon ja järjestelyn itse, palauttaa False:n mille tahansa hylätylle syötteelle perinteenä LastErrorissä ja paljastaa puretun sivun Widthin, Heightin ja GetScanlinein kautta, jotka palauttavat yhden tavun pikseliä kohti
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- ja random access -erillistiedostot käyttävät samaa kutsua
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;
Rajat kannattaa sanoa suoraan. Random access -tuki on tiedostojärjestelyn ominaisuus, ei satunnaisen sivun API: TPLJBIG2Decoder palauttaa edelleen ensimmäisen sivun bittikartan, eikä ole olemassa kutsua, jolla poimittaisiin sivu 7 40-sivuisesta tiedostosta tai purettaisiin sivuja laiskasti. Tuntemattoman datapituuden segmentit hylätään random access -tiedostoissa, ja olemassa olevat rajat custom Huffman -etuliitepituuksissa ja taulukkomerkintöjen määrissä pysyvät ennallaan. Nuo rajat ovat kapeita sen verran, että Delphi-sovellus voi ohjata hylätyt tapaukset muualle LastErrorin perusteella, ja loput kuvaputkesta, PDF-kuvien poiminnasta JBIG2-enkoodaukseen, käsitellään PDFlibPas Delphi PDF -kirjaston tuotesivulla