PDFlibPasはlibtiffバインディングではなく手書きのObject PascalパーサーでTIFFをデコードしており、バージョン3.534.1では、このパーサーが入力を拒否する箇所をまさに強化しました。BigTIFFのマジック43を名指しで拒否し、タグ解析の段階でTileOffsetsとTileByteCountsを拒否し、すべてのバッファーをInt64演算でサイズ決定した上で、256 MiBのデコード上限を設けています
この修正が塞ぐ欠陥は、実験環境では決して表面化しません。表面化するのは、三年間静かに稼働し続けてきたスキャンゲートウェイに、顧客が地理空間アーカイブや医用のホールスライド画像を通したときです。ファイルは正当なTIFFヘッダーを持ち、解析も通ります。出てくるのは縞ノイズの一枚か、サービスを落とす数GB規模のメモリー確保であり、その過程のどこも入力を不正と宣言しません。ここで防ぐべき障害の形はこれです。クラッシュではなく、自信たっぷりに届けられる誤った答えです
IIやMMではクラシックTIFFと証明できない理由
バイトオーダーマーカーは両方の方言で共有されているからです。クラシックTIFFとBigTIFFはどちらもIIかMMで始まり、両者を実際に区別するのは直後の16ビットマジックです。TIFF 6.0仕様で定義されたクラシックTIFFは42、64ビットオフセットを持つBigTIFFは43です。FValidTIFF := PopWord = 42と書かれたローダーはクラシックTIFFについては間違っていませんが、二つのまったく異なる拒否を一つの無言のbooleanに潰してしまうため、BigTIFFが名前を付け替えただけの途切れたJPEGと区別できなくなります。PDFlibPasはこの二つを分離し、それぞれをTPDFTIFF.LastErrorに記録するようになりました。4バイトに満たないヘッダー、不正なバイトオーダーマーカー、マジック43、その他のマジック値が、いずれも異なるテキストを返します。ライブラリはBigTIFFをデコードしませんが、その事実をはっきり言葉にすること自体が要点です。呼び出し側が受け取るのは「これはTIFFではない」と「これは64ビットオフセット配置のTIFFで、内蔵デコーダーは実装していない」の違いであり、それは一回の返信で答えられるサポート起票と、一週間の推測作業に変わる起票の違いです
var
Tiff: TPDFTIFF;
Page: Integer;
begin
Tiff := TPDFTIFF.Create;
try
Tiff.LoadFromFile('inbox\scan-0417.tif');
if not Tiff.ValidTIFF then
raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
if Tiff.PageCount < 1 then
raise Exception.Create('TIFF carries no decodable page');
for Page := 1 to Tiff.PageCount do
Writeln(Format('page %d: %dx%d, %d spp',
[Page,
Tiff.PageInfo[Page].Width,
Tiff.PageInfo[Page].Height,
Tiff.PageInfo[Page].SamplesPerPixel]));
finally
Tiff.Free;
end;
end;
タイルは異なる幾何であり、もう一つのオフセット配列ではない
PDFlibPasは、ピクセルデータに触れる前のタグ解析の時点でタイル型TIFFを拒否します。バグを招く近道は見てすぐ分かります。タグ324(TileOffsets)とタグ325(TileByteCounts)はファイルオフセットとバイト数の配列で、構造的にはストリップ配列とまったく同じため、既存のストリップ用フィールドを向かせれば2行で済み、きれいにコンパイルも通ります。それでも間違いです。タイルは端数分の余白ブロックを伴う二次元グリッドを形成し、各タイル内に固有の行ストライドを持ち、RowsPerStripのセマンティクスを一切持たない、とTIFF 6.0のタイル画像セクションは明記しています。したがって、タイルのペイロードをストリップデコーダーに流し込んでも派手には失敗しません。SimpleExtractとCompDecodeは誤ったストライドでデータをたどり、寸法は正しくピクセルが間違った画像を出力します。旧コードはこれに追い打ちをかけ、TTIFFPageにStripsAreTiles、ColumnsPerTile、RowsPerTileを置いていました。タイル組み立て器を一切持たないデコーダーが記録するタイル幾何です。3.534.1では、タグ324と325のハンドラーがタイルエラーを発生させてIFDを即座に放棄するため、拒否には「tiled」という言葉が載り、数週間後に描画苦情として表面化することはありません
寸法のクランプ一つはメモリー予算ではない
幅と高さをそれぞれ65,535にクランプするのは必要ですが、それだけでは全く足りません。確保を駆動する量は積だからです。RowsPerStrip * Width * SamplesPerPixelは、どの項が自らの上限に達するよりはるかに前で32ビット演算をオーバーフローでき、オーバーフローしなくても、サービスが試みるべきでない確保を指し得ます。PDFlibPasは行バイト数をInt64で計算し、三つの上限を同時に強制します。次元ごとに65,535、カラーコンポーネント32、デコード済みバイトで256 MiBです
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// TPDFTIFF.ValidatePageForDecode の内部
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
(RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
Exit(False);
DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
(DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
(DecodedBytes > MaxInt) then
Exit(False);
ここには、定数そのものより重要な三つの詳細があります。高さの判定は乗算ではなく除算で書かれているため、過大な積はそもそも生成されません。1未満または画像高さを超えるRowsPerStripは、先に高さへ正規化されます。これはTIFF 6.0がすでに暗示する単一ストリップの読み方であり、悪意あるタグがストリップバッファーを膨らませるのを止めます。そしてこのルーチンは共有されています。ValidatePageForDecodeはタグ解析の末尾で実行され、SimpleExtractとCompDecodeの両方の入口でも再び実行されるため、直接デコーダーへ至るコードが予算をすり抜けることはできません。これは信頼できないPDFオブジェクトグラフの解析でPDFlibPasが守るのと同じ規則です。三つの扉のうち一つでだけ強制される制限は、制限ではないからです
PageInfoを読む前に呼び出し側は何を確認すべきか
まずValidTIFFを確認し、次にPageCountを確認し、その後に初めてPageInfoをインデックスします。拒否されたファイルではPageCountがゼロのまま残ることがあり、GetPageInfoは範囲外のインデックスに対して未初期化のTTIFFPageレコードを返すため、失敗の報告までの道すがら解像度やサンプル数を読むエラーパスは、ノイズを読むことになります。バージョン3.534.1は、ライブラリ内部の両呼び出し箇所を修正しました。画像インポート経路は有効ブランチの中でのみXResとYResを読み、TPDFlib.GetImagePageCountはページ数が非ゼロであることを鵜呑みにせずValidTIFFを要求します。下流では、AddImageFromFileのOptions引数がマルチページTIFFの1始まりページ番号であるため、ループ開始後にではなく開始前にGetImagePageCountが信頼できなければなりません。ゼロページは今や「ここにデコード可能なものは何もない」という実在する答えであり、早期リターンの偶然ではありません。両面スキャン原稿の照合とインターリーブを行う際、一枚静かに誤デコードされた用紙が誤った位置に収まると最も深刻な場面で、この点が効いてきます
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // ヘッダー不正、BigTIFF、タイル配置、予算超過のいずれか
Pdf.NewDocument;
for I := 1 to Pages do
begin
Pdf.NewPage;
ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
if ImageID > 0 then
begin
Pdf.SelectImage(ImageID);
Pdf.DrawImage(0, 0, 595, 842);
end;
end;
Pdf.SaveToFile('scan-0417.pdf');
finally
Pdf.Free;
end;
end;
デコーダーを自前で持つか、libtiffをリンクするか
PDFlibPasは内蔵デコーダーを維持します。決め手は作者性ではなく、プラットフォーム到達範囲です。約1,873行のObject Pascalは、コンパイラーが届くどこでもコンパイルできます。Win32、Win64、macOS、iOS、Android、そしてLinux上のFPCです。libtiff 4.7.1は34のtif_*.c翻訳単位にまたがる約30,000行のCであり、現存するプリビルドのオブジェクトファイルはWindowsのみをカバーします。採用すれば、完全なTIFFカバレッジと引き換えに、Cツールチェーンが動くマシンだけに縮んだサポート対象プラットフォーム一覧と、誰も通ったことのないリンカーパスを引き受けることになります
この選択のコストは、飾らずに述べる価値があります。内蔵デコーダーは、スキャン文書業務が実際に生み出すものを処理します。CCITT Group 3一次元・二次元、Group 4、LZW、Deflate、PackBits、TIFF内JPEGで、WhiteIsZero、BlackIsZero、RGB、パレット、CMYKの各フォトメトリックとPredictor 1・2に対応します。これらのペイロードはISO 32000-1 §7.4.4と§7.4.6のPDFフィルターと揃っており、スキャンパイプラインでTIFFフロントエンドが重みを持つ理由です。処理できないのは、BigTIFF、タイル、浮動小数点のPredictor 3、PixarLogとSGILog、旧式JPEG圧縮6、サブIFDピラミッドです。3.534.1以降、これらはすべて誤った画像ではなく名指しの拒否となり、ライブラリはlibtiff決定を再開する引き金の一覧を文書として保持しています:
- 顧客がBigTIFFファイルを報告し、変換ステップではなくネイティブ対応を必要とする
- 顧客が医用、GIS、産業ソースのタイル型TIFFを報告し、その場でデコードすることを必要とする
- 顧客が浮動小数点Predictor 3のTIFFを報告する
- 公表された脆弱性が内蔵のCCITTまたはLZWデコード経路に届く
- クロスプラットフォームの論拠が適用されなくなる。macOS、iOS、Androidの対応が打ち切られるか、再利用可能なlibtiff統合がすでにmacOSとLinuxをカバーした場合だ
移行自体は仮説ではなく、範囲を決めてあります。USE_LIBTIFF条件コンパイルなら、TPDFTIFFの公開サーフェスを保ったまま、LoadFromStreamをストリームコールバック付きのTIFFClientOpenへ回し、Pascalパーサーを非Windowsフォールバックとして残せます。これらの引き金の一つが実際に発火するまでの間、二つのデコーダーと倍増したテスト行列の保守が、顧客が感じ取れるものを買うことはありません。脱出路を文書に書き残した上でコストを先送りすることは、それを無視することとは別物です
スキャン文書パイプラインにおけるこの強化の位置づけ
TPDFTIFFはコンバーターではなくゲートとして扱います。ファイルをロードし、ValidTIFFを読み、falseであればLastErrorを一字一句そのまま記録します。その文字列こそ、現場報告から診断までの最短経路だからです。ゲートを落ちたファイルは上流で変換すればまだ復旧でき、今日のBigTIFFとタイル型ソースへの実用的な答えはそれです。TIFFの範囲外の入力には、PDFlibPasはAVIF、HEIF、JPEG XL画像入力経路という別の道を用意しています。どのデコーダーがどのフォーマットを担うかという問いを、自然発生的にではなく明示的に保つためです
これらはすべて通常の画像APIの背後に収まっているため、文書パイプラインは、もともと確認すべきだったページ数の確認を超えて呼び出しコードを一行も変えずに、より厳しい境界を手に入れます。DelphiやC++Builder向けのネイティブTIFFからPDFへの経路を検討しているなら、コンポーネント一式と画像処理の詳細はPDF Library for Delphiのページに文書化されています