Un contrat scanné de cinquante pages répète le même alphabet à chaque page, mais un encodeur JBIG2 qui construit un dictionnaire de symboles par image réentraîne cet alphabet cinquante fois séparément. HotPDF, le composant PDF natif pour Delphi et C++Builder, peut à la place accumuler un seul dictionnaire de symboles partagé sur l'ensemble du document et le promouvoir en un unique flux /JBIG2Globals au niveau du document, si bien que le flux JBIG2 propre à chaque page ne fait plus que référencer des identifiants de symboles au lieu de stocker sa propre copie de l'alphabet
Cet article reste volontairement restreint et ne couvre que la manière dont HotPDF construit ce partage entre pages en interne — les fondamentaux de JBIG2, la comparaison avec CCITT, et les compromis entre Lossless et LossyLevel figurent déjà dans l'article compagnon sur la compression bilevel JBIG2 native en Delphi, que cet article suppose déjà lu
Pourquoi la compression JBIG2 par page répète-t-elle encore le même coût ?
La réponse est que rien ne transporte d'état entre les appels. Chaque fois que l'encodeur de HotPDF construit un dictionnaire de symboles pour une image, ce dictionnaire est limité à cet unique appel AddImage : la passe de correspondance de formes repart de zéro, chaque glyphe de la page est classé comme nouveau, et les bitmaps résultants sont codés arithmétiquement et stockés à nouveau. Alimentez ce même encodeur avec cinquante pages composées dans la même police et il répète volontiers toute cette passe d'entraînement cinquante fois, car de son point de vue chaque page est une image sans rapport qui se trouve simplement ressembler à la précédente. Le UseSymbolDictionary par page bat déjà largement un encodage de région générique plat sur une seule page, mais il plafonne bien avant le niveau qu'une véritable numérisation multi-pages laisse sur la table
Comment HotPDF partage-t-il un dictionnaire de symboles unique entre les pages ?
Activez AccumulateGlobalsAcrossPages sur THPDFJBIG2Options et HotPDF garde un seul dictionnaire de symboles vivant en mémoire pendant toute la durée de vie du document au lieu de le jeter après chaque image. Les glyphes de chaque page suivante sont vérifiés par rapport à ce dictionnaire courant avant que quoi que ce soit ne soit recodé : une forme qui existe déjà est réutilisée via son identifiant de symbole, et seule une forme jamais vue auparavant est ajoutée et codée dans le dictionnaire. La comparaison réutilise la même logique de tolérance que LossyLevel applique sur une seule page — une numérisation légèrement bruitée de la même lettre compte tout de même comme une correspondance — de sorte que l'accumulateur ne gonfle pas silencieusement d'une entrée de dictionnaire par variation au niveau du pixel du même glyphe. L'extraction se produit d'abord et alimente cette comparaison : HotPDF parcourt le bitmap de chaque page et en extrait les formes connexes par remplissage par diffusion sur les pixels noirs, la même idée que tracer des taches d'encre à la main, et ce sont ces formes extraites, pas des blocs de pixels bruts, qui sont comparées au dictionnaire courant
Comment le dictionnaire partagé se place à l'intérieur d'un flux /JBIG2Globals
Le dictionnaire accumulé est écrit comme un seul segment de dictionnaire de symboles à l'intérieur du flux /JBIG2Globals, maintenu à un numéro de segment fixe afin que chaque page puisse pointer vers la même cible. À l'intérieur de l'organisation JBIG2 intégrée que définit ISO 32000-1 §7.4.7, un segment de région de texte peut nommer un autre segment comme sa source de symboles via le champ de segment référencé dans l'en-tête de segment, et c'est exactement le mécanisme sur lequel HotPDF s'appuie : le flux globals porte l'unique grand dictionnaire de symboles, et le flux JBIG2 propre à chaque page se réduit à un segment d'information de page plus un segment de région de texte dont la liste de référencés pointe vers le segment globals. Ce qui était auparavant un flux de bits autonome par page devient une courte liste de positions et d'identifiants de symboles, et chaque page construite ainsi référence l'objet indirect /JBIG2Globals identique plutôt qu'une copie de celui-ci. La couverture de régression propre à HotPDF vérifie précisément cela : encoder un court document où chaque page a une disposition de glyphes différente, le recharger, et compter combien de références d'objet /JBIG2Globals distinctes apparaissent dans le fichier — un document, une référence d'objet, quel que soit le nombre de pages ayant contribué des symboles
Activer l'accumulation de dictionnaire de symboles entre pages
Le commutateur se trouve sur le même enregistrement d'options couvert dans l'article compagnon, et il nécessite quatre réglages en accord les uns avec les autres avant que l'accumulation ne s'engage réellement
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;
Cet appariement n'est pas une décoration facultative. Le point d'extension d'encodeur externe décrit dans l'article sur la compression bilevel — celui que vous enregistrez via RegisterJBIG2EncoderBackend pour des taux de qualité production — est construit autour d'un encodage par image, et les propres démonstrations d'accumulation et tests de régression de HotPDF associent toujours AccumulateGlobalsAcrossPages à UseExternalEncoder := False. Traitez cela comme une exigence stricte plutôt que comme une suggestion : le partage entre pages est une fonctionnalité de l'encodeur natif, et un backend externe enregistré ne fait tout simplement pas partie du chemin qui construit le dictionnaire partagé
De combien une numérisation multi-pages devient-elle réellement plus petite ?
La réponse honnête commence par ce qui n'a d'abord pas fait bouger l'aiguille. Une version antérieure a ajouté un cache adressé par contenu pour les flux /JBIG2Globals — une recherche indexée par un hachage FNV-1a 64 bits des octets du flux, afin que deux images produisant par hasard des données globals identiques octet pour octet puissent partager un seul objet PDF. Mesuré face à la sortie réelle, ce cache n'a presque rien apporté, car la détection de doublons d'images entières déjà existante de HotPDF regroupait déjà les images identiques octet pour octet avant même que le cache n'ait la moindre chance de s'exécuter. La leçon fut que la déduplication au niveau du flux ne porte ses fruits que lorsque deux images de page véritablement différentes peuvent tout de même partager un dictionnaire en croissance, ce qu'apporte la véritable accumulation entre pages
Pour ce cas plus difficile, la propre estimation d'ingénierie de HotPDF situe l'économie supplémentaire à environ 30 à 60 pour cent de plus petit que ce qu'atteint la seule déduplication au niveau du flux, pour une numérisation multi-pages typique construite à partir d'une police récurrente — la plage varie selon la part du vocabulaire visuel du document qui se répète réellement, puisqu'une page pleine de diagrammes uniques ne donne rien au dictionnaire à réutiliser. Traitez cela comme un objectif de conception plutôt que comme une garantie pour une entrée spécifique quelconque, et mesurez vos propres documents plutôt que de faire confiance à un chiffre unique. La démonstration JBIG2Benchmark fournie avec HotPDF existe précisément à cette fin : elle encode la même numérisation multi-pages de quatre façons différentes et affiche la taille de fichier résultante pour chaque configuration, si bien que la comparaison s'exécute sur votre propre mélange de numérisations plutôt que sur un exemple synthétique
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.
Où l'accumulation entre pages atteint ses limites
Le dictionnaire accumulé est plafonné à 4096 symboles, le même plafond que l'encodeur natif par image applique déjà sur une seule page. Franchissez cette limite en cours de document et HotPDF ne lève pas d'exception ni n'interrompt l'exécution : l'accumulateur refuse le nouveau glyphe, et la page qui l'a introduit retombe automatiquement sur un encodage indépendant par image, si bien que le document en sort tout de même correct — vous cessez simplement d'obtenir l'économie entre pages pour les pages qui ont dépassé le plafond. Une seconde protection surveille la taille totale plutôt que le nombre de symboles : une fois que la largeur combinée des symboles du dictionnaire accumulé franchit 131071 pixels, HotPDF déverse automatiquement le lot courant sur disque et démarre un nouveau groupe globals frais, plutôt que de laisser une structure en mémoire croître sans limite. Aucune des deux limites ne nécessite de code de votre part, les deux étant des repli automatiques plutôt que des exceptions à intercepter
La conformité PDF/A est le seul réglage qui désactive tout le mécanisme plutôt que de simplement le plafonner. HotPDF substitue silencieusement CCITT Group 4 à JBIG2 dès que PDFACompliance est non vide, sur chaque page, indépendamment de AccumulateGlobalsAcrossPages ou de tout autre réglage sur JBIG2Options — un choix de conformité délibéré, pas un bogue, mais cela signifie qu'un profil d'archivage et le partage de symboles entre pages sont aujourd'hui mutuellement exclusifs. Quelle que soit la configuration à laquelle vous aboutissez, décodez ce que vous avez écrit avant de lui faire confiance : rechargez le fichier avec LoadFromFile et extrayez chaque page via ExtractLoadedImage, qui résout les globals partagés pour vous de la même manière que le ferait tout lecteur conforme, et comparez le résultat à vos bitmaps source
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;
Le partage de dictionnaire entre pages ne touche que le côté image bilevel d'un document. Si le même pipeline émet aussi des pages de texte générées aux côtés des numérisations — pages de couverture, pages d'index, une couche de texte OCR — les flux d'objets et les flux xref s'attaquent à l'autre moitié du budget de taille de fichier en compressant la structure de document que ces pages ajoutent. Le partage des globals JBIG2 entre pages fait partie du composant HotPDF pour Delphi et C++Builder, aux côtés des options JBIG2 par image et du reste du pipeline de compression