一份五十页的扫描合同在每一页都重复使用相同的字母表,但如果 JBIG2 编码器为每幅图像建立一个符号字典,就会把这套字母表分别重新训练五十次。HotPDF 这个原生 Delphi 和 C++Builder PDF 组件可以改为在整个文档中累积一个共享符号字典,并将其提升为单个文档级 /JBIG2Globals 流,因此每页自己的 JBIG2 流只需引用符号 ID,而不必存储自己的字母表副本
本文特意限定范围,只介绍 HotPDF 如何在内部构建跨页面共享机制——JBIG2 基础知识、CCITT 对比,以及 Lossless 与 LossyLevel 的取舍已经写在在 Delphi 中使用原生 JBIG2 二值压缩的配套文章中,本文假定你已经读过该文
为什么逐页 JBIG2 压缩仍会重复相同的成本
原因是调用之间不会传递状态。每当 HotPDF 的编码器为一幅图像建立符号字典时,该字典的作用域就限定在这一次 AddImage 调用中:形状匹配过程从零开始,页面上的每个字形都会被归类为新字形,生成的位图也会重新进行算术编码并存储。将同一个编码器用于五十页相同字体的页面时,它会心安理得地重复整个训练过程五十次,因为在它看来每一页都是彼此无关、只是外观相似的图像。逐页使用 UseSymbolDictionary 在单页上已经远胜于普通的通用区域编码,但它远未达到真正多页扫描所能实现的上限
HotPDF 如何跨页面共享一个符号字典
在 THPDFJBIG2Options 上启用 AccumulateGlobalsAcrossPages 后,HotPDF 会在文档生命周期内将一个符号字典保留在内存中,而不是每幅图像处理完就丢弃。后续每一页的字形在重新编码之前都会与这个持续累积的字典进行比对:已经存在的形状通过其符号 ID 重用,只有此前从未出现过的形状才会追加到字典并编码。该比较会复用 LossyLevel 在单页上使用的同一套容差逻辑——同一个字母的轻微噪声扫描仍会被视为匹配——因此累积器不会因为同一字形的像素级变化而无声地膨胀成每种变化一个字典条目。提取发生在比较之前并为比较提供输入:HotPDF 会遍历每页位图,通过针对黑色像素进行泛洪填充来提取连通形状,这和手动描摹墨迹块的思路相同,而真正参与运行字典比较的是这些提取出的形状,不是原始像素块
共享字典如何位于 /JBIG2Globals 流中
累积的字典会作为一个符号字典段写入 /JBIG2Globals 流,并固定在一个段编号上,使每一页都能指向同一个目标。在 ISO 32000-1 §7.4.7 定义的嵌入式 JBIG2 组织中,文本区域段可以通过段头中的被引用段字段指定另一个段作为其符号源,这正是 HotPDF 所依赖的机制:globals 流携带一个大型符号字典,而每页自己的 JBIG2 流则缩减为页面信息段和一个文本区域段,其被引用段列表回指 globals 段。过去每页都独立存在的位流现在变成一组简短的位置和符号 ID,每页都以相同的间接 /JBIG2Globals 对象为引用,而不是保存一份副本。HotPDF 自己的回归测试正是这样检查的:编码一个每页字形布局不同的短文档,重新加载它,然后统计文件中出现了多少个不同的 /JBIG2Globals 对象引用——一个文档、一个对象引用,无论有多少页面向其中贡献了符号
启用跨页面符号字典累积
这个开关位于配套文章介绍的同一个选项记录上,必须有四项设置彼此配合,累积功能才会真正启用
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;
这种组合并不是可有可无的装饰。二值压缩文章中介绍的外部编码器接口——通过 RegisterJBIG2EncoderBackend 注册、用于生产级压缩比的接口——围绕逐图像编码构建,而 HotPDF 自己的累积示例和回归测试总是将 AccumulateGlobalsAcrossPages 与 UseExternalEncoder := False 配对使用。请将其视为硬性要求,而不是建议:跨页面共享是原生编码器功能,已注册的外部后端根本不属于构建共享字典的路径
多页扫描实际能缩小多少
诚实的答案要从最初没有带来明显效果的方案说起。早期版本为 /JBIG2Globals 流增加了内容寻址缓存——它以流字节的 64 位 FNV-1a 哈希作为查找键,因此恰好生成字节完全相同 globals 数据的两幅图像可以共享一个 PDF 对象。根据真实输出测量,该缓存几乎没有帮助,因为 HotPDF 已有的整图重复检测在缓存有机会运行之前就已经合并了字节完全相同的图像。这个教训是,只有当两幅真正不同的页面图像仍能共享一个持续增长的字典时,流级去重才会产生收益,而真正的跨页面累积正是实现这一点的机制
对于这个更困难的场景,HotPDF 自己的工程估算是:对于由一种重复字体构成的典型多页扫描,相比单独进行流级去重,还能额外缩小约 30% 到 60%;具体范围取决于文档视觉词汇中实际重复的部分有多少,因为充满独特图表的页面没有可供字典重用的内容。请把这个数字视为设计目标,而不是任何特定输入的保证,并测量你自己的文档,不要依赖单个数字。随 HotPDF 提供的 JBIG2Benchmark 示例正是为此而存在:它以四种不同方式编码同一份多页扫描,并打印每种配置生成的文件大小,因此比较针对的是你自己的扫描组合,而不是合成数据
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.
跨页面累积的限制
累积字典最多容纳 4096 个符号,这与逐图像原生编码器在单页上已经执行的上限相同。如果在文档处理中途超过这个限制,HotPDF 不会抛出异常或中止运行:累积器会拒绝新字形,而引入该字形的页面会自动回退到独立的逐图像编码,因此文档仍能正确生成——只是超过上限的那些页面不再获得跨页面节省。另一个保护机制监视的是总大小而不是符号数量:当累积字典的符号总宽度超过 131071 像素后,HotPDF 会将当前批次写入磁盘并自动开始一个新的 globals 组,而不是让单个内存结构无限增长。这两个限制都不需要你编写任何代码,因为它们是自动回退而不是需要捕获的异常
PDF/A 合规性是唯一会关闭整个机制、而不只是限制它的设置。当 PDFACompliance 非空时,HotPDF 会在每一页上静默地用 CCITT Group 4 替代 JBIG2,不受 AccumulateGlobalsAcrossPages 或 JBIG2Options 上其他设置的影响——这是有意为之的合规选择,而不是错误,但这意味着存档配置与跨页面符号共享目前互相排斥。无论你采用哪种配置,都应在信任输出前解码验证:使用 LoadFromFile 重新加载文件,并通过 ExtractLoadedImage 提取每一页,该方法会像任何符合规范的阅读器一样为你解析共享 globals,然后将结果与源位图进行比较
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;
跨页面字典共享只影响文档中的二值图像部分。如果同一条管线还会在扫描页旁生成文本页——封面、索引页或 OCR 文本层——对象流和交叉引用流会压缩这些页面增加的文档结构,从而处理文件大小预算的另一半。跨页面 JBIG2 globals 是面向 Delphi 和 C++Builder 的 HotPDF Component 的一部分,与逐图像 JBIG2 选项及其余压缩管线一同提供