PDFlibPas 可以透過兩種不同的後端把雙階影像編碼為 JBIG2。一個是永遠存在的原生 Object Pascal MMR 編碼器;另一個是在掃描文字上產出明顯更小輸出的外部符號字典編碼器,而它是可選的:專案必須連結後端單元,它才存在。這個區別正是這個功能最常見的意外來源,所以值得先說清楚:DefaultJBIG2EncodeOptions 預設就會請求外部編碼器,而當後端單元未被連結時,該請求會無聲回退到 Pascal MMR 路徑
在 Delphi 與 C++Builder 上,外部後端是一組預先建置的靜態目標檔。在 Free Pascal 上它必須變成 DLL,而走到這個結論的過程,是一個對任何嘗試把 C++ 目標檔連結進 Free Pascal 程式的人都有用的連結器故事
註冊就是契約
後端單元從自己的初始化區段呼叫 RegisterJBIG2EncoderBackend 完成自我註冊。呼叫端透過選項位元 PDF_JBIG2_OPTION_EXTERNAL_ENCODER(值為 4)或延伸影像進入點的 UseExternalEncoder 參數來請求它。程式庫的彙整單元刻意不引入後端單元,因為攜帶一大組目標檔應該由每個專案自行決定;例如在 C++Builder 樹中,它由需要它的專案明確引入
對呼叫端而言,後果是:請求外部編碼器是一種偏好,不是保證,忘記連結單元的建置會產出更大的檔案而不是錯誤。如果輸出大小重要到值得要求更好的編碼器,那它也重要到值得確認您真的拿到了
uses
PDFlibrary,
{$IFDEF FPC}
PDFlibJBIG2EncDLL; // Free Pascal 的動態後端
{$ELSE}
PDFlibJBIG2EncC; // Delphi / C++Builder 的靜態目標檔組
{$ENDIF}
var
Pdf: TPDFlib;
ImageId: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.NewPage;
// Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
// BlackDotSize, LossyLevel
ImageId := Pdf.AddImageJBIG2FromFileEx('scan-page-1.tif',
0, 1, 1, 0, 0, 0);
if ImageId = 0 then
raise Exception.Create('JBIG2 encoding failed');
Pdf.SaveToFile('archive.pdf');
finally
Pdf.Free;
end;
end;
編譯單元只改了兩行,符號才是真功夫
讓後端單元本身在 Free Pascal 下編譯通過,正好只需要兩處改動:設定組譯器方言,以及把基於記錄的格式設定建構子換成全域預設變數。這相當忠實地反映了平實的 Pascal 在兩個編譯器之間有多可移植
符號那一側才是真正的工作。這組目標檔引用 176 個 C 符號。其中 128 個在單元內已有 Pascal 實作,只需附上匯出名稱,因為 Delphi 用函式名稱當符號名稱,而 Free Pascal 要求明確的公開名稱宣告。27 個與 JPEG 2000 編解碼器共用,必須只從一處匯出,因為重複定義會破壞任何同時連結兩者的程式。剩下的 21 個是平台與 C 執行階段項目:十六個 Win32 檔案函式加上少數幾個標準程式庫呼叫,它們進了一個新的相容單元
這些沒有一項在概念上困難,但連結器願意嘗試之前,每一項都必須做完。而連結器就是一切卡住的地方
三條連結路線,三個死胡同
Free Pascal 內部連結器讀不了這些目標檔,因為它們由會產出關聯式 COMDAT 區段的編譯器產生,而內部連結器回報不支援這種區段。那是斷然拒絕,不是警告
改用外部連結器看似答案。Free Pascal 隨附的 binutils 連結器在對這個封存檔套用區段垃圾收集時直接當掉,而那個旗標是 Free Pascal 為 64 位元 Windows 目標傳遞的固定參數集的一部分,無法從命令列移除;文件記載用於抑制它的開關在這條路徑上被忽略。改供應新得多的 binutils 則以另一種方式失敗:它完全無法處理 Free Pascal 的連結指令碼,沒有指令碼時產出空輸出,有指令碼時產出一整牆重定位錯誤
過程中發現的一個邊界值得知道,即使您永遠不會踩到連結器問題。外部連結器以執行檔輸出目錄、而不是原始碼樹為基準解析目標檔路徑,所以相對路徑的引入目標檔指示只有在輸出目錄碰巧等於編譯期工作目錄時才有效。程式庫不能對使用者的專案做這種假設,這本身就足夠成為偏好連結程式庫而非散裝目標檔的理由
為什麼換一個 C++ 編譯器也沒用
下一個顯而易見的想法,是用一個 Free Pascal 讀得懂其目標檔的編譯器重建 C++ 側。這也行不通,而且原因是根本性的,不是開關問題。一個包含樣板的極小 C++ 編譯單元,即使關掉所有程式碼產生功能,仍然會產出弱外部符號,因為樣板與 inline 具現化在構造上就會產生它們。Free Pascal 斷然拒絕這類符號。反方向也失敗:主流 C++ 連結器無法消化另一個編譯器的目標檔,原因同樣是那套 COMDAT 區段處理
所以 C++ 程式碼無法以目標檔形式透過任何現有路線交付給 Free Pascal。它可以以 DLL 交付,實際上也是這麼做的:編碼器與其影像處理相依被建成一個暴露兩個平面 C 進入點的程式庫,Free Pascal 後端單元動態繫結這些進入點,並且完全像靜態後端那樣自我註冊。Delphi 與 C++Builder 路徑完全未被觸碰,這是正確結果;一個工具鏈上的可移植性問題,不應該攪亂已經正常運作的工具鏈
極性是唯一會咬您一口的東西
在 Windows 雙階點陣圖與 JBIG2 編碼器之間,存在一個任何型別系統都抓不到的慣例錯位。每像素一位元的裝置無關點陣圖掃描線,把值為 1 的位元視為白;編碼器把值為 1 的位元視為黑。把掃描線原樣交過去,您會得到一份完全有效的 JBIG2 串流,內容卻是您頁面的底片負片
// 1 位元 DIB:位元為 1 表示白。JBIG2 編碼器:位元為 1
// 表示黑。送入之前把每個位元組反轉
for I := 0 to RowBytes - 1 do
Row[I] := Row[I] xor $FF;
驗證方法與修復本身同樣重要。比較壓縮串流長度什麼也告訴不了您,因為負片影像壓縮後大小相近;看一眼頁面只能證明它不是明顯反轉。可靠的檢查是把兩條編碼路徑——原生 Pascal 與外部——的輸出都渲染成 PNG,再逐位元組比較:兩個編碼器對同一張來源影像都是無損的,所以只要不是完全一致,就是其中之一有錯誤。這個比較現在是永久回歸測試,也是每當兩個實作被要求完全一致時,都值得建立的斷言類型
該用哪個後端
對一般雙階內容——抖動半調、線稿、混合圖形——原生 Pascal MMR 編碼器已經足夠,而且沒有部署成本。對掃描文字(JBIG2 為之而生的場景),外部符號字典編碼器才是體積縮減所在,因為它把重複的字形形狀分解進字典,而不是每次出現都重新編碼。如果您正在產出掃描文件的歸檔,這個差異大到會改變儲存規劃
上游問題——雙階影像最初是怎麼產出的——對輸出大小同樣要緊;基於區域的單色渲染在單色區域渲染文章中涵蓋,整份文件的體積策略在PDF 檔案體積最佳化與字型子集中討論。對頁面重複的掃描集,去重複往往勝過更好的壓縮,那是感知式影像去重複的主題。各平台的工具鏈與後端可用性列於 losLab PDF Developer Library 產品頁