Viisikymmensivuinen skannattu sopimus toistaa samaa aakkostoa jokaisella sivulla, mutta JBIG2-koodain, joka rakentaa yhden symbolisanakirjan per kuva, kouluttaa tuon aakkoston uudelleen viisikymmentä erillistä kertaa. HotPDF, natiivi Delphi- ja C++Builder-PDF-komponentti, voi sen sijaan kerätä yhden jaetun symbolisanakirjan koko asiakirjan yli ja ylentää sen yhdeksi asiakirjatason /JBIG2Globals-virraksi, joten jokaisen sivun oma JBIG2-virta vain viittaa symboli-ID:ihin sen sijaan, että tallentaisi oman kopionsa aakkostosta
Tämä artikkeli pysyy tarkoituksella kapeana ja käsittelee vain sitä, miten HotPDF rakentaa tuon sivujen välisen jakamisen sisäisesti — JBIG2-perusteet, CCITT-vertailu ja Lossless- vastaan LossyLevel-kompromissit käsitellään jo artikkelissa rinnakkaisartikkeli natiivista JBIG2-kaksitasoisesta pakkauksesta Delphissä, jonka tämä artikkeli olettaa jo luetuksi
Miksi sivukohtainen JBIG2-pakkaus yhä toistaa saman kustannuksen?
Vastaus on, ettei mikään kanna tilaa kutsujen välillä. Aina kun HotPDF:n koodain rakentaa symbolisanakirjan yhdelle kuvalle, tuo sanakirja rajautuu tuohon yhteen AddImage-kutsuun: muototunnistuskierros alkaa nollasta, jokainen sivun glyfi luokitellaan uudeksi, ja tuloksena syntyvät bittikartat aritmeettisesti koodataan ja tallennetaan tuoreina. Syötä samalle koodaimelle viisikymmentä sivua samaa fonttia, ja se toistaa mielellään koko koulutuskierroksen viisikymmentä kertaa, koska sen näkökulmasta jokainen sivu on toisiinsa liittymätön kuva, joka sattuu näyttämään samalta. Sivukohtainen UseSymbolDictionary voittaa jo selvästi tasaisen yleisalue-koodauksen yhdellä sivulla, mutta se jää selvästi alle sen katon, jonka todellinen monisivuinen skannaus jättäisi pöydälle
Miten HotPDF jakaa yhden symbolisanakirjan sivujen kesken?
Ota AccumulateGlobalsAcrossPages käyttöön THPDFJBIG2Options-oliossa, niin HotPDF pitää yhden symbolisanakirjan hengissä muistissa koko asiakirjan eliniän ajan sen sijaan, että hylkäisi sen jokaisen kuvan jälkeen. Jokaisen seuraavan sivun glyfit tarkistetaan tuota juoksevaa sanakirjaa vasten ennen kuin mitään koodataan uudelleen: jo olemassa oleva muoto käytetään uudelleen sen symboli-ID:llä, ja vain muoto, jota kukaan ei ole vielä nähnyt, lisätään ja koodataan sanakirjaan. Vertailu käyttää uudelleen samaa toleranssilogiikkaa, jota LossyLevel soveltaa yhdellä sivulla — hieman kohiseva skannaus samasta kirjaimesta lasketaan silti osumaksi — joten kerääjä ei hiljaa paisu yhdeksi sanakirjamerkinnäksi per pikselitason vaihtelu samasta glyfistä. Poiminta tapahtuu ensin ja ruokkii tuota vertailua: HotPDF käy läpi jokaisen sivun bittikartan ja poimii yhtenäiset muodot tulvatäytöllä (flood fill) mustia pikseleitä vasten, saman idean mukaisesti kuin mustepilkkujen jäljittäminen käsin, ja juuri nämä poimitut muodot, ei raa'at pikselilohkot, verrataan juoksevaan sanakirjaan
Miten jaettu sanakirja istuu /JBIG2Globals-virran sisällä
Kerätty sanakirja kirjoitetaan yhtenä symbolisanakirjasegmenttinä /JBIG2Globals-virran sisällä, kiinteässä segmenttinumerossa, jotta jokainen sivu voi osoittaa samaan kohteeseen. ISO 32000-1 §7.4.7:n määrittelemän upotetun JBIG2-organisaation sisällä tekstialuesegmentti voi nimetä toisen segmentin symbolilähteekseen segmenttiotsikon viitattu-segmentti-kentän kautta, ja juuri tähän mekanismiin HotPDF nojaa: globals-virta kantaa yhden suuren symbolisanakirjan, ja jokaisen sivun oma JBIG2-virta kutistuu sivutietosegmentiksi plus tekstialuesegmentiksi, jonka viitattu-lista osoittaa takaisin globals-segmenttiin. Se, mikä oli aiemmin itsenäinen bittivirta per sivu, muuttuu lyhyeksi listaksi sijainteja ja symboli-ID:itä, ja jokainen tällä tavalla rakennettu sivu viittaa identtiseen epäsuoraan /JBIG2Globals-olioon sen kopion sijaan. HotPDF:n oma regressiokattavuus tarkistaa juuri tämän: koodaa lyhyt asiakirja, jossa jokaisella sivulla on eri glyfiasettelu, lataa se uudelleen ja laske, kuinka monta erillistä /JBIG2Globals-olioviittausta tiedostossa näkyy — yksi asiakirja, yksi olioviittaus, riippumatta siitä, kuinka moni sivu antoi symboleja siihen
Sivujen välisen symbolisanakirjan keräämisen kytkeminen päälle
Kytkin sijaitsee samassa asetustietueessa, joka käsitellään rinnakkaisartikkelissa, ja se vaatii neljä asetusta olemaan sopusoinnussa keskenään ennen kuin kerääminen todella kytkeytyy päälle
var
Pdf: THotPDF;
Bmp: TBitmap;
PageIdx, ImgIdx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.JBIG2Options.Lossless := True;
Pdf.JBIG2Options.UseSymbolDictionary := True;
Pdf.JBIG2Options.UseGlobalSegments := True;
Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := True; // opt-in, default False
Pdf.JBIG2Options.UseExternalEncoder := False; // accumulation needs the native path
Pdf.JBIG2Options.UseNativeArithmeticFallback := True;
Pdf.BeginDoc;
for PageIdx := 0 to ScannedPages.Count - 1 do
begin
if PageIdx > 0 then
Pdf.AddPage;
Bmp := ScannedPages[PageIdx]; // 1-bit TBitmap for this page
ImgIdx := Pdf.AddImage(Bmp, icJBIG2);
Pdf.CurrentPage.ShowImage(ImgIdx, 0, 0, Bmp.Width, Bmp.Height, 0);
end;
Pdf.EndDoc; // the shared /JBIG2Globals stream is finalized here
finally
Pdf.Free;
end;
end;
Tuo pariutus ei ole valinnainen koriste. Kaksitasoisen pakkauksen artikkelissa kuvattu ulkoisen koodaimen liitäntä — se, jonka rekisteröit funktiolla RegisterJBIG2EncoderBackend tuotantotason pakkaussuhteita varten — on rakennettu kuvakohtaisen koodauksen ympärille, ja HotPDF:n omat keräysdemot ja regressiotestit pariuttavat aina AccumulateGlobalsAcrossPages-arvon kanssa UseExternalEncoder := False. Kohtele tätä kovana vaatimuksena, ei ehdotuksena: sivujen välinen jakaminen on natiivin koodaimen ominaisuus, ja rekisteröity ulkoinen taustajärjestelmä ei yksinkertaisesti kuulu siihen polkuun, joka rakentaa jaetun sanakirjan
Kuinka paljon pienempi monisivuinen skannaus todella on?
Rehellinen vastaus alkaa siitä, mikä ei ensin siirtänyt mittaria. Aiempi julkaisu lisäsi sisällön mukaan osoitetun välimuistin /JBIG2Globals-virroille — haun, jonka avaimena on virran tavujen 64-bittinen FNV-1a-tiiviste, jotta kaksi kuvaa, jotka sattuivat tuottamaan tavulleen identtistä globals-dataa, voisivat jakaa yhden PDF-olion. Todelliseen tulosteeseen verrattuna tuo välimuisti auttoi tuskin lainkaan, koska HotPDF:n olemassa oleva koko kuvan kattava duplikaattien tunnistus oli jo romahduttanut tavulleen identtiset kuvat ennen kuin välimuisti ehti koskaan ajaa. Opetus oli, että virtatason deduplikaatio kannattaa vasta, kun kaksi aidosti erilaista sivukuvaa voi silti jakaa yhden kasvavan sanakirjan, mikä on juuri se, mitä aito sivujen välinen kerääminen tuottaa
Tuota vaikeampaa tapausta varten HotPDF:n oma tekninen arvio asettaa lisäsäästön noin 30–60 prosenttiin pienemmäksi kuin pelkkä virtatason deduplikaatio saavuttaa, tyypilliselle monisivuiselle skannaukselle, joka on rakennettu yhdestä toistuvasta fontista — vaihteluväli liikkuu sen mukaan, kuinka paljon asiakirjan visuaalisesta sanastosta todella toistuu, koska sivu täynnä ainutlaatuisia kaavioita ei anna sanakirjalle mitään uudelleenkäytettävää. Kohtele tätä suunnittelutavoitteena, ei takuuna millekään tietylle syötteelle, ja mittaa omat asiakirjasi sen sijaan, että luottaisit yhteen lukuun. HotPDF:n mukana toimitettava JBIG2Benchmark-demo on olemassa juuri tätä tarkoitusta varten: se koodaa saman monisivuisen skannauksen neljällä eri tavalla ja tulostaa tuloksena syntyvän tiedostokoon jokaiselle asetukselle, joten vertailu ajetaan omaa skannaussekoitustasi vasten synteettisen sijaan
procedure RunScenario(const Title: string; AccumulateGlobals: Boolean);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.JBIG2Options.Lossless := True;
Pdf.JBIG2Options.UseSymbolDictionary := True;
Pdf.JBIG2Options.UseGlobalSegments := True;
Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := AccumulateGlobals;
Pdf.JBIG2Options.UseExternalEncoder := not AccumulateGlobals;
// ... encode the same three-page scan here, then compare file sizes.
finally
Pdf.Free;
end;
end;
begin
RunScenario('Per-image lossless baseline', False);
RunScenario('Cross-page accumulated globals', True);
end.
Missä sivujen välinen kerääminen kohtaa rajansa
Kerätty sanakirja on rajattu 4096 symboliin, samaan kattoon, jonka kuvakohtainen natiivi koodain jo pakottaa yhdellä sivulla. Ylitä tuo raja kesken asiakirjan, ja HotPDF ei nosta poikkeusta tai keskeytä ajoa: kerääjä hylkää uuden glyfin, ja sivu, joka toi sen mukanaan, palautuu automaattisesti riippumattomaan kuvakohtaiseen koodaukseen, joten asiakirja tulee silti oikein ulos — menetät vain sivujen välisen säästön niiltä sivuilta, jotka ylittivät katon. Toinen suoja valvoo kokonaiskokoa symbolimäärän sijaan: heti kun kerätyn sanakirjan yhdistetty symbolileveys ylittää 131071 pikseliä, HotPDF vuodattaa nykyisen erän levylle ja aloittaa uuden globals-ryhmän automaattisesti sen sijaan, että antaisi yhden muistinvaraisen rakenteen kasvaa rajattomasti. Kumpikaan raja ei vaadi mitään koodia puoleltasi, koska molemmat ovat automaattisia varakeinoja, ei poikkeuksia, jotka sinun pitäisi napata
PDF/A-yhdenmukaisuus on ainoa asetus, joka sammuttaa koko mekanismin sen sijaan, että vain rajaisi sitä. HotPDF korvaa hiljaa JBIG2:n CCITT Group 4:llä heti, kun PDFACompliance on epätyhjä, jokaisella sivulla, riippumatta AccumulateGlobalsAcrossPages-arvosta tai mistään muusta JBIG2Options-oliossa — tarkoituksellinen yhdenmukaisuusvalinta, ei bugi, mutta se tarkoittaa, että arkistointiprofiili ja sivujen välinen symbolijako ovat tänään toisensa poissulkevia. Mihin asetukseen tahansa päädytkin, dekoodaa se, mitä kirjoitit, ennen kuin luotat siihen: lataa tiedosto takaisin funktiolla LoadFromFile ja vedä jokainen sivu läpi funktiolla ExtractLoadedImage, joka ratkaisee jaetut globals-tiedot puolestasi samalla tavalla kuin mikä tahansa standardinmukainen lukija tekisi, ja vertaa tulosta lähdebittikarttoihisi
var
Loaded: THotPDF;
PageBmp: TBitmap;
PageIdx: Integer;
begin
Loaded := THotPDF.Create(nil);
try
Loaded.LoadFromFile('scanned-contract.pdf');
for PageIdx := 0 to Loaded.PagesCount - 1 do
begin
PageBmp := Loaded.ExtractLoadedImage(PageIdx); // resolves the shared globals for you
try
// Compare PageBmp against the source bitmap for this page.
finally
PageBmp.Free;
end;
end;
finally
Loaded.Free;
end;
end;
Sivujen välinen sanakirjan jakaminen koskettaa vain asiakirjan kaksitasoista kuvapuolta. Jos sama putki tuottaa myös luotuja tekstisivuja skannausten rinnalle — kansilehtiä, hakemistosivuja, OCR-tekstikerroksen — olio- ja xref-virrat hyökkäävät tiedostokokobudjetin toiseen puoliskoon pakkaamalla asiakirjarakenteen, jonka nuo sivut lisäävät. Sivujen välinen JBIG2-globals-jakaminen toimitetaan osana vakiomuotoista HotPDF-komponenttia Delphille ja C++Builderille, yhdessä kuvakohtaisten JBIG2-asetusten ja pakkausputken muun osan kanssa