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 cada 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 único flujo /JBIG2Globals a nivel de documento, de modo que el flujo JBIG2 propio de cada página se limita a referenciar identificadores de símbolo en lugar de almacenar su propia copia del alfabeto
Este artículo se mantiene deliberadamente centrado y cubre solo cómo construye HotPDF internamente ese compartimiento entre páginas; los fundamentos de JBIG2, la comparación con CCITT y las compensaciones entre Lossless y LossyLevel ya viven en el artículo complementario sobre la compresión bilevel JBIG2 nativa en Delphi, que este texto da por leído
¿Por qué la compresión JBIG2 por página sigue repitiendo el mismo coste?
La respuesta es que nada conserva 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 coincidencia 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 desde cero. Alimentad al mismo codificador con cincuenta páginas compuestas con la misma tipografía y repetirá encantado toda esa pasada de entrenamiento cincuenta veces, porque desde su punto de vista cada página es una imagen sin relación que resulta parecerse a las demás. El UseSymbolDictionary por página ya supera con creces una codificación de región genérica plana en una sola página, pero se queda muy por debajo del techo que un escaneo multipágina real deja sobre la mesa
¿Cómo comparte HotPDF un único diccionario de símbolos entre páginas?
Activad AccumulateGlobalsAcrossPages en THPDFJBIG2Options y HotPDF mantiene un único diccionario de símbolos vivo en memoria durante toda la vida del documento en lugar de descartarlo tras cada imagen. Los glifos de cada página posterior se comprueban contra ese diccionario en curso antes de que se vuelva a codificar nada: una forma que ya existe se reutiliza por su identificador de símbolo, y solo una forma que nadie ha visto antes se añade 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 hincha silenciosamente hasta tener 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 relleno por inundación 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 en bruto, las que se comparan contra el diccionario en curso
Cómo encaja 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, alojado en un número de segmento fijo para que todas las páginas puedan 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 la cabecera de segmento, y ese es exactamente el mecanismo en el que se apoya HotPDF: el flujo de globales 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 referencias apunta de vuelta al segmento de globales. Lo que antes era un flujo de bits autocontenido por página se convierte en una lista corta de posiciones e identificadores de símbolo, y cada página construida de este modo hace referencia al mismo objeto indirecto /JBIG2Globals en lugar de a una copia de él. La propia cobertura de pruebas de regresión de HotPDF comprueba precisamente esto: codifica un documento corto en el que cada página tiene una disposición de glifos distinta, lo recarga y cuenta cuántas referencias distintas al objeto /JBIG2Globals aparecen en el archivo, un documento, una referencia de objeto, sin importar cuántas páginas hayan aportado símbolos a él
Activar la acumulación de diccionario de símbolos entre páginas
El interruptor reside en el mismo registro de opciones cubierto en el artículo complementario, y necesita cuatro ajustes coincidentes 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 un adorno opcional. El punto de conexión del codificador externo descrito en el artículo sobre compresión bilevel, el que registráis a través de RegisterJBIG2EncoderBackend para obtener tasas de nivel de producción, está construido en torno a la codificación por imagen, y las propias demostraciones y pruebas de regresión de acumulación de HotPDF siempre emparejan AccumulateGlobalsAcrossPages con UseExternalEncoder := False. Tratad esto como un requisito estricto y no como una sugerencia: el compartimiento entre páginas es una característica del codificador nativo, y un backend externo registrado sencillamente no forma parte de la vía que construye el diccionario compartido
¿Cuánto se reduce realmente un escaneo multipágina?
La respuesta honesta empieza por lo que no marcó ninguna diferencia primero. Una versión anterior añadió 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, de modo que dos imágenes que resultaran producir datos de globales idénticos byte a byte pudieran compartir un único objeto PDF. Medida contra la salida real, esa caché apenas ayudó, porque la detección de duplicados de imagen completa ya existente en HotPDF ya colapsaba las imágenes idénticas byte a byte antes de que la caché llegara siquiera a entrar en juego. La lección fue que la deduplicación a nivel de flujo solo compensa una vez que dos imágenes de página genuinamente distintas todavía pueden compartir un mismo diccionario en crecimiento, que es exactamente lo que entrega la acumulación real entre páginas
Para ese caso más exigente, la propia estimación de ingeniería de HotPDF sitúa el ahorro adicional en aproximadamente entre un 30 y un 60 por ciento menos que lo que consigue por sí sola la deduplicación a nivel de flujo, para un escaneo multipágina típico construido con una tipografía recurrente; el rango se mueve según cuánto del vocabulario visual del documento realmente se repita, ya que una página llena de diagramas únicos no le da al diccionario nada que reutilizar. Tratad esto como un objetivo de diseño y no como una garantía para ninguna entrada concreta, y medid vuestros propios documentos en lugar de confiar en una cifra única. La demostración JBIG2Benchmark que se distribuye con HotPDF existe exactamente para eso: codifica el mismo escaneo multipágina de cuatro formas distintas e imprime el tamaño de archivo resultante para cada configuración, de modo que la comparación se ejecuta contra vuestra propia mezcla de escaneos en lugar de contra 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 está limitado a 4096 símbolos, el mismo techo que ya impone el codificador nativo por imagen en una sola página. Cruzad ese límite a mitad de documento y HotPDF no lanza ninguna excepción ni aborta la ejecución: el acumulador rechaza el nuevo glifo, y la página que lo introdujo recae automáticamente en la codificación independiente por imagen, así que el documento sigue saliendo correcto, simplemente dejáis de obtener el ahorro entre páginas para las páginas que superaron el techo. Una segunda salvaguarda vigila el tamaño total en lugar del recuento de símbolos: en cuanto el ancho combinado de símbolos del diccionario acumulado supera los 131071 píxeles, HotPDF vuelca automáticamente el lote actual a disco y comienza un nuevo grupo de globales, en lugar de dejar que una estructura en memoria crezca sin límite. Ninguno de los dos límites necesita código de vuestra parte, ya que ambos son mecanismos de respaldo automáticos y no excepciones que haya que capturar
La conformidad PDF/A es el único ajuste que desactiva todo el mecanismo en lugar de simplemente acotarlo. HotPDF sustituye discretamente JBIG2 por CCITT Group 4 en cuanto PDFACompliance no está vacío, en todas las páginas, independientemente de AccumulateGlobalsAcrossPages o de cualquier otro ajuste de JBIG2Options, una decisión de conformidad deliberada, no un fallo, pero significa que un perfil de archivado y el compartimiento de símbolos entre páginas son hoy mutuamente excluyentes. Sea cual sea la configuración a la que lleguéis, descodificad lo que habéis escrito antes de confiar en ello: recargad el archivo con LoadFromFile y extraed cada página mediante ExtractLoadedImage, que resuelve por vosotros los globales compartidos del mismo modo que lo haría cualquier lector conforme, y comparad el resultado con vuestros 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 al lado de imagen bilevel de un documento. Si el mismo flujo de trabajo también genera páginas de texto generado junto a los escaneos, portadas, 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 añaden esas páginas. Los globales JBIG2 entre páginas forman parte del componente HotPDF para Delphi y C++Builder, junto con las opciones JBIG2 por imagen y el resto del pipeline de compresión