技術記事

HotPDF on Free Pascal:Deflate、AES とコーデックの限界

HotPDF は Free Pascal 3.2.2 と Lazarus の下でコンパイルおよび実行できます。この移植の正直な要約は 2 文です。文書の作成、ロード、保存、圧縮、展開、暗号化、復号はすべて Pascal 専用バックエンド上で機能するため、Lazarus アプリケーションは C 依存なしに本物の PDF を生成し消費できます。オプションのネイティブ画像コーデックは機能しません。事前ビルドされた Win64 オブジェクトが、どちらの Free Pascal リンカーも取り込めない COFF 系統を使うためです。そのツールチェーンではエントリポイントは、フェイルクローズするスタブへ解決されます

HotPDF の Free Pascal 機能マップ。フェイルクローズする画像コーデックスタブの隣に、機能する Pascal 製 deflate、AES、文書バックエンド。
文書、圧縮、暗号化の機能は Pascal 専用バックエンド上で走り、ネイティブ画像コーデックはフェイルクローズするスタブへ解決されます

「コンパイルできる」から「動く」へ至るには、特定の修正の集合が必要でした。そのどれもが、Free Pascal へ移行する他のあらゆる Delphi コードベースを必ず捕まえる罠です。痛みの順に書き留める価値があります

ユニットがコンパイルできることは、なぜ何も証明しないのか

Pascal ユニットは、何の役にも立たないシンボルを参照したまま、コンパイラを満足させられるからです。113 個すべてのライブラリユニットが Free Pascal の下できれいにビルドできた時点で、アーカイブコンテナハンドラは本当に機能していました。CBZ を開いて PDF へ変換するスモークテストで検証済みです。一方、XFA フォームのフラット化はまったく機能していませんでした。フラット化は圧縮された /XFA パケットストリームを展開する必要があり、deflate エントリポイントがまだスタブだったからです。ビルド出力のどこにも、この 2 つのケースを区別するものはありませんでした

ここから出た規則は短いものです。機能が新しいツールチェーンで動くとリリースノートに書く前に、そのツールチェーン上で機能をエンドツーエンドで試すランタイムプローブを書いてください。コンパイルの網羅は前提条件であって、決して証拠ではありません。移植が何をカバーするかの全貌は、Free Pascal と Lazarus の Win64 サポートノートにあります

cdecl スタブ内の raise は呼び出し側へ届かない

症状があまりに紛らわしいため、これは独立したセクションに値します。スタブユニットは、静的ライブラリがするように C エントリポイントを公開します。スタブは次のように見えます

// 一見もっともらしい。実際はそうではない
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Win64 向け Free Pascal では、この例外は呼び出し側へ伝播しません。これを目にする try..except ハンドラは存在しません。このように宣言された cdecl 境界をまたぐ巻き戻しは、Pascal の例外フレームを運ばないからです。プロセスは終了コード 217 で終了します。アプリケーション側からは、エラーもなく、メッセージもなく、ログ行もなく、消えるプログラムがあるだけです。これは誤った答えよりも厳密に悪いことです。誤った答えなら処理できるからです

cdecl スタブ内で投げられた例外が Free Pascal プロセスを終了コード 217 で終わらせる理由と、Pascal エントリポイントをゲートする修正方法。
Pascal の例外フレームは cdecl 境界をまたいて巻き戻せないため、プロセスは黙って死にます。修正は、スタブに到達する前の段階でゲートすることです

魅力的な修正は、スタブに代わりに失敗コードを返させることです。inflate についてはそれが正しい。zlib には明確に定義されたエラー戻り値があるからです。しかし一般には誤りです。ゼロを返す jpeg_read_header のスタブは、誰も初期化していない構造体で処理を続けるよう呼び出し側に告げます。恒久的な修正は、C 形のスタブの内側ではなく Pascal エントリポイントでゲートすることです。その API がすでに持つ失敗の規約を何であれ使います

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // スタブに到達する前に拒否する。cdecl をまたぐ例外ではなく、
  // この API 自身の失敗規約を使う
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib は zlib ではない。その差は 2 つの文書クラス

Free Pascal で利用できる Pascal 製 deflate 実装は 2 つのフレーミングを扱います。zlib ラッパーと生の deflate です。gzip フレーミングは扱えません。zlib が windowBits 値 16 から 31 で選ぶものです。値 32 から 47 が選ぶ自動検出モードも扱えません。HotPDF は両方を必要とします。安全な SVG 取り込み経路は 31 を要求し、ローダーはストリームのフレーミングが曖昧なときに 47 を要求するフォールバックの階段を持っています。どちらかを省くと、文書の一族全体が開かなくなり、そのデコードエラーは、欠けているフレーミングではなくストリームを指します

paszlib と zlib の windowBits カバー範囲の比較。HotPDF の SVG 取り込みとローダーフォールバックに必要な gzip フレーミングと自動検出の範囲が欠けている。
paszlib は zlib ラッパーと生の deflate を扱いますが、HotPDF には windowBits 31 と 47 も必要です。そのためシムは gzip フレーミングを自前で供給しなければなりません

さらに鋭い非互換性がもう 1 つあります。paszlib が宣言する z_stream レコードは、C のものと同じメモリレイアウトを持っていません。その msg フィールドはポインタではなく short string であり、total_intotal_out は、C ABI がマシンワードであるところを 64 ビットにしています。したがって呼び出し側のレコードをそのまま渡すことはできません。機能する配置は、公開レコードがすでに予約している state ポインタの背後に paszlib の状態を置き、呼び出しごとに公開フィールドを出入りへコピーすることです。gzip の CRC と 8 バイトの長さトレーラも同じシムレイヤーで処理します。フレーミングの決定をすでに担っている場所であり、ここが自然な置き場所です

型なし var パラメータへの動的配列の受け渡し

これは今、あなたのコードに座っている可能性が最も高いバグです。動的配列を型なしの var パラメータへ渡すと、呼び出された側が受け取るのは配列変数のアドレスです。つまりポインタのアドレスであり、ペイロードのアドレスではありません。したがってそこへの読み込みは、変数自体と、その隣に座る何かを上書きします

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // 誤り:FBuffer 変数のアドレスを渡してしまう
  FStream.Read(FBuffer, Length(FBuffer));

  // 正しい:最初のペイロードバイトのアドレスを渡す
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

Delphi では誤った形式がしばしば動いているように見えます。破壊するのが、その後誰も読まない隣接スタックスロットだからです。Free Pascal では同じ行が最初の使用でセグメンテーション違反になります。目視での発見がこれほど難しいのは、静的配列にはこの問題がないからです。静的配列変数はそれ自体がペイロードであり、数百行先の宣言次第で、同じファイル内の両方の書き方が正しくなり得ます

System.Zip なしの ZIP コンテナ

Free Pascal には RTL の zip ユニットに相当するものがなく、利用できる代替は API 面が異なる上に、古いコンテナ形式が今も使うレガシー暗号化をサポートしません。そのため小さなライブラリ内リーダーのほうが、適応させるより短く済みました。時間を取られ、誤りやすいフォーマットの細部が 2 つあります

1 つ目は暗号化ヘッダーのチェックバイトです。12 番目のバイトは通常 CRC の上位バイトですが、汎用フラグのビット 3 が立っているとき、つまりサイズが末尾のデータディスクリプタにあり CRC がまだ判明していないときは、チェックバイトは代わりに更新時刻の上位バイトから来ます。CRC 形式だけを実装すると、ストリーミングモードで書かれたすべてのアーカイブが、正しいパスワードを拒否します。2 つ目は ZIP64 拡張フィールドです。3 つの 64 ビットフィールドは固定順で現れますが、対応する 32 ビットフィールドが飽和したときにだけ書き込まれます。固定オフセットで読むと、テストしたアーカイブでは動き、次のアーカイブで失敗します。どの 32 ビットフィールドが飽和しているかに応じて位置ベースで解析してください

知っておく価値のある 1 つの便利機能があります。Free Pascal の展開ストリームは、zlib ヘッダーをスキップする第 2 コンストラクタ引数を取ります。ZIP エントリは生の deflate を格納するため、まさにこれが必要なものです。この経路はライブラリの zlib シムにまったく触れないため、欠けている C バックエンドの影響を受けません

LCL の下でのカラーグリフの透過

ラスタライズされたカラーグリフのアルファチャネルを読むことは、直接の翻訳が存在しない唯一のグラフィックス細部です。LCL の PNG クラスにはアルファを公開するスキャンラインアクセサがなく、PNG をビットマップへ代入するとアルファは捨てられます。その結果、カラー絵文字は完全に不透明なまま到着し、背後に黒い箱と合成されます。機能する経路はインターフェース画像です。PNG から作成し、カラーアクセサを通じてピクセルを読みます。コンポーネントが 16 ビットであり、バイトになるには 8 ビットの右シフトが必要なことを忘れずに。このサーフェスは自然な上から下の行順を使うため、VCL のスキャンラインコードが必要とする Height - 1 - Y の反転は移植するのではなく取り除かなければなりません

バグ報告の前の 2 つのビルドシステムメモ

フルビルドが、$crc 接尾辞と 16 進値で終わる名前の未定義シンボルで失敗することがときどきあります。この接尾辞はパラメータ型から計算され、1 つのビルドが同じパスで 2 つの異なるインターフェースバージョンに対してユニットをコンパイルすると一致しなくなります。ビルドの再実行で解消されます。シグネチャは誤っていません

2 つ目として、Free Pascal 3.2.2 には無名メソッドがありません。そのためライブラリがクロージャで並列パイプラインを配線していた場所は、Free Pascal ビルドでは決定論的な逐次フォールバックを取ります。出力は同一ですが、スループットは同一ではありません。並列ページレンダリングに依存しているなら、当面 Delphi に留まる理由になります。パイプラインの設計は並列レンダリングパイプラインの記事に述べています。画像コーデックの状況は、ツールチェーンの選択が速度だけでなく機能を変えるもう 1 つの場所です。Lazarus 配備では画像フォーマットをそれに応じて計画すべきです。現在のツールチェーン別マトリックスは、HotPDF Delphi PDF component の製品ページにあります