一份五十頁的掃描合約每一頁都重複相同的字母表,但每張影像各自建立一個符號字典的 JBIG2 編碼器,會將這套字母表重新訓練五十次。原生 Delphi 與 C++Builder PDF 元件 HotPDF 可以改為在整份文件中累積一個共用符號字典,並將它提升為單一文件層級的 /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 組織中,文字區域區段可以透過區段標頭中的 referred-to-segment 欄位,將另一個區段命名為其符號來源,而這正是 HotPDF 所依靠的機制:全域串流承載一個大型符號字典,每一頁自己的 JBIG2 串流則縮減為頁面資訊區段,加上一個 referred-to 清單指向全域區段的文字區域區段。過去每頁各自完整的位元串流,現在變成位置與符號 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 雜湊作為查詢鍵,因此兩張恰好產生位元組完全相同全域資料的影像可以共用一個 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 會將目前批次自動溢寫至磁碟並開始新的全域群組,而不讓單一記憶體結構無限制成長。這兩項限制都不需要你撰寫任何程式碼,因為它們是自動退回機制,而不是需要攔截的例外
PDF/A 相容性是唯一會讓整個機制關閉,而不只是限制它的設定。只要 PDFACompliance 非空,HotPDF 就會在每一頁悄悄以 CCITT Group 4 取代 JBIG2,不受 AccumulateGlobalsAcrossPages 或 JBIG2Options 上其他設定影響 — 這是刻意的相容性選擇,不是錯誤,但表示目前封存設定檔與跨頁符號共用彼此互斥。無論你採用哪種設定,在信任結果前都應解碼所寫入的內容:使用 LoadFromFile 載入檔案,再透過 ExtractLoadedImage 取出每一頁,該方法會像任何相容讀取器一樣為你解析共用全域資料,然後將結果與來源點陣圖比較
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 文字層 — 物件串流與 xref 串流會壓縮這些頁面增加的文件結構,處理檔案大小預算的另一半。跨頁 JBIG2 全域資料是HotPDF 元件 Delphi 與 C++Builder 版本的一部分,並與每張影像的 JBIG2 選項及其餘壓縮流程一同提供