Artículo técnico

Compartir diccionarios de símbolos JBIG2 entre páginas en Delphi

Un contrato escaneado de cincuenta páginas repite el mismo alfabeto en cada página, pero un codificador JBIG2 que construye un diccionario de símbolos por imagen vuelve a entrenar ese alfabeto cincuenta veces por separado. HotPDF, el componente PDF nativo para Delphi y C++Builder, puede en cambio acumular un único diccionario de símbolos compartido a lo largo de todo el documento y promoverlo a un solo flujo /JBIG2Globals a nivel de documento, así que el propio flujo JBIG2 de cada página simplemente referencia IDs de símbolo en lugar de almacenar su propia copia del alfabeto

Este texto se mantiene deliberadamente acotado y cubre solo cómo HotPDF construye internamente ese compartimiento entre páginas —los fundamentos de JBIG2, la comparación con CCITT, y las decisiones entre Lossless y LossyLevel ya se cubren en el artículo complementario sobre compresión bilevel JBIG2 nativa en Delphi, que este texto asume que usted ya leyó

¿Por qué la compresión JBIG2 por página sigue repitiendo el mismo costo?

La respuesta es que nada mantiene estado entre llamadas. Cada vez que el codificador de HotPDF construye un diccionario de símbolos para una imagen, ese diccionario queda acotado a esa única llamada a AddImage: la pasada de comparación de formas empieza desde cero, cada glifo de la página se clasifica como nuevo, y los mapas de bits resultantes se codifican aritméticamente y se almacenan de nuevo. Aliméntele al mismo codificador cincuenta páginas compuestas con la misma tipografía y con gusto repite esa pasada de entrenamiento completa cincuenta veces, porque desde su punto de vista cada página es una imagen sin relación que resulta parecerse. El UseSymbolDictionary por página ya supera por un amplio margen a una codificación de región genérica plana en una sola página, pero se topa con un techo bastante antes del límite que un escaneo real de varias páginas deja sobre la mesa

¿Cómo comparte HotPDF un único diccionario de símbolos entre páginas?

Active AccumulateGlobalsAcrossPages en THPDFJBIG2Options y HotPDF mantiene un diccionario de símbolos vivo en memoria durante toda la vida del documento en lugar de descartarlo después de cada imagen. Los glifos de cada página posterior se comprueban contra ese diccionario en curso antes de que nada se vuelva a codificar: una forma que ya existe se reutiliza por su ID de símbolo, y solo una forma que nadie ha visto antes se agrega y se codifica en el diccionario. La comparación reutiliza la misma lógica de tolerancia que LossyLevel aplica en una sola página —un escaneo ligeramente ruidoso de la misma letra sigue contando como coincidencia— así que el acumulador no se infla silenciosamente en una entrada de diccionario por cada variación a nivel de píxel del mismo glifo. La extracción ocurre primero y alimenta esa comparación: HotPDF recorre el mapa de bits de cada página y extrae formas conectadas mediante inundación (flood fill) contra los píxeles negros, la misma idea que trazar manchas de tinta a mano, y son esas formas extraídas, no bloques de píxeles crudos, las que se comparan contra el diccionario en curso

Cómo se ubica el diccionario compartido dentro de un flujo /JBIG2Globals

El diccionario acumulado se escribe como un único segmento de diccionario de símbolos dentro del flujo /JBIG2Globals, mantenido en un número de segmento fijo para que cada página pueda apuntar al mismo destino. Dentro de la organización JBIG2 incrustada que define ISO 32000-1 §7.4.7, un segmento de región de texto puede nombrar a otro segmento como su fuente de símbolos mediante el campo de segmento referenciado en el encabezado de segmento, y ese es exactamente el mecanismo en el que se apoya HotPDF: el flujo de globals lleva el único diccionario de símbolos grande, y el propio flujo JBIG2 de cada página se reduce a un segmento de información de página más un segmento de región de texto cuya lista de segmentos referenciados apunta de vuelta al segmento de globals. Lo que solía ser un flujo de bits autocontenido por página se convierte en una lista corta de posiciones e IDs de símbolo, y cada página construida de esta manera referencia el mismo objeto indirecto /JBIG2Globals en lugar de una copia de él. La propia cobertura de pruebas de regresión de HotPDF comprueba precisamente esto: codificar un documento corto donde cada página tiene un diseño de glifos distinto, recargarlo, y contar cuántas referencias de objeto /JBIG2Globals distintas aparecen en el archivo —un documento, una referencia de objeto, sin importar cuántas páginas hayan contribuido símbolos a él

Activar la acumulación de diccionario de símbolos entre páginas

El interruptor se ubica en el mismo registro de opciones cubierto en el artículo complementario, y requiere que cuatro configuraciones coincidan entre sí antes de que la acumulación realmente se active

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;

Ese emparejamiento no es decoración opcional. El punto de conexión de codificador externo descrito en el artículo de compresión bilevel —el que se registra mediante RegisterJBIG2EncoderBackend para lograr proporciones de nivel de producción— está construido en torno a la codificación por imagen, y las propias demostraciones de acumulación y pruebas de regresión de HotPDF siempre emparejan AccumulateGlobalsAcrossPages con UseExternalEncoder := False. Trate esto como un requisito estricto en lugar de una sugerencia: el compartimiento entre páginas es una característica del codificador nativo, y un backend externo registrado simplemente no forma parte de la ruta que construye el diccionario compartido

¿Cuánto más pequeño se vuelve realmente un escaneo de varias páginas?

La respuesta honesta empieza por lo que, en primer lugar, no movió la aguja. Una versión anterior agregó una caché direccionada por contenido para los flujos /JBIG2Globals —una búsqueda indexada por un hash FNV-1a de 64 bits de los bytes del flujo, así que dos imágenes que produjeran datos de globals idénticos byte por byte podían compartir un solo objeto PDF. Medida contra la salida real, esa caché apenas ayudó, porque la detección de duplicados de imagen completa que ya tenía HotPDF ya colapsaba las imágenes idénticas byte por byte antes de que la caché tuviera siquiera oportunidad de ejecutarse. La lección fue que la deduplicación a nivel de flujo solo rinde una vez que dos imágenes de página genuinamente distintas todavía pueden compartir un diccionario en crecimiento, que es lo que entrega la acumulación real entre páginas

Para ese caso más difícil, la propia estimación de ingeniería de HotPDF sitúa el ahorro adicional en aproximadamente 30 a 60 por ciento más pequeño que lo que logra la deduplicación a nivel de flujo por sí sola, para un escaneo típico de varias páginas construido con una fuente recurrente —el rango se mueve según cuánto del vocabulario visual del documento realmente se repite, ya que una página llena de diagramas únicos no le da al diccionario nada que reutilizar. Trate eso como un objetivo de diseño en lugar de una garantía para cualquier entrada específica, y mida sus propios documentos en lugar de confiar en un solo número. La demostración JBIG2Benchmark que se incluye con HotPDF existe exactamente para ese propósito: codifica el mismo escaneo de varias páginas de cuatro maneras distintas e imprime el tamaño de archivo resultante para cada configuración, así que la comparación se ejecuta contra su propia mezcla de escaneos en lugar de una sintética

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.

Dónde encuentra sus límites la acumulación entre páginas

El diccionario acumulado tiene un tope de 4096 símbolos, el mismo límite que el codificador nativo por imagen ya impone en una sola página. Si se cruza ese límite a mitad del documento, HotPDF no lanza una excepción ni aborta la ejecución: el acumulador rechaza el nuevo glifo, y la página que lo introdujo recurre automáticamente a la codificación independiente por imagen, así que el documento sigue saliendo correcto —simplemente se deja de obtener el ahorro entre páginas para las páginas que superaron el tope. Una segunda salvaguarda vigila el tamaño total en lugar del conteo de símbolos: en cuanto el ancho combinado de símbolos del diccionario acumulado cruza los 131071 píxeles, HotPDF vuelca el lote actual a disco y comienza automáticamente un nuevo grupo de globals, en lugar de dejar que una estructura en memoria crezca sin límite. Ninguno de los dos límites necesita código de su parte, ya que ambos son mecanismos de respaldo automáticos y no excepciones que haya que capturar

La conformidad PDF/A es la única configuración que desactiva todo el mecanismo en lugar de simplemente limitarlo. HotPDF silenciosamente sustituye JBIG2 por CCITT Group 4 en cuanto PDFACompliance no está vacío, en cada página, independientemente de AccumulateGlobalsAcrossPages o cualquier otra cosa en JBIG2Options —una decisión de conformidad deliberada, no un bug, pero significa que un perfil de archivo y el compartimiento de símbolos entre páginas son hoy mutuamente excluyentes. Cualquiera que sea la configuración a la que llegue, decodifique lo que escribió antes de confiar en ello: cargue el archivo de vuelta con LoadFromFile y extraiga cada página mediante ExtractLoadedImage, que resuelve los globals compartidos por usted de la misma manera en que lo haría cualquier lector conforme, y compare el resultado contra sus mapas de bits de origen

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;

El compartimiento de diccionario entre páginas solo afecta el lado de imagen bilevel de un documento. Si el mismo pipeline también emite páginas de texto generadas junto a los escaneos —hojas de portada, páginas de índice, una capa de texto OCR— los flujos de objetos y los flujos xref atacan la otra mitad del presupuesto de tamaño de archivo comprimiendo la estructura de documento que agregan esas páginas. El compartimiento de globals JBIG2 entre páginas se incluye como parte del componente HotPDF para Delphi y C++Builder, junto con las opciones JBIG2 por imagen y el resto del pipeline de compresión