技術記事

JBIG2 エンコーダーバックエンドと Free Pascal リンカー

PDFlibPas は 2 値画像を JBIG2 として、2 つの異なるバックエンド経由でエンコードできます。1 つは常に存在するネイティブの Object Pascal MMR エンコーダーです。もう 1 つはスキャンテキストで出力を大幅に小さくする外部のシンボル辞書エンコーダーで、こちらはオプションです。存在すること自体が、プロジェクトがバックエンドユニットをリンクしていることに依存します。この違いは、この機能に関する最も一般的な驚きの源です。だから最初に述べておく価値があります。DefaultJBIG2EncodeOptions は既定で外部エンコーダーを要求し、バックエンドユニットがリンクされていないとき、この要求は黙って Pascal MMR 経路へフォールバックします

Delphi と C++Builder では外部バックエンドは事前ビルドされた静的オブジェクトの集合です。Free Pascal では DLL にする必要がありました。その結論に至る道のりは、C++ オブジェクトを Free Pascal プログラムへリンクしようとしたことのある人なら誰にでも役立つリンカーの物語です

登録こそが契約

バックエンドユニットは初期化セクションから RegisterJBIG2EncoderBackend を呼び出すことで自分自身を登録します。呼び出し側がこれを要求するのは、値 4 を持つオプションビット PDF_JBIG2_OPTION_EXTERNAL_ENCODER 経由か、拡張画像エントリポイントの UseExternalEncoder パラメータ経由です。ライブラリのアンブレラユニットは意図的にバックエンドユニットを引き込みません。大きなオブジェクト集合を抱えるかどうかは各プロジェクトの判断であるべきだからです。たとえば C++Builder のツリーでは、必要とするプロジェクトが明示的に含めています

呼び出し側にとっての帰結は、外部エンコーダーの要求は優先指定であって保証ではないということです。ユニットを忘れたビルドは、エラーではなくより大きなファイルを生み出します。より良いエンコーダーを要求するほど出力サイズが重要なら、それを実際に得られたか確認するほど重要なはずです

PDFlibPas の JBIG2 エンコード要求フロー。バックエンドユニットがない場合、外部エンコーダーの優先指定が黙ってネイティブ Pascal MMR 経路へフォールバックする様子。
外部シンボル辞書エンコーダーの要求は優先指定です。リンクされていれば出力は縮み、されていなければ Pascal MMR 経路がより大きなファイルのまま黙って走ります
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;

ユニットのコンパイルは 2 行の変更。シンボルが本当の仕事

バックエンドユニット自体を Free Pascal の下でコンパイルできるようにするには、正確に 2 つの変更で足りました。アセンブラ方言の設定と、レコードベースのフォーマット設定コンストラクタをグローバルな既定変数への置き換えです。素直な Pascal が 2 つのコンパイラ間でどれほど移植しやすいか、その妥当な反映です

シンボル側が本当の仕事でした。オブジェクト集合は 176 個の C シンボルを参照します。このうち 128 個はユニット内にすでに Pascal 実装があり、エクスポート名を付けるだけで済みました。Delphi は関数名をそのままシンボル名として使いますが、Free Pascal では明示的な public 名宣言が必要だからです。27 個は JPEG 2000 コーデックと共有されており、ちょうど 1 か所からエクスポートしなければなりませんでした。2 回定義すると、両方をリンクするプログラムが壊れるからです。残る 21 個はプラットフォームと C ランタイムのエントリ、すなわち 16 個の Win32 ファイル関数と一握りの標準ライブラリ呼び出しで、新しい互換ユニットへ入れました

どれも概念的に難しいものではなく、リンカーが試みてくれるようになるまでに必要な作業ばかりです。そして止まったのはリンカーでした

3 つのリンク経路、3 つの袋小路

Free Pascal の内部リンカーはオブジェクトファイルを読めません。これらは連想 COMDAT セクションを出力するコンパイラが生成したものであり、内部リンカーはこれをサポートしないと報告するからです。これはきっぱりした拒否であり、警告ではありません

外部リンカーへ切り替えることが答えに見えました。Free Pascal に同梱される binutils リンカーは、このアーカイブにセクションガベージコレクションを適用しようとしてそのままクラッシュします。そしてそのフラグは、Free Pascal が 64 ビット Windows ターゲット向けに渡す固定パラメータセットの一部であるため、コマンドラインから取り除けません。これを抑制する文書化されたスイッチは、この経路では無視されます。代わりにはるかに新しい binutils を用意すると、今度は違う形で失敗します。Free Pascal のリンクスクリプトをまったく処理できず、スクリプトなしでは空の出力を、ありでは再配置エラーの壁を生み出します

途中で見つかった境界は、リンカー問題に遭遇することが一度もなくても知っておく価値があります。外部リンカーはオブジェクトファイルのパスを、ソースツリーではなく実行ファイル出力ディレクトリからの相対で解決します。そのため相対の include-object 指示が機能するのは、出力ディレクトリがたまたまコンパイル時の作業ディレクトリと一致するときだけです。ライブラリが利用者のプロジェクトについてこれを仮定することはできず、それ自体が、ばらのオブジェクトよりもリンク済みライブラリを好む理由になります

Free Pascal の下で C++ JBIG2 エンコーダーオブジェクトのリンクに失敗した 3 経路と、解決となった 2 つのフラットな C エントリポイントを公開する DLL。
COMDAT セクションは内部リンカーを打ち負かし、両方の外部リンカーも失敗します。そのため C++ エンコーダーは 1 つの DLL として出荷され、バックエンドユニットが動的にバインドします

別の C++ コンパイラがなぜ役に立たないのか

次の自明な発想は、Free Pascal が読めるオブジェクトを生むコンパイラで C++ 側をビルドし直すことです。これも機能しません。そして理由はスイッチの問題ではなく根本的なものです。テンプレートを含む最小の C++ 翻訳単位は、コード生成機能をすべて切ってコンパイルしても、弱い外部シンボルを出力します。テンプレートとインラインの実体化が構造上これを生むからです。Free Pascal はこのシンボルクラスをきっぱり拒否します。逆方向も失敗します。同じ COMDAT セクション処理のために、主流の C++ リンカーはもう片方のコンパイラのオブジェクトを取り込めません

つまり、どの利用可能な経路を通っても、C++ コードをオブジェクトとして Free Pascal へ届けることはできません。DLL としてなら届けられます。実際そうしました。エンコーダーとその画像処理依存は、2 つのフラットな C エントリポイントを公開する 1 つのライブラリへビルドされ、Free Pascal のバックエンドユニットがそれらを動的にバインドし、静的バックエンドとまったく同じように自分自身を登録します。Delphi と C++Builder の経路にはまったく触れていません。これが正しい着地です。1 つのツールチェーン上の移植性の問題が、すでに機能しているツールチェーンを乱すべきではありません

確実に足を踏ませるのは極性

Windows の 2 値ビットマップと JBIG2 エンコーダーの間には、型システムでは捕捉できない慣習の不一致があります。1 ピクセル 1 ビットの DIB のスキャンラインは、セットされたビットを白として扱います。エンコーダーはセットされたビットを黒として扱います。スキャンラインをそのまま渡すと、ページの写真ネガの、完全に有効な JBIG2 ストリームが手に入ります

1 ビット DIB と JBIG2 の極性の慣習。セットビットはスキャンラインでは白、エンコーダーでは黒であり、全バイト反転で解決する。
同じバイト、逆の意味です。各バイトを反転しなければ、エンコーダーは写真ネガの有効な JBIG2 ストリームを生み出します
// 1 ビット DIB:セットビットは白を意味する。JBIG2 エンコーダー:
// セットビットは黒を意味する。投入時に全バイトを反転する
for I := 0 to RowBytes - 1 do
  Row[I] := Row[I] xor $FF;

検証の方法は、修正そのものと同じくらい重要です。圧縮ストリームの長さの比較は何も教えてくれません。ネガ画像も似たようなサイズに圧縮されるからです。ページを見ても、あからさまに反転していないことしか証明できません。信頼できる確認は、ネイティブ Pascal と外部の両エンコード経路の出力を PNG へレンダリングしてバイト単位で比較することです。両エンコーダーとも同じソース画像に対してロスレスなので、完全一致以外のものはどちらかのバグです。この比較は今や恒久的なリグレッションテストであり、2 つの実装が厳密に一致するはずのときに必ず組み込む価値のある種類の表明です

どちらのバックエンドを使うか

一般的な 2 値コンテンツ、ディザリングされたハーフトーン、線画、混在グラフィックスには、ネイティブの Pascal MMR エンコーダーで十分であり、配備コストもありません。スキャンテキスト、すなわち JBIG2 が設計されたケースには、外部のシンボル辞書エンコーダーがサイズ削減の担い手です。繰り返されるグリフ形状を辞書へ括り出し、出現ごとに再エンコードしないからです。スキャン文書のアーカイブを制作しているなら、この違いはストレージ計画を変えるほど大きいものです

上流の問い、すなわち 2 値画像をそもそもどう生成するかも、出力サイズについて同じくらい重要です。領域ベースのモノクロレンダリングはモノクロ領域レンダリングの記事で、文書全体のサイズ戦略はPDF ファイルサイズの最適化とフォントサブセット化の記事で扱っています。繰り返しページを含むスキャン集合には、より良い圧縮よりも重複排除が効くことが多く、これは知覚画像の重複排除の記事の主題です。プラットフォームごとのツールチェーンとバックエンドの対応状況は、losLab PDF Developer Library の製品ページに一覧があります