Techninis straipsnis

Bendras JBIG2 simbolių žodynas Delphi puslapiuose

Penkiasdešimties puslapių skenuotoje sutartyje kiekviename puslapyje kartojasi ta pati abėcėlė, tačiau JBIG2 koduotuvas, kiekvienam vaizdui kuriantis atskirą simbolių žodyną, tą abėcėlę iš naujo apmoko penkiasdešimt kartų. HotPDF, savasis Delphi ir C++Builder PDF komponentas, gali kaupti vieną bendrą simbolių žodyną visame dokumente ir perkelti jį į vieną dokumento lygio /JBIG2Globals srautą, todėl kiekvieno puslapio JBIG2 srautas tik nurodo simbolių ID, o ne saugo atskirą abėcėlės kopiją

Šis straipsnis sąmoningai apsiriboja siaura tema ir aprašo tik tai, kaip HotPDF viduje sukuria dalijimąsi tarp puslapių — JBIG2 pagrindai, CCITT palyginimas ir Lossless bei LossyLevel kompromisai jau aptarti gretimame straipsnyje apie savąjį JBIG2 dvejetainį glaudinimą Delphi aplinkoje, kurį šiame straipsnyje laikome perskaitytu

Kodėl JBIG2 glaudinimas atskiriems puslapiams vis dar kartoja tas pačias sąnaudas?

Atsakymas tas, kad tarp iškvietimų niekas neišlaiko būsenos. Kiekvieną kartą, kai HotPDF koduotuvas kuria vieno vaizdo simbolių žodyną, tas žodynas taikomas tik tam vieninteliam AddImage iškvietimui: formų sutapdinimo etapas pradedamas nuo nulio, kiekvienas puslapio glifas klasifikuojamas kaip naujas, o gautos taškinės matricos iš naujo aritmetiškai užkoduojamos ir išsaugomos. Pateikus tam pačiam koduotuvui penkiasdešimt tos pačios šriftų šeimos puslapių, jis mielai penkiasdešimt kartų pakartoja visą mokymo etapą, nes jo požiūriu kiekvienas puslapis yra nesusijęs vaizdas, kuris tik atrodo panašiai. Vieno puslapio atveju puslapinis UseSymbolDictionary jau gerokai lenkia paprastą bendros srities kodavimą, tačiau pasiekia ribą gerokai anksčiau, nei tikras kelių puslapių skenuotas dokumentas leidžia išnaudoti galimybes

Kaip HotPDF dalijasi vienu simbolių žodynu tarp puslapių?

Įjunkite AccumulateGlobalsAcrossPages objekte THPDFJBIG2Options, ir HotPDF visą dokumento gyvavimo laiką atmintyje išlaikys vieną simbolių žodyną, užuot išmetęs jį po kiekvieno vaizdo. Kiekvieno vėlesnio puslapio glifai patikrinami pagal tuo metu kaupiamą žodyną prieš juos iš naujo koduojant: jau esanti forma pakartotinai naudojama pagal jos simbolio ID, o tik dar nematyta forma pridedama prie žodyno ir jame užkoduojama. Palyginimui naudojama ta pati tolerancijos logika, kurią LossyLevel taiko viename puslapyje — šiek tiek triukšmingas to paties simbolio skenas vis tiek laikomas atitikmeniu — todėl kaupiklis tyliai neišsipučia iki vieno žodyno įrašo kiekvienam to paties glifo pikselių lygio skirtumui. Išskyrimas atliekamas pirmiausia ir pateikia duomenis šiam palyginimui: HotPDF pereina kiekvieno puslapio taškinį vaizdą ir užliejimo algoritmu iš juodų pikselių išskiria susietas formas, panašiai kaip ranka apvedant rašalo dėmes, o lyginamos būtent šios išskirtos formos, ne neapdoroti pikselių blokai

Kaip bendras žodynas įterpiamas į /JBIG2Globals srautą

Sukauptas žodynas įrašomas kaip vienas simbolių žodyno segmentas /JBIG2Globals sraute ir laikomas fiksuotu segmento numeriu, kad kiekvienas puslapis galėtų nurodyti tą patį tikslą. Įterptoje JBIG2 struktūroje, kurią apibrėžia ISO 32000-1 §7.4.7, teksto srities segmentas segmento antraštėje esančiame nurodyto segmento lauke gali įvardyti kitą segmentą kaip savo simbolių šaltinį, ir būtent tuo mechanizmu remiasi HotPDF: globalių duomenų sraute laikomas vienas didelis simbolių žodynas, o kiekvieno puslapio JBIG2 srautas sutrumpėja iki puslapio informacijos segmento ir teksto srities segmento, kurio nurodomų segmentų sąrašas grįžta į globalų segmentą. Anksčiau kiekvieno puslapio savarankiškas bitų srautas tampa trumpu pozicijų ir simbolių ID sąrašu, o kiekvienas taip sukurtas puslapis nurodo tą patį netiesioginį /JBIG2Globals objektą, o ne jo kopiją. HotPDF regresiniai testai tikrina būtent tai: užkoduoja trumpą dokumentą, kuriame kiekviename puslapyje išdėstyti skirtingi glifai, įkelia jį iš naujo ir suskaičiuoja, kiek faile rodoma skirtingų /JBIG2Globals objektų nuorodų — vienas dokumentas, viena objekto nuoroda, nesvarbu, kiek puslapių prie jo pridėjo simbolių

Kaip įjungti simbolių žodyno kaupimą tarp puslapių

Šis jungiklis yra tame pačiame parinkčių įraše, kuris aprašytas gretimame straipsnyje, o kaupimas iš tikrųjų įsijungia tik tada, kai tarpusavyje dera keturios nuostatos

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;

Ši pora nėra neprivaloma puošmena. Išoriniam koduotuvui skirta jungtis, aprašyta dvejetainio glaudinimo straipsnyje — ta, kurią produkcinio lygio santykiams registruojate per RegisterJBIG2EncoderBackend — veikia pagal vaizdų kodavimą, o HotPDF kaupimo demonstracijos ir regresiniai testai visada derina AccumulateGlobalsAcrossPages su UseExternalEncoder := False. Laikykite tai griežtu reikalavimu, o ne pasiūlymu: dalijimasis tarp puslapių yra savojo koduotuvo funkcija, o registruotas išorinis modulis paprasčiausiai nėra kelio, kuriuo kuriamas bendras žodynas, dalis

Kiek iš tikrųjų sumažėja kelių puslapių skeno dydis?

Sąžiningas atsakymas prasideda nuo to, kas iš pradžių nepadarė didelio poveikio. Ankstesniame leidime atsirado turiniu adresuojama /JBIG2Globals srautų podėlio sistema — paieška pagal 64 bitų FNV-1a srauto baitų maišą, kad du vaizdai, atsitiktinai sukūrę vienodus globalių duomenų baitus, galėtų dalytis vienu PDF objektu. Išmatavus tikrą išvestį paaiškėjo, kad šis podėlis padėjo labai mažai, nes HotPDF esamas viso vaizdo dublikatų aptikimas jau sutraukdavo vienodus vaizdus dar prieš podėliui pradedant veikti. Pamoka tokia: srauto lygio dublikatų šalinimas atsiperka tik tada, kai du iš tikrųjų skirtingi puslapio vaizdai vis dar gali dalytis vienu augančiu žodynu, o būtent tai suteikia tikras kaupimas tarp puslapių

Šiuo sudėtingesniu atveju HotPDF inžinerinis įvertis numato, kad papildomas sutaupymas paprastai sudaro maždaug 30–60 procentų mažesnį dydį, palyginti su vien srauto lygio dublikatų šalinimu, kai kelių puslapių skenas sukurtas iš vieno pasikartojančio šrifto — intervalas priklauso nuo to, kiek dokumento vaizdinio žodyno kartojasi, nes puslapis, pilnas unikalių diagramų, nesuteikia žodynui ko pakartotinai naudoti. Laikykite tai projektiniu tikslu, o ne konkrečios įvesties garantija, ir matuokite savo dokumentus, užuot pasikliovę vienu skaičiumi. Su HotPDF pateikiama JBIG2Benchmark demonstracija skirta būtent tam: ji keturiais skirtingais būdais užkoduoja tą patį kelių puslapių skeną ir išveda kiekvienos konfigūracijos failo dydį, todėl palyginimas atliekamas su jūsų skenų rinkiniu, o ne su sintetiniu pavyzdžiu

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.

Kur kaupimas tarp puslapių pasiekia ribas

Sukauptame žodyne galima laikyti ne daugiau kaip 4096 simbolius — tai ta pati riba, kurią savasis koduotuvas jau taiko vienam puslapiui. Pasiekus šią ribą dokumento viduryje, HotPDF nekelia išimties ir nenutraukia vykdymo: kaupiklis atsisako naujo glifo, o jį įvedęs puslapis automatiškai grįžta prie nepriklausomo vaizdo kodavimo, todėl dokumentas vis tiek sukuriamas teisingai — tiesiog negaunate dalijimosi tarp puslapių sutaupymo tiems puslapiams, kurie viršijo ribą. Kita apsauga stebi bendrą dydį, o ne simbolių skaičių: kai sukaupto žodyno bendras simbolių plotis viršija 131071 pikselį, HotPDF išsaugo dabartinę grupę diske ir automatiškai pradeda naują globalių duomenų grupę, užuot leidęs vienai atmintyje esančiai struktūrai augti be ribų. Nė vienai iš šių ribų jūsų kode nereikia jokio apdorojimo, nes abi yra automatiniai atsarginiai keliai, o ne išimtys, kurias turėtumėte sugauti

PDF/A atitiktis yra vienintelė nuostata, kuri visiškai išjungia šį mechanizmą, o ne tik jį apriboja. Vos tik PDFACompliance nėra tuščias, HotPDF kiekviename puslapyje tyliai pakeičia JBIG2 į CCITT Group 4, nepriklausomai nuo AccumulateGlobalsAcrossPages ar bet ko kito objekte JBIG2Options — tai sąmoningas atitikties pasirinkimas, o ne klaida, tačiau šiandien tai reiškia, kad archyvavimo profilis ir dalijimasis simboliais tarp puslapių yra nesuderinami. Kad ir kokią konfigūraciją pasirinktumėte, prieš ja pasitikėdami iškoduokite sukurtą failą: įkelkite jį su LoadFromFile ir ištraukite kiekvieną puslapį per ExtractLoadedImage, kuri automatiškai išsprendžia bendrus globalius duomenis taip, kaip tai padarytų bet kuri suderinama skaityklė, tada palyginkite rezultatą su pradiniais taškiniais vaizdais

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;

Dalijimasis žodynu tarp puslapių veikia tik dokumento dvejetainių vaizdų dalyje. Jei tas pats procesas kartu generuoja tekstinius puslapius šalia skenų — viršelio lapus, rodyklės puslapius ar OCR tekstinį sluoksnį — objektų srautai ir xref srautai suspaudžia kitą failo dydžio biudžeto dalį, nes glaudina šių puslapių pridedamą dokumento struktūrą. Dalijimasis JBIG2 globaliais duomenimis tarp puslapių yra HotPDF komponento, skirto Delphi ir C++Builder, dalis kartu su puslapinėmis JBIG2 parinktimis ir likusiu glaudinimo procesu