Padesátistránková naskenovaná smlouva opakuje na každé stránce stejnou abecedu, ale enkodér JBIG2, který staví jeden slovník symbolů na obrázek, tuto abecedu znovu natrénuje padesátkrát zvlášť. HotPDF, nativní komponenta PDF pro Delphi a C++Builder, může místo toho akumulovat jeden sdílený slovník symbolů přes celý dokument a povýšit jej na jediný proud /JBIG2Globals na úrovni dokumentu, takže vlastní proud JBIG2 každé stránky jen odkazuje na ID symbolů místo toho, aby si ukládal vlastní kopii abecedy
Tento text je záměrně úzce zaměřený a pokrývá jen to, jak HotPDF interně staví toto sdílení napříč stránkami — základy JBIG2, srovnání s CCITT a kompromisy mezi Lossless a LossyLevel už žijí v doprovodném článku o nativní bilevel kompresi JBIG2 v Delphi, který tento text předpokládá, že jste už četli
Proč komprese JBIG2 po stránkách stále opakuje stejný náklad?
Odpověď je, že nic si mezi voláními neponechává stav. Pokaždé, když enkodér HotPDF staví slovník symbolů pro jeden obrázek, je tento slovník omezený na to jedno volání AddImage: průchod porovnávání tvarů začíná od nuly, každý glyf na stránce se klasifikuje jako nový, a výsledné bitmapy se aritmeticky zakódují a uloží znovu od začátku. Nakrmíte-li stejný enkodér padesáti stránkami sázenými stejným písmem, ochotně zopakuje celý trénovací průchod padesátkrát, protože z jeho pohledu je každá stránka nesouvisející obrázek, který se náhodou podobá ostatním. UseSymbolDictionary po stránkách už na jedné stránce s velkým náskokem porazí ploché zakódování generické oblasti, ale zdaleka nedosáhne stropu, který skutečný vícestránkový sken nechává na stole
Jak HotPDF sdílí jeden slovník symbolů napříč stránkami?
Zapněte AccumulateGlobalsAcrossPages na THPDFJBIG2Options a HotPDF udrží jeden slovník symbolů naživu v paměti po celou dobu života dokumentu místo toho, aby jej po každém obrázku zahodil. Glyfy každé následující stránky se zkontrolují proti tomuto běžícímu slovníku ještě předtím, než se cokoli znovu zakóduje: tvar, který už existuje, se znovu použije podle svého ID symbolu, a jen tvar, který nikdo dosud neviděl, se připojí a zakóduje do slovníku. Porovnávání znovu používá stejnou logiku tolerance, kterou LossyLevel aplikuje na jedné stránce — mírně zašuměný sken téhož písmene se pořád počítá jako shoda — takže akumulátor tiše nenabobtná do jednoho záznamu slovníku na každou pixelovou odchylku téhož glyfu. Extrakce probíhá jako první a živí toto porovnávání: HotPDF prochází bitmapu každé stránky a pomocí flood fill proti černým pixelům vytáhne spojené tvary, stejná myšlenka jako ruční obkreslování inkoustových skvrn, a jsou to právě extrahované tvary, ne surové bloky pixelů, co se porovnává proti běžícímu slovníku
Jak sdílený slovník sedí uvnitř proudu /JBIG2Globals
Akumulovaný slovník se zapíše jako jeden segment slovníku symbolů uvnitř proudu /JBIG2Globals, držený na pevném čísle segmentu, aby na stejný cíl mohla ukazovat každá stránka. Uvnitř organizace vloženého JBIG2, kterou definuje ISO 32000-1 §7.4.7, může segment textové oblasti pojmenovat jiný segment jako svůj zdroj symbolů přes pole odkazovaného segmentu v hlavičce segmentu, a to je přesně mechanismus, o který se HotPDF opírá: proud globals nese jeden velký slovník symbolů a vlastní proud JBIG2 každé stránky se zmenší na segment informací o stránce plus segment textové oblasti, jehož seznam odkazovaných segmentů ukazuje zpět na segment globals. Z toho, co dřív bylo samostatný bitstream pro každou stránku, se stane krátký seznam pozic a ID symbolů, a každá stránka postavená tímto způsobem odkazuje na tentýž nepřímý objekt /JBIG2Globals místo jeho kopie. Vlastní regresní pokrytí HotPDF kontroluje přesně tohle: zakóduje krátký dokument, kde má každá stránka jiné rozvržení glyfů, znovu jej načte a spočítá, kolik odlišných odkazů na objekt /JBIG2Globals se v souboru objeví — jeden dokument, jeden odkaz na objekt, bez ohledu na to, kolik stránek do něj přispělo symboly
Zapnutí akumulace slovníku symbolů napříč stránkami
Přepínač sedí na stejném záznamu voleb popsaném v doprovodném článku a k tomu, aby akumulace skutečně nastoupila, potřebuje čtyři vzájemně souhlasící nastavení
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;
Toto párování není volitelná dekorace. Rozhraní pro externí enkodér popsané v článku o bilevel kompresi — to, které registrujete přes RegisterJBIG2EncoderBackend kvůli produkčním kompresním poměrům — je postavené kolem kódování po jednotlivých obrázcích, a vlastní demo verze akumulace i regresní testy HotPDF vždy párují AccumulateGlobalsAcrossPages s UseExternalEncoder := False. Berte to jako tvrdý požadavek, ne jako doporučení: sdílení napříč stránkami je funkce nativního enkodéru, a registrovaný externí backend prostě není součástí cesty, která staví sdílený slovník
O kolik skutečně zmenší vícestránkový sken?
Poctivá odpověď začíná tím, co ukazatel na začátku nepohnulo. Dřívější vydání přidalo cache adresovanou obsahem pro proudy /JBIG2Globals — vyhledávání klíčované 64bitovým hashem FNV-1a bajtů proudu, takže dva obrázky, které náhodou vyprodukovaly bajtově identická data globals, mohly sdílet jeden objekt PDF. Změřeno na skutečném výstupu tato cache pomohla jen málo, protože stávající detekce duplicit celých obrázků v HotPDF už bajtově identické obrázky sbalovala ještě předtím, než cache vůbec dostala šanci se spustit. Poučení bylo, že deduplikace na úrovni proudu se vyplatí teprve tehdy, když si dva skutečně odlišné obrázky stránek přesto mohou sdílet jeden rostoucí slovník, což je přesně to, co přináší skutečná akumulace napříč stránkami
Pro tento těžší případ vlastní inženýrský odhad HotPDF klade dodatečnou úsporu na zhruba 30 až 60 procent menší velikost, než jaké dosahuje samotná deduplikace na úrovni proudu, u typického vícestránkového skenu postaveného na jednom opakujícím se písmu — rozsah se posouvá podle toho, kolik z vizuální slovní zásoby dokumentu se skutečně opakuje, protože stránka plná unikátních diagramů dá slovníku nic k opakovanému použití. Berte to jako návrhový cíl, ne jako záruku pro konkrétní vstup, a měřte vlastní dokumenty místo toho, abyste věřili jednomu číslu. Demo JBIG2Benchmark, které se dodává s HotPDF, existuje přesně za tímto účelem: zakóduje stejný vícestránkový sken čtyřmi různými způsoby a vytiskne výslednou velikost souboru pro každou konfiguraci, takže srovnání běží proti vaší vlastní směsi skenů místo syntetické
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.
Kde akumulace napříč stránkami naráží na své limity
Akumulovaný slovník je omezen na 4096 symbolů, stejný strop, jaký už nativní enkodér vynucuje na jedné stránce pro jeden obrázek. Překročíte-li tento limit uprostřed dokumentu, HotPDF nevyvolá výjimku ani neukončí běh: akumulátor nový glyf odmítne a stránka, která jej zavedla, automaticky spadne na nezávislé kódování po jednotlivých obrázcích, takže dokument pořád vyjde správně — jen přestanete dostávat úsporu napříč stránkami pro ty stránky, které limit překročily. Druhá pojistka hlídá spíš celkovou velikost než počet symbolů: jakmile kombinovaná šířka symbolů akumulovaného slovníku překročí 131071 pixelů, HotPDF automaticky rozlije aktuální dávku na disk a začne novou skupinu globals, místo aby nechal jednu strukturu v paměti růst neomezeně. Žádný z limitů nevyžaduje na vaší straně žádný kód, protože oba jsou automatické záložní mechanismy, ne výjimky, které byste museli zachytávat
Konformita PDF/A je to jediné nastavení, které celý mechanismus vypne úplně, místo aby jej jen omezilo. HotPDF ve chvíli, kdy je PDFACompliance neprázdné, tiše nahradí JBIG2 za CCITT Group 4, na každé stránce, nezávisle na AccumulateGlobalsAcrossPages nebo čemkoli jiném na JBIG2Options — jde o záměrné rozhodnutí kvůli konformitě, ne o chybu, ale znamená to, že archivní profil a sdílení symbolů napříč stránkami jsou dnes vzájemně vylučující se. Ať už skončíte s jakoukoli konfigurací, dekódujte, co jste zapsali, dřív než tomu uvěříte: znovu načtěte soubor přes LoadFromFile a protáhněte každou stránku přes ExtractLoadedImage, což za vás rozřeší sdílené globals stejným způsobem jako jakýkoli standardu odpovídající čtenář, a porovnejte výsledek s vašimi zdrojovými bitmapami
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;
Sdílení slovníku napříč stránkami se dotýká jen bilevel obrazové strany dokumentu. Pokud stejná pipeline vedle skenů vydává i generované textové stránky — titulní listy, rejstříky, vrstvu OCR textu — proudy objektů a proudy xref útočí na druhou polovinu rozpočtu velikosti souboru tím, že komprimují strukturu dokumentu, kterou tyto stránky přidávají. Sdílené globals JBIG2 napříč stránkami jsou součástí komponenty HotPDF pro Delphi a C++Builder, spolu s volbami JBIG2 po jednotlivých obrázcích a zbytkem kompresní pipeline