Articolo tecnico

Condividere Dizionari di Simboli JBIG2 tra Pagine in Delphi

Un contratto scansionato di cinquanta pagine ripete lo stesso alfabeto su ogni pagina, ma un encoder JBIG2 che costruisce un dizionario di simboli per ogni immagine riaddestra quell'alfabeto cinquanta volte separate. HotPDF, il componente PDF nativo per Delphi e C++Builder, può invece accumulare un unico dizionario di simboli condiviso lungo l'intero documento e promuoverlo a un singolo stream /JBIG2Globals a livello di documento, cosicché lo stream JBIG2 di ogni pagina si limiti a fare riferimento agli ID dei simboli invece di memorizzare una propria copia dell'alfabeto

Questo articolo resta deliberatamente circoscritto e tratta solo come HotPDF costruisce internamente questa condivisione tra pagine — i fondamenti di JBIG2, il confronto con CCITT e i compromessi tra Lossless e LossyLevel risiedono già nell'articolo di approfondimento sulla compressione bilevel JBIG2 nativa in Delphi, che questo articolo presuppone già letto

Perché la compressione JBIG2 per pagina ripete ancora lo stesso costo?

La risposta è che nulla porta stato tra una chiamata e l'altra. Ogni volta che l'encoder di HotPDF costruisce un dizionario di simboli per un'immagine, quel dizionario è circoscritto a quella singola chiamata AddImage: il passaggio di riconoscimento delle forme riparte da zero, ogni glifo sulla pagina viene classificato come nuovo, e i bitmap risultanti vengono codificati aritmeticamente e memorizzati da capo. Dai in pasto allo stesso encoder cinquanta pagine composte nello stesso carattere tipografico e ripeterà volentieri l'intero passaggio di addestramento cinquanta volte, perché dal suo punto di vista ogni pagina è un'immagine non correlata che per caso somiglia alle altre. UseSymbolDictionary per pagina batte già di gran lunga una semplice codifica a regione generica su una singola pagina, ma si ferma ben al di sotto del tetto che una vera scansione multipagina lascia sul tavolo

Come condivide HotPDF un unico dizionario di simboli tra le pagine?

Abilita AccumulateGlobalsAcrossPages su THPDFJBIG2Options e HotPDF mantiene vivo in memoria un unico dizionario di simboli per l'intera vita del documento invece di scartarlo dopo ogni immagine. I glifi di ogni pagina successiva vengono verificati contro quel dizionario in corso prima che qualsiasi cosa venga ricodificata: una forma già esistente viene riutilizzata tramite il proprio ID di simbolo, e solo una forma mai vista prima viene aggiunta e codificata nel dizionario. Il confronto riutilizza la stessa logica di tolleranza che LossyLevel applica su una singola pagina — una scansione leggermente rumorosa della stessa lettera conta comunque come corrispondenza — cosicché l'accumulatore non si gonfi silenziosamente in una voce di dizionario per ogni variazione a livello di pixel dello stesso glifo. L'estrazione avviene per prima e alimenta quel confronto: HotPDF percorre il bitmap di ogni pagina ed estrae forme connesse tramite flood fill sui pixel neri, la stessa idea del tracciare a mano macchie d'inchiostro, e sono quelle forme estratte, non blocchi di pixel grezzi, a essere confrontate con il dizionario in corso

Come si colloca il dizionario condiviso dentro uno stream /JBIG2Globals

Il dizionario accumulato viene scritto come un unico segmento dizionario-di-simboli dentro lo stream /JBIG2Globals, mantenuto a un numero di segmento fisso cosicché ogni pagina possa puntare allo stesso target. Dentro l'organizzazione JBIG2 incorporata definita da ISO 32000-1 §7.4.7, un segmento di regione testuale può indicare un altro segmento come propria fonte di simboli tramite il campo del segmento a cui si fa riferimento nell'intestazione del segmento, ed è esattamente questo il meccanismo su cui fa leva HotPDF: lo stream globals porta l'unico grande dizionario di simboli, e lo stream JBIG2 di ogni pagina si riduce a un segmento di informazioni di pagina più un segmento di regione testuale il cui elenco di riferimento punta indietro al segmento globals. Quello che un tempo era un flusso di bit autosufficiente per pagina diventa un breve elenco di posizioni e ID di simboli, e ogni pagina costruita in questo modo fa riferimento allo stesso identico oggetto indiretto /JBIG2Globals invece che a una sua copia. La copertura di regressione propria di HotPDF verifica esattamente questo: codifica un documento breve in cui ogni pagina ha un layout di glifi diverso, ricaricalo, e conta quanti riferimenti distinti a oggetti /JBIG2Globals compaiono nel file — un documento, un riferimento a oggetto, indipendentemente da quante pagine abbiano contribuito simboli ad esso

Attivare l'accumulo del dizionario di simboli tra pagine

L'interruttore risiede sullo stesso record di opzioni trattato nell'articolo di approfondimento, e richiede quattro impostazioni concordi tra loro prima che l'accumulo si attivi realmente

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;

Quell'abbinamento non è un ornamento facoltativo. Il punto di innesto per l'encoder esterno descritto nell'articolo sulla compressione bilevel — quello che si registra tramite RegisterJBIG2EncoderBackend per rapporti di compressione di livello produttivo — è costruito attorno alla codifica per singola immagine, e le demo e i test di regressione propri dell'accumulo di HotPDF abbinano sempre AccumulateGlobalsAcrossPages a UseExternalEncoder := False. Trattalo come un requisito rigido piuttosto che un suggerimento: la condivisione tra pagine è una funzionalità dell'encoder nativo, e un backend esterno registrato semplicemente non fa parte del percorso che costruisce il dizionario condiviso

Quanto diventa realmente più piccola una scansione multipagina?

La risposta onesta parte da ciò che inizialmente non ha spostato l'ago della bilancia. Una release precedente ha aggiunto una cache indirizzata per contenuto per gli stream /JBIG2Globals — una ricerca indicizzata da un hash FNV-1a a 64 bit dei byte dello stream, cosicché due immagini che per caso producevano dati globals identici byte per byte potessero condividere un unico oggetto PDF. Misurata contro output reali, quella cache ha aiutato a malapena, perché il rilevamento di duplicati sull'intera immagine già esistente in HotPDF stava già collassando le immagini identiche byte per byte prima ancora che la cache avesse la possibilità di intervenire. La lezione è stata che la deduplicazione a livello di stream paga solo una volta che due immagini di pagina genuinamente diverse possono comunque condividere un unico dizionario in crescita, che è esattamente ciò che offre la vera accumulazione tra pagine

Per quel caso più difficile, la stima ingegneristica propria di HotPDF colloca il risparmio aggiuntivo a circa il 30-60 percento in meno rispetto a quanto ottiene da sola la deduplicazione a livello di stream, per una tipica scansione multipagina costruita con un font ricorrente — l'intervallo si sposta in base a quanto del vocabolario visivo del documento effettivamente si ripete, poiché una pagina piena di diagrammi unici non lascia nulla da riutilizzare al dizionario. Trattalo come un obiettivo di progettazione piuttosto che una garanzia per qualsiasi input specifico, e misura i tuoi documenti invece di fidarti di un singolo numero. La demo JBIG2Benchmark fornita con HotPDF esiste esattamente per questo scopo: codifica la stessa scansione multipagina in quattro modi diversi e stampa la dimensione file risultante per ciascuna configurazione, cosicché il confronto avvenga contro il proprio mix di scansioni invece che contro uno sintetico

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.

Dove l'accumulo tra pagine incontra i propri limiti

Il dizionario accumulato è limitato a 4096 simboli, lo stesso tetto già imposto dall'encoder nativo per singola immagine su una singola pagina. Supera quel limite a metà documento e HotPDF non solleva un'eccezione né interrompe l'esecuzione: l'accumulatore rifiuta il nuovo glifo, e la pagina che lo ha introdotto ricade automaticamente sulla codifica indipendente per singola immagine, cosicché il documento risulti comunque corretto — si smette semplicemente di ottenere il risparmio tra pagine per qualunque pagina abbia superato il tetto. Una seconda salvaguardia monitora la dimensione totale anziché il conteggio dei simboli: una volta che la larghezza combinata dei simboli del dizionario accumulato supera 131071 pixel, HotPDF riversa automaticamente il lotto corrente su disco e avvia un nuovo gruppo globals, invece di lasciare che una singola struttura in memoria cresca senza limiti. Nessuno dei due limiti richiede codice da parte tua, poiché entrambi sono fallback automatici piuttosto che eccezioni da intercettare

La conformità PDF/A è l'unica impostazione che disattiva l'intero meccanismo invece di limitarsi a porgli un tetto. HotPDF sostituisce silenziosamente CCITT Group 4 a JBIG2 nel momento in cui PDFACompliance non è vuoto, su ogni pagina, indipendentemente da AccumulateGlobalsAcrossPages o da qualsiasi altra cosa in JBIG2Options — una scelta di conformità deliberata, non un bug, ma significa che un profilo archivistico e la condivisione di simboli tra pagine sono oggi mutuamente esclusivi. Qualunque configurazione tu scelga, decodifica ciò che hai scritto prima di fidartene: ricarica il file con LoadFromFile ed estrai ogni pagina tramite ExtractLoadedImage, che risolve i globals condivisi per te nello stesso modo in cui farebbe qualsiasi lettore conforme, e confronta il risultato con i tuoi bitmap sorgente

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;

La condivisione del dizionario tra pagine tocca solo il lato delle immagini bilevel di un documento. Se la stessa pipeline emette anche pagine di testo generate accanto alle scansioni — copertine, pagine indice, un layer di testo OCR — gli object stream e gli xref stream attaccano l'altra metà del budget di dimensione del file comprimendo la struttura documento che quelle pagine aggiungono. La condivisione dei globals JBIG2 tra pagine fa parte del componente HotPDF per Delphi e C++Builder, insieme alle opzioni JBIG2 per singola immagine e al resto della pipeline di compressione