Um contrato digitalizado de cinquenta páginas repete o mesmo alfabeto em todas as páginas, mas um codificador JBIG2 que construa um dicionário de símbolos por imagem volta a treinar esse alfabeto cinquenta vezes em separado. O HotPDF, o componente PDF nativo para Delphi e C++Builder, pode em vez disso acumular um único dicionário de símbolos partilhado ao longo de todo o documento e promovê-lo a um único fluxo /JBIG2Globals ao nível do documento, pelo que o fluxo JBIG2 próprio de cada página passa apenas a referenciar identificadores de símbolo em vez de armazenar a sua própria cópia do alfabeto
Este texto mantém-se deliberadamente restrito e cobre apenas como o HotPDF constrói internamente essa partilha entre páginas — os fundamentos do JBIG2, a comparação com o CCITT, e as compensações entre Lossless e LossyLevel já vivem em o artigo complementar sobre compressão bilevel nativa JBIG2 em Delphi, que este texto pressupõe já ter sido lido
Porque continua a compressão JBIG2 por página a repetir o mesmo custo?
A resposta é que nada transporta estado entre chamadas. Sempre que o codificador do HotPDF constrói um dicionário de símbolos para uma imagem, esse dicionário fica limitado a essa única chamada AddImage: a passagem 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 de novo. Alimente o mesmo codificador com cinquenta páginas compostas no mesmo tipo de letra e ele repete de bom grado toda essa passagem de treino cinquenta vezes, porque do seu ponto de vista cada página é uma imagem sem relação que calha de se parecer com as outras. O UseSymbolDictionary por página já supera por larga margem uma codificação de região genérica plana numa única página, mas atinge o seu teto bem antes do limite que uma digitalização real de várias páginas deixa por explorar
Como partilha o HotPDF um único dicionário de símbolos entre páginas?
Ativar AccumulateGlobalsAcrossPages em THPDFJBIG2Options faz com que o HotPDF mantenha um dicionário de símbolos vivo em memória durante toda a vida do documento, em vez de o descartar após 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á exista é reutilizada pelo seu identificador de símbolo, e só uma forma nunca antes vista é adicionada e codificada no dicionário. A comparação reutiliza a mesma lógica de tolerância que LossyLevel aplica numa única página — uma digitalização ligeiramente ruidosa da mesma letra continua a contar como correspondência — pelo que o acumulador não incha silenciosamente numa entrada de dicionário por cada variação ao 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 através de preenchimento por inundação (flood fill) sobre os pixels pretos, a mesma ideia de traçar manchas de tinta à mão, e são essas formas extraídas, e não blocos de pixel em bruto, que são comparadas com o dicionário corrente
Como se encaixa o dicionário partilhado dentro de um fluxo /JBIG2Globals
O dicionário acumulado é escrito como um único segmento de dicionário de símbolos dentro do fluxo /JBIG2Globals, mantido a um número de segmento fixo para que todas as páginas possam apontar para o mesmo alvo. Dentro da organização JBIG2 incorporada 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 através do campo de segmento referido no cabeçalho de segmento, e é exatamente esse o mecanismo em que o HotPDF se apoia: o fluxo de globais transporta o único grande dicionário de símbolos, e o próprio fluxo JBIG2 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 para o segmento de globais. O que costumava ser um fluxo de bits autocontido por página torna-se uma pequena lista de posições e identificadores de símbolo, e cada página construída desta 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: codificar um documento curto onde cada página tem um layout de glifos diferente, recarregá-lo, e contar quantas referências distintas a objetos /JBIG2Globals aparecem no ficheiro — um documento, uma referência de objeto, independentemente de quantas páginas contribuíram símbolos para ele
Ativar a acumulação de dicionário de símbolos entre páginas
O interruptor situa-se no mesmo registo de opções abordado no artigo complementar, e precisa de quatro definições concordantes entre si antes de a acumulação efetivamente 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 emparelhamento não é decoração opcional. A costura de codificador externo descrita no artigo sobre compressão bilevel — a que se regista através de RegisterJBIG2EncoderBackend para taxas de nível de produção — é construída à volta da codificação por imagem, e as próprias demonstrações de acumulação e testes de regressão do HotPDF emparelham sempre AccumulateGlobalsAcrossPages com UseExternalEncoder := False. Trate isto como um requisito rígido e não como uma sugestão: a partilha entre páginas é uma funcionalidade do codificador nativo, e um backend externo registado simplesmente não faz parte do caminho que constrói o dicionário partilhado
Quanto mais pequena fica efetivamente uma digitalização de várias páginas?
A resposta honesta começa por aquilo que, à partida, não fez diferença. Uma versão anterior acrescentou uma cache indexada por conteúdo para fluxos /JBIG2Globals — uma pesquisa indexada por um hash FNV-1a de 64 bits dos bytes do fluxo, para que duas imagens que calhassem de produzir dados de globais byte a byte idênticos pudessem partilhar um único objeto PDF. Medida contra resultados reais, essa cache ajudou muito pouco, porque a deteção de duplicados de imagem inteira já existente no HotPDF já colapsava imagens byte a byte idênticas antes de a cache sequer ter oportunidade de correr. A lição foi que a deduplicação ao nível do fluxo só compensa assim que duas imagens de página genuinamente diferentes ainda conseguem partilhar um dicionário em crescimento, que é o que a verdadeira acumulação entre páginas proporciona
Para esse caso mais difícil, a própria estimativa de engenharia do HotPDF situa a poupança adicional em cerca de 30 a 60 por cento mais pequena do que a deduplicação ao nível do fluxo consegue sozinha alcançar, para uma digitalização típica de várias páginas construída a partir de um tipo de letra recorrente — o intervalo desloca-se consoante quanto do vocabulário visual do documento efetivamente se repete, uma vez que uma página cheia de diagramas únicos não dá ao dicionário nada para reutilizar. Trate isto como um objetivo de design e não como uma garantia para qualquer entrada específica, e meça os seus próprios documentos em vez de confiar num único número. A demonstração JBIG2Benchmark distribuída com o HotPDF existe precisamente para esse fim: codifica a mesma digitalização de várias páginas de quatro formas diferentes e imprime o tamanho de ficheiro resultante para cada configuração, pelo que a comparação corre contra a sua própria mistura de digitalizações, e não contra 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 a acumulação entre páginas atinge os seus limites
O dicionário acumulado está limitado a 4096 símbolos, o mesmo teto que o codificador nativo por imagem já impõe numa única página. Ultrapassar esse limite a meio do documento não faz o HotPDF levantar uma exceção nem abortar a execução: o acumulador recusa o novo glifo, e a página que o introduziu recua automaticamente para codificação independente por imagem, pelo que o documento continua a sair correto — apenas se deixa de obter a poupança entre páginas para as páginas que ultrapassaram o teto. Uma segunda salvaguarda vigia 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 para disco e inicia automaticamente um novo grupo de globais, em vez de deixar uma estrutura em memória crescer sem limite. Nenhum dos dois limites exige qualquer código do seu lado, uma vez que ambos são recuos automáticos e não exceções a apanhar
A conformidade PDF/A é a única definição que desliga todo o mecanismo em vez de apenas o limitar. O HotPDF substitui discretamente o JBIG2 por CCITT Group 4 assim que PDFACompliance não está vazio, em todas as páginas, independentemente de AccumulateGlobalsAcrossPages ou de qualquer outra coisa em JBIG2Options — uma escolha de conformidade deliberada, não um bug, mas significa que um perfil de arquivo e a partilha de símbolos entre páginas são hoje mutuamente exclusivos. Seja qual for a configuração escolhida, descodifique o que escreveu antes de confiar nisso: carregue o ficheiro de volta com LoadFromFile e extraia cada página através de ExtractLoadedImage, que resolve os globais partilhados por si, da mesma forma que qualquer leitor conforme faria, e compare o resultado com os 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;
A partilha de dicionário entre páginas só toca no lado da imagem bilevel de um documento. Se o mesmo pipeline também emitir páginas de texto geradas a par das digitalizações — folhas de rosto, páginas de índice, uma camada de texto OCR — os fluxos de objetos e os fluxos xref atacam a outra metade do orçamento de tamanho de ficheiro ao comprimir a estrutura de documento que essas páginas acrescentam. A partilha de globais JBIG2 entre páginas é distribuída como parte do componente HotPDF para Delphi e C++Builder, a par das opções JBIG2 por imagem e do resto do pipeline de compressão