Um contrato digitalizado de cinquenta páginas repete o mesmo alfabeto em cada página, mas um codificador JBIG2 que constrói um dicionário de símbolos por imagem retreina esse alfabeto cinquenta vezes separadamente. O HotPDF, o componente PDF nativo para Delphi e C++Builder, pode em vez disso acumular um único dicionário de símbolos compartilhado ao longo de todo o documento e promovê-lo a um único stream /JBIG2Globals em nível de documento, de modo que o stream JBIG2 próprio de cada página apenas referencia IDs de símbolo em vez de armazenar sua própria cópia do alfabeto
Este texto permanece propositalmente restrito e cobre apenas como o HotPDF constrói esse compartilhamento entre páginas internamente — os fundamentos de JBIG2, a comparação com CCITT, e as trocas entre Lossless e LossyLevel já estão em o artigo complementar sobre compressão bilevel nativa JBIG2 em Delphi, que este texto presume que você já leu
Por que a compressão JBIG2 por página ainda repete o mesmo custo?
A resposta é que nada carrega estado entre chamadas. Toda vez que o codificador do HotPDF constrói um dicionário de símbolos para uma imagem, esse dicionário é escopado àquela única chamada AddImage: a passada de correspondência de formas começa do zero, cada glifo da página é classificado como novo, e os bitmaps resultantes são codificados aritmeticamente e armazenados do zero. Alimente o mesmo codificador com cinquenta páginas na mesma tipografia e ele repete alegremente toda essa passada de treinamento cinquenta vezes, porque do seu ponto de vista cada página é uma imagem não relacionada que por acaso se parece com as outras. O UseSymbolDictionary por página já supera de longe uma codificação de região genérica chapada em uma única página, mas ele atinge seu teto bem antes do limite que uma digitalização multipágina real deixa na mesa
Como o HotPDF compartilha um único dicionário de símbolos entre páginas?
Ative AccumulateGlobalsAcrossPages em THPDFJBIG2Options e o HotPDF mantém um dicionário de símbolos vivo em memória durante toda a vida do documento, em vez de descartá-lo depois de cada imagem. Os glifos de cada página subsequente são verificados contra esse dicionário corrente antes de qualquer coisa ser recodificada: uma forma que já existe é reutilizada pelo seu ID de símbolo, e apenas uma forma que ninguém viu antes é anexada e codificada no dicionário. A comparação reutiliza a mesma lógica de tolerância que LossyLevel aplica em uma única página — uma digitalização ligeiramente ruidosa da mesma letra ainda conta como correspondência — de modo que o acumulador não cresce silenciosamente para uma entrada de dicionário por variação em nível de pixel do mesmo glifo. A extração acontece primeiro e alimenta essa comparação: o HotPDF percorre o bitmap de cada página e extrai formas conectadas por meio de preenchimento por inundação (flood fill) contra os pixels pretos, a mesma ideia de traçar manchas de tinta à mão, e são essas formas extraídas, não blocos de pixel brutos, que são comparadas contra o dicionário corrente
Como o dicionário compartilhado fica dentro de um stream /JBIG2Globals
O dicionário acumulado é gravado como um único segmento de dicionário de símbolos dentro do stream /JBIG2Globals, mantido em um número de segmento fixo para que toda página possa apontar para o mesmo alvo. Dentro da organização JBIG2 embutida que a ISO 32000-1 §7.4.7 define, um segmento de região de texto pode nomear outro segmento como sua fonte de símbolos por meio do campo de segmento referenciado no cabeçalho do segmento, e é exatamente esse o mecanismo em que o HotPDF se apoia: o stream de globais carrega o único grande dicionário de símbolos, e o stream JBIG2 próprio de cada página encolhe para um segmento de informação de página mais um segmento de região de texto cuja lista de referência aponta de volta ao segmento de globais. O que costumava ser um bitstream autocontido por página vira uma pequena lista de posições e IDs de símbolo, e cada página construída dessa forma referencia o mesmo objeto indireto /JBIG2Globals, em vez de uma cópia dele. A própria cobertura de regressão do HotPDF verifica precisamente isso: codifica um documento curto onde cada página tem um layout de glifos diferente, recarrega-o, e conta quantas referências de objeto /JBIG2Globals distintas aparecem no arquivo — um documento, uma referência de objeto, não importa quantas páginas tenham contribuído símbolos para ele
Ativando o acúmulo de dicionário de símbolos entre páginas
O interruptor fica no mesmo registro de opções coberto no artigo complementar, e ele exige quatro configurações concordando entre si antes de o acúmulo de fato entrar em ação
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;
Esse pareamento não é decoração opcional. A costura de codificador externo descrita no artigo de compressão bilevel — a que você registra por meio de RegisterJBIG2EncoderBackend para taxas de nível de produção — é construída em torno de codificação por imagem, e as próprias demos e testes de regressão de acúmulo do HotPDF sempre pareiam AccumulateGlobalsAcrossPages com UseExternalEncoder := False. Trate isso como uma exigência rígida e não como uma sugestão: o compartilhamento entre páginas é um recurso do codificador nativo, e um backend externo registrado simplesmente não faz parte do caminho que constrói o dicionário compartilhado
De quanto realmente diminui uma digitalização multipágina?
A resposta honesta começa pelo que não mudou muita coisa primeiro. Um lançamento anterior adicionou um cache endereçado por conteúdo para streams /JBIG2Globals — uma busca indexada por um hash FNV-1a de 64 bits dos bytes do stream, de modo que duas imagens que por acaso produzissem dados de globais byte a byte idênticos pudessem compartilhar um objeto PDF. Medido contra a saída real, esse cache mal ajudou, porque a detecção de duplicata de imagem inteira já existente no HotPDF já estava colapsando imagens byte a byte idênticas antes mesmo de o cache ter chance de rodar. A lição foi que a deduplicação em nível de stream só se paga quando duas imagens de página genuinamente diferentes ainda conseguem compartilhar um dicionário em crescimento, que é exatamente o que o verdadeiro acúmulo entre páginas entrega
Para esse caso mais difícil, a própria estimativa de engenharia do HotPDF coloca a economia adicional em aproximadamente 30 a 60 por cento menor do que a deduplicação em nível de stream sozinha consegue, para uma digitalização multipágina típica construída a partir de uma fonte recorrente — a faixa varia de acordo com quanto do vocabulário visual do documento de fato se repete, já que uma página cheia de diagramas únicos não dá ao dicionário nada para reutilizar. Trate isso como uma meta de projeto, e não uma garantia para qualquer entrada específica, e meça seus próprios documentos em vez de confiar em um único número. A demo JBIG2Benchmark que acompanha o HotPDF existe exatamente para esse propósito: ela codifica a mesma digitalização multipágina de quatro formas diferentes e imprime o tamanho de arquivo resultante para cada configuração, de modo que a comparação roda contra sua própria mistura de digitalizações, não uma 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.
Onde o acúmulo entre páginas atinge seus limites
O dicionário acumulado tem um teto de 4096 símbolos, o mesmo limite que o codificador nativo por imagem já impõe em uma única página. Ultrapasse esse limite no meio do documento e o HotPDF não levanta uma exceção nem aborta a execução: o acumulador recusa o novo glifo, e a página que o introduziu recai automaticamente para codificação independente por imagem, de modo que o documento ainda sai correto — você só deixa de obter a economia entre páginas para as páginas que ultrapassaram o teto. Uma segunda salvaguarda observa o tamanho total em vez da contagem de símbolos: assim que a largura combinada dos símbolos do dicionário acumulado ultrapassa 131071 pixels, o HotPDF despeja o lote atual em disco e inicia um novo grupo de globais automaticamente, em vez de deixar uma estrutura em memória crescer sem limite. Nenhum dos dois limites exige código do seu lado, já que ambos são recuos automáticos, e não exceções que você precise capturar
A conformidade PDF/A é a única configuração que desliga todo o mecanismo, em vez de apenas limitá-lo. O HotPDF substitui silenciosamente JBIG2 por CCITT Grupo 4 no instante em que PDFACompliance não está vazio, em toda página, independentemente de AccumulateGlobalsAcrossPages ou qualquer outra coisa em JBIG2Options — uma escolha deliberada de conformidade, não um bug, mas significa que um perfil arquivístico e o compartilhamento de símbolos entre páginas são mutuamente exclusivos hoje. Seja qual for a configuração em que você pousar, decodifique o que escreveu antes de confiar nisso: carregue o arquivo de volta com LoadFromFile e extraia cada página por meio de ExtractLoadedImage, que resolve os globais compartilhados para você da mesma forma que qualquer leitor em conformidade faria, e compare o resultado contra seus bitmaps de origem
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;
O compartilhamento de dicionário entre páginas afeta apenas o lado de imagem bilevel de um documento. Se o mesmo pipeline também emitir páginas de texto geradas ao lado das digitalizações — folhas de rosto, páginas de índice, uma camada de texto OCR — object streams e xref streams atacam a outra metade do orçamento de tamanho de arquivo comprimindo a estrutura de documento que essas páginas adicionam. O compartilhamento de globais JBIG2 entre páginas vem como parte do componente HotPDF para Delphi e C++Builder, ao lado das opções JBIG2 por imagem e do restante do pipeline de compressão