技術記事

Delphi で TIFF の 12bit・可逆・算術符号 JPEG を PDF へ

Delphi 用 PDF Library の PDFlibPas は、8 ビットベースラインを超える JPEG 圧縮を持つ TIFF 画像を取り込めます。12 ビット拡張シーケンシャル(SOF1)、Huffman 可逆(SOF3)、算術符号化(SOF9 と SOF10)、テーブルオフセット付きの旧形式 TIFF 6.0 JPEG です。PDF の DCTDecode はこれらのプロセスを運べないため、PDFlibPas はネイティブな Pascal でデコードし、サンプルをコンポーネントあたり 8 または 16 ビットで可逆的に埋め込みます

一式すべてには v3.724.0 以降が必要で、狙いはスキャナ、医療・科学系のキャプチャソフト、1990 年代で凍結されたアーカイブシステムが吐く TIFF に定められています。以下では、なぜこれらの変種を PDF へそのままコピーできないのか、PDFlibPas がどう着地させるのか、そして途中で見つかった Pascal の罠を順に見ます

なぜ 12 ビット JPEG は PDF へそのまま入らないのか

12 ビット JPEG をパススルーできないのは、PDF にそのデコードを義務付けられたフィルタが存在しないからです。ISO 32000-1 §7.4.8 は DCTDecode を JPEG ベースライン形式を中心に定義し、PDF 1.3 でプログレッシブ拡張を加えました。その条項のどこにも、12 ビットのサンプル精度、ITU-T T.81 附属書 H の可逆プロセス、附属書 D の算術エントロピーコーダは出てきません。DCTDecode ストリーム内で SOF1 の 12 ビットフレームや SOF9 マーカーに出会ったリーダーは、何も描かなくてもよいことになっており、大半のビューアはまさにそうします

JPEG-in-TIFF ファイルの大半を占める 8 ビット Huffman ベースラインとプログレッシブのストリップについては、PDFlibPas は今でもすべてのスキャンを検証した上で圧縮バイトを DCTDecode として通し、PDF を小さく保ちます。それ以外はすべてデコードします。PDFlibPas は最初の JPEG ブロックのフレームマーカーとサンプル精度を読み、SOF3、SOF9、SOF10 あるいは 8 以外の精度を検出すると、ページはパススルー経路ではなくサンプルデコーダを通ります。フレームマーカーによる算術符号化の検出は、聞こえ以上に重要です。サンプル精度だけを見る検査では、古典的な TIFF が 8 ビットの SOF9 ストリップを DCTDecode 経路へ滑り込ませ、リーダーが表示できない PDF が出来上がってしまいます

Delphi 用 PDFlibPas における TIFF JPEG ブロックの判断フロー。フレームマーカーが 8 ビットスキャンを DCTDecode へ、他のプロセスをネイティブデコードへ振り分ける
TIFF JPEG ストリップが DCTDecode で通るかネイティブでデコードされるかを決めるのはフレームマーカーであって、サンプル精度だけではありません

代わりに PDFlibPas が PDF に書き込むもの

PDFlibPas はデコード済み JPEG ブロックを、BitsPerComponent 8 または 16 の普通の画像 XObject へ変換します。サンプルは既定では無圧縮で格納され、画像を追加する前に CompressImages(1) を呼ぶと FlateDecode 付きで書き出されます。ISO 32000-1 §8.9.5.1 は PDF 1.5 からコンポーネントあたり 16 ビットを認めているため、16 ビットサンプルの画像が追加されると、PDFlibPas は文書バージョンを最低でも 1.5 に上げます。マルチストリップやタイル画像も含まれます。これらのルートオブジェクトは Form XObject で、自分自身に調べるべき BitsPerComponent を持ちません

JPEG プロセスごとのマッピングは固定です

  • 12 ビット Huffman JPEG(シーケンシャル SOF1、プログレッシブ SOF2)は逆量子化と逆変換を経て 16 ビットサンプルへ拡張されます
  • 算術符号化 JPEG(シーケンシャル SOF9、プログレッシブ SOF10)は 8 ビット精度では 8 ビットサンプルに、12 ビット精度では 16 ビットサンプルになります
  • Huffman 可逆 JPEG(SOF3)は宣言された精度に正確に合わせて 8 ビットまたは 16 ビットサンプルになります

12 から 16 ビットへの拡張はビットレプリケーション、つまり (V shl 4) or (V shr 8) を使うため、4095 は 65535 に写り、白は 65520 で頭打ちになる代わりに白のままです。PDFlibPas が背後でこれらの画像を 8 ビットに落とすことはありません。ライブラリ自前の表示サーフェス向けにページをラスタライズするときは各サンプルの上位バイトを使いますが、埋め込まれた PDF 画像は 16 ビットすべてを保ちます

Delphi 用 PDFlibPas におけるデコード済み JPEG プロセスのサンプル幅へのマッピング。12 ビット Huffman は 16 ビットへ拡張、算術と可逆は宣言精度に従う
各 JPEG プロセスの PDF 内でのサンプル幅は固定で、12 ビットデータはビットレプリケーションで拡張されるため白は白のままです

代償はファイルサイズです。CompressImages(1) を使っても、Flate 圧縮した 16 ビットサンプルは元の DCT データより大きくなり、使わなければさらに大きくなります。診断やアーカイブのワークフローなら、この取引はたいてい正しい側です。サイズのほうが優先される案件では、Delphi で PDF ファイルサイズを減らすガイドがバイトの行き先を示してくれます

TPDFlib でこれらの TIFF を取り込む

呼び出し側のコードはまったく変わりません。TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer は Options で 1 基点の TIFF ページ番号を受け取り、画像 ID を返します。ページを取り込めないときは 0 です。対応外の JPEG 変種は画像の挿入より前に拒否されるため、0 が返ったときに半分だけデコードされたものが文書へ入り込むことはありません

uses
  PDFlibrary;

function TiffToPdf(const TiffName, PdfName: WideString): Boolean;
var
  Lib: TPDFlib;
  Page, PageCount, ImageID: Integer;
  DPI, W, H: Double;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    Lib.SetMeasurementUnits(0);   // 既定のユーザー空間、1/72 インチ
    Lib.SetOrigin(1);             // DrawImage 用の左上原点
    Lib.CompressImages(1);        // デコード済みサンプルに Flate
    PageCount := Lib.GetImagePageCount(TiffName);
    if PageCount = 0 then
      Exit;
    for Page := 1 to PageCount do
    begin
      ImageID := Lib.AddImageFromFile(TiffName, Page);  // Options = TIFF ページ番号
      if ImageID = 0 then
        Exit;                     // 対応外か壊れた画像データ
      Lib.SelectImage(ImageID);
      case Lib.ImageResolutionUnits of
        2: DPI := Lib.ImageHorizontalResolution;          // 1 インチあたりドット数
        3: DPI := Lib.ImageHorizontalResolution * 2.54;   // 1 cm あたりドット数
      else
        DPI := 0;
      end;
      if DPI <= 0 then
        DPI := 300;
      W := Lib.ImageWidth * 72 / DPI;
      H := Lib.ImageHeight * 72 / DPI;
      if Page > 1 then
        Lib.NewPage;
      Lib.SetPageDimensions(W, H);
      Lib.DrawImage(0, 0, W, H);
    end;
    Result := Lib.SaveToFile(PdfName) = 1;
  finally
    Lib.Free;
  end;
end;

形式ごとの分岐は一切不要で、メモリ上に保持した TIFF データに対しては AddImageFromStream と AddImageFromString が同じ規則に従います

旧形式 JPEG:オフセット位置のテーブル、ヘッダなしのストリップ

v3.713.0 から、PDFlibPas はテーブルが JPEGQTables、JPEGDCTables、JPEGACTables の各オフセットタグから来るか、テーブルのみの JPEGInterchangeFormat ストリームから来る旧形式 JPEG TIFF(Compression = 6)を取り込めます。TIFF 6.0 のセクション 22 がこの配置を定義し、TIFF Technical Note 2 が後に Compression = 7 と共有の JPEGTables タグで置き換えましたが、スキャナは何年も旧形式を書き続けました

コンテナ側の BigTIFF やタイル配置はTIFF デコーダ堅牢化の記事が扱っており、ここでの焦点は JPEG ペイロードです。各テーブルオフセットタグは生の 64 バイト量子化テーブル、あるいは 16 個のカウントとその値の並びからなる Huffman テーブルを、コンポーネントごとに 1 つ指します。PDFlibPas はこれらを、合成した JPEGTables ストリーム内の DQT と DHT セグメントへ変換します。そのうえで、素のエントロピー符号化データの各ストリップには専用のヘッダを与えます。SOI、そのストリップの行数を載せたフレームマーカー、JPEGRestartInterval が設定されていれば DRI、SOS、エントロピーデータ、EOI の順です。ストリップを 1 つのスキャンへ接着することはできません。旧形式 JPEG はストリップ境界ごとに DC 予測を初期化するからです

間違えやすい細部が 2 つあります。YCbCr 画像の 3 コンポーネントはしばしば同じバイトをテーブルオフセットとして指すため、PDFlibPas はテーブル番号を割り当てる前に内容でテーブルを重複排除します。1 クラスの Huffman テーブルが 3 枚以上残ると、合成フレームは SOF0 の代わりに SOF1 を使います。ベースラインフレームが参照できるのは DC 2 枚と AC 2 枚までだからです。JPEGInterchangeFormat がスキャンを含む完全な JPEG ストリームを指している場合は、PDFlibPas はそのストリームを画像全体として扱い、新形式のストリップ 1 本として取り込みます。可逆、タイル、別プレーンの旧形式 JPEG は推測ではなく明示的なエラーで拒否されるため、それらは上流で新形式 JPEG へ変換してください

なぜ TIFF 内の CMYK JPEG は反転して現れるのか

TIFF 内の CMYK JPEG が反転して現れるのは、単体 JPEG ファイル用の Adobe APP14 流儀をインポータが適用してしまうときです。Photoshop は単体 JPEG に反転した CMYK を書き込むため、PDF プロデューサは伝統的に補正として /Decode [1 0 1 0 1 0 1 0] を足してきました。TIFF の PhotometricInterpretation 5 はサンプルをインク量と定義し、0 はインクなしを意味します。そして主流の TIFF リーダーは APP14 を見ずに、デコード済み JPEG サンプルを額面通りに扱います

PDFlibPas は、JPEG 圧縮された CMYK TIFF ストリップを反転 Decode 配列なしで埋め込みます。APP14 マーカーの有無は問いません。APP14 の変換フラグは YCCK から CMYK への変換の要否を決める役割を果たし、そのステップは DCTDecode が自前で処理します。単体の 4 コンポーネント JPEG ファイルは Adobe 流の反転規約を保つため、同一の JPEG バイトでもコンテナに応じて異なる扱いを受けます。両形式がそれを要求しているのです

なぜ可逆 JPEG デコードは Pascal のシフトで壊れるのか

Pascal で可逆 JPEG のデコードが壊れるのは、予測器 5 と 6 が算術右シフトを必要とするのに、Pascal の shr は論理シフトだからです。SOF3 は各サンプルを左(Ra)、上(Rb)、左上(Rc)の近傍から予測し、差分だけを符号化します。T.81 の表 H.1 にある 7 つの予測器のうち、予測器 5 は Ra + ((Rb - Rc) >> 1)、予測器 6 は Rb + ((Ra - Rc) >> 1) で、括弧内の差は左上の近傍が明るい側なら負になります。負の Integer を shr に渡すと符号ビットが大きさのビットに化け、画像はノイズへ溶けます

Delphi と PDFlibPas における可逆 JPEG 予測器のシフト挙動。論理 shr は負値を壊し、div 2 はゼロへ切り詰め、HalfDown はリファレンス同様に床へ丸める
予測器 5 と 6 には床丸めのセマンティクスが必要で、小さなヘルパー 1 つで Delphi と Free Pascal のどちらでもリファレンスデコーダと一致します

シフトを div 2 へ置き換えても直りません。div はゼロ方向へ切り詰め、算術シフトは負の無限大方向へ丸めるため、両者は負の奇数値すべてで食い違います。Free Pascal には System ユニットに SarLongint がありますが、Delphi には等価物がなく、PDFlibPas は両方で同一にコンパイルされるヘルパーを使います

// 1 ビットの算術右シフト:floor(Value / 2)
// shr は論理シフトであり、div 2 はゼロ方向へ切り詰める
function HalfDown(Value: Integer): Integer;
begin
  if Value >= 0 then
    Result := Value shr 1
  else
    Result := -((1 - Value) shr 1);
end;

// Value = -3 のとき:-3 shr 1 = 2147483646、-3 div 2 = -1、HalfDown(-3) = -2

同じ類の問題の型幅の側面については、Delphi Linux64 の Longint とポインタの落とし穴を参照してください

可逆デコーダの残りの部分にも、境界まわりの規則があります。最初の行と各リスタートマーカーの直後の行は左の近傍だけから予測し、精度 P、ポイント変換 Pt に対して 2^(P−Pt−1) で始まります。それ以降の行は上のサンプルから始まります。再構成は 2^16 を法としてラップし、ポイント変換は出力だけをスケールし、予測チェーンには決して触れません。Huffman の DC カテゴリ 16、つまり追加ビットなしの差分 32768 は可逆モードでのみ合法で、テーブルパーサはフレームがどのプロセスを名乗るかを知る前にこれを受け付けられなければなりません

PDFlibPas は 7 つの予測器すべてと、行全体をカバーするポイント変換とリスタート間隔をサポートします。JPEG の精度は BitsPerSample と等しく 8 か 16 である必要があり、全コンポーネントのサンプリングは 1x1 でなければなりません。YCbCr の可逆データと 12 ビットの可逆 JPEG は明示的なエラーで拒否します

ネイティブ Pascal での 12 ビット DCT と算術符号化

PDFlibPas の 12 ビット系と算術系のデコーダは 1 つの逆 DCT を共有し、異なるのはエントロピーデコードだけです。12 ビット Huffman JPEG では DC カテゴリは 15 まで、AC カテゴリは 14 まで届き、量子化テーブルは 16 ビットのエントリを持ち得て、DC 予測はスキャンごと、リスタートごとに初期化されます。係数は逆量子化までジグザグ順のままに置かれ、逆量子化は同じジグザグインデックスの量子化値を使わなければならず、その後で分離型 2 パス IDCT の自然順へ移ります。デコードされたサンプルは、独立した浮動小数点リファレンスデコーダの ±1 以内に収まります

算術符号化は Huffman を T.81 附属書 D の QM コーダで置き換えます。114 エントリの確率状態テーブル、64 ビンの DC 統計領域、256 ビンの AC 領域を、DAC セグメントのコンディショニングパラメータ(既定は L = 0、U = 1、Kx = 5)に従って駆動します。統計、DC コンテキスト、予測はリスタート間隔ごと、スキャン開始ごとに初期化されます。PDFlibPas が算術画像向けに共有の JPEGTables を正規化するとき DAC セグメントを保持するのは、このためです。落とすと、すべてのストリップが誤った統計モデルでデコードされます

v3.723.0 でこれらのデコード経路にサブサンプリングされたコンポーネントと、JFIF の式による YCbCr から RGB への変換が加わり、v3.724.0 で 12 ビット Huffman(SOF2)と算術(SOF10)の双方にプログレッシブスキャンが加わりました。サブサンプリングされたクロマは平滑化ではなくフル解像度への複製で扱います。これはリファレンスデコーダのクロマ平滑化をオフにしたときと同じ挙動です。階層型 JPEG と算術符号化の可逆 JPEG(SOF11)は明示的なエラーで拒否します

Delphi でバイナリ JPEG フィクスチャをテストする

Delphi のユニットテストで手作りした JPEG フィクスチャは、まるでデコーダのバグのように見える形で失敗することがあります。#$xx 文字から組み立てた 255 文字超の文字列定数は UnicodeString に型付けられ、AnsiString への代入でアクティブなコードページを通って変換されます。エントロピー符号化データに日常的に現れる $80 から $9F のバイトは、入り口で書き換えられるのです。実行時にマーカーヘッダを組み立てる場合にも同じ罠があります。Delphi では AnsiChar + AnsiChar が UnicodeString を生むため、単一バイトは AnsiString へ追記するか、SetLength とインデックス代入で書くことになります。フィクスチャには 16 進テキストを保存し、実行時にデコードするのが確実です

uses
  SysUtils, PDFlibrary;

function HexToBytes(const Hex: AnsiString): AnsiString;
var
  I: Integer;
begin
  SetLength(Result, Length(Hex) div 2);
  for I := 1 to Length(Result) do
    Result[I] := AnsiChar(StrToInt('$' + string(Copy(Hex, I * 2 - 1, 2))));
end;

// フィクスチャは素の ASCII のままなので、どのコードページも触れない
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

テスト画像を生成するときは、jpegtran -arithmetic が書くのはシーケンシャルの SOF9 で、SOF10 には -progressive も要る点に注意してください。また、0 以外の画像 ID は取り込みが受け付けられたことしか証明しません。ページをレンダリングしてピクセルを比較してください。パススルーの間違いは、どのビューアも描けない well-formed な PDF を生むのですから

レガシーなスキャンアーカイブを PDF へ

これらの JPEG 変種は、アーカイブ移行の最中に一斉に姿を現しがちです。PDFlibPas は 12 ビット、可逆、算術、旧形式の JPEG TIFF を、他と同じ AddImageFromFile 呼び出しで読み、PDF の中へ完全な精度のまま保持し、その集合の外側にある珍しいプロセスは明示的なエラーで拒否します。ライセンス、機能一覧、トライアルダウンロードは、PDFlibPas PDF Library for Delphi の製品ページを参照してください