技術記事

Delphi の CCITTFaxDecode: 非圧縮モードと破損行

ネイティブ Delphi PDF コンポーネントである HotPDF は、CCITTFaxDecode 画像を、T.4/T.6 非圧縮モード拡張と DamagedRowsBeforeError が駆動する破損行リカバリの完全なサポート付きでデコードします。非圧縮モードに入るのは 111 で終わる拡張コードだけ、破損行が修復されるのは EndOfLine が設定された Group 3 データだけです。それ以外は、斜めのノイズページを生成する代わりに、きれいに失敗します

ファックス符号化画像は PDF 画像デコードのロングテールです。出会うほとんどの CCITT ストリームはスキャナドライバや文書取込パイプラインの産物で、そこは普通の Group 4 しか出力しません。一般的なケースを処理できるデコーダは、投げたすべてのテストを通過します。それから 15 年物のファックスサーバーのアーカイブが届き、ページが 400 行目あたりから斜めのノイズとしてレンダリングされ、ITU-T 勧告の誰も実装しない 2 つの部分こそ、そのファイルが使っている 2 つの部分だと気づくわけです。本記事では、その 2 つの部分を HotPDF がどう扱うか、そしてデコーダが正しく見えながら誤り得る箇所をたどります

ビットストリームを実際に変える CCITTFaxDecode パラメータはどれか

DecodeParms 辞書の中で、デコーダのビットストリームの区切り方を変えるエントリは 4 つです。K、EndOfLine、EncodedByteAlign、DamagedRowsBeforeError。ISO 32000-1 §7.4.6(Table 11)がこれらを定義しており、どれか 1 つでも間違えれば、最初の不一致より後の全行がずれます。BlackIs1、Columns、Rows、EndOfBlock も重要ですが、これらが左右するのは極性、幾何、デコードの停止位置であって、行境界の見つけ方ではありません

  • K は符号化方式を選びます。負は純粋な 2 次元の Group 4(T.6)、0 は 1 次元の Modified Huffman(Group 3 1D)、正は混在の Group 3 2D です。混在モードでは各 EOL の後のタグビットが、次の行が 1D か 2D かを告げます
  • EndOfLine は、すべての Group 3 行が EOL コード——1 が 1 個続く 11 個のゼロビット——を先行して持つことを意味します
  • EncodedByteAlign は、符号化された各行がバイト境界から始まることを意味し、デコーダは読む前にパディングビットを捨てなければなりません
  • DamagedRowsBeforeError は、デコーダが諦めるまでに何行の破損行を許容するかで、既定値は 0 です

THotPDF.ExtractLoadedImage や THotPDF.DecodeLoadedImageStream を呼ぶと、HotPDF はこれらすべてを画像辞書から読みます。意図的な逸脱が 1 つあり、知っておく価値があります。Columns が欠けている場合、HotPDF は仕様の既定値 1728 ではなく画像の Width にフォールバックし、Rows が欠けていれば Height を使います。2480 ピクセルの A4 スキャンで Columns を省略するプロデューサは間違っていますが、これは十分にありふれていて、画像の幾何を信用すれば使えるページになり、文字通りの既定値ならゴミになります

000000 はブロックの終わりとして扱えないのはなぜか

2 次元行では、先頭の 6 つのゼロビットは曖昧です。これを EOFB の始まりとして読むデコーダは、正当な非圧縮モードの入り口を画像の終わりと誤読します。Group 4 には行間に EOL がなく、ゼロが多いコードは、2 つの EOL が背中合わせになった EOFB と、ITU-T T.4 と T.6 の拡張コードだけです。どちらも 000000 で始まり、7 ビット目で分かれます

HotPDF の CCITT デコーダはこのプレフィックスをビットごとに解決します。7 ビット目が 1 なら次の 3 ビットは拡張コードで、全体のパターンは 0000001xxx です。非圧縮モードに入るのは xxx = 111 のときだけです。PDF のファックスフィルタに他の拡張値の用途はないため、それ以外の拡張値は不正形式として扱われます。7 ビット目が 0 なら EOL の継続しかあり得ないので、デコーダはさらに 4 つのゼロビットと締めの 1 を要求し、合計 11 個のゼロと 1 個の 1 が揃って初めて EOFB を報告します。それより短くても長くてもエラーであり、寛容な画像終端ではありません

寛容版は魅力的です。壊れたファイルの一クラス全体を問題なしに見せてくれるからです。デコードが早めに静かに止まり、例外なしの短い画像が手に入ります。しかしそれはまさに、正当なファックスを最初の非圧縮データの区間で切り詰めるバグでもあります。スキャンされた様式フォームでは、その区間はたいてい濃い署名か印鑑です。HotPDF は厳格な読みを選びます。短い画像は、エンコーダが本当にそこで終わったことを意味します

非圧縮モードはピクセルをどう符号化するのか

非圧縮モードはランレングス符号をリテラルのピクセルパターンに置き換えます。各コードワードは最大 5 ピクセルを運び、exit コードで通常に戻るまで続きます。1 次元行は 12 ビットのコード 000000001111 で入り、2 次元行は上述の 10 ビット拡張 0000001111 を使います。エンコーダは、1、2 ピクセルのランがピクセル自体より高くつく網かけ風の領域で、このモードへ切り替えます

モードの内部では、コードワードは先頭のゼロの数で数えます。ゼロから 4 個のゼロの後に 1 が続けば、その数の白ピクセルと、その後の 1 個の黒ピクセルです。5 個のゼロの後に 1 は、5 個の白ピクセルだけを意味します。6 から 10 個のゼロの後に 1 は exit コードです。6 を超えた分のゼロは最大 4 個の追加の白ピクセルを出力し、exit コードの後の 1 ビット——タグビット——は、通常のラン符号化が再開されるときの次のランの色を設定します

{ 非圧縮モードのコードワード(ITU-T T.4 / T.6)
  W = 白ピクセル、B = 黒ピクセル、T = タグビット

  1            B
  01           W B
  001          W W B
  0001         W W W B
  00001        W W W W B
  000001       W W W W W
  0000001T     exit, then resume runs with color T
  00000001T    W, exit
  000000001T   W W, exit
  0000000001T  W W W, exit
  00000000001T W W W W, exit
  more than ten zeros: malformed }

タグビットが微妙な部分です。ラン符号化は状態を持ちます。1D 行は白と黒のランを交互に並べ、2D 行は現在の変化要素 a0 の色を追跡します。exit コードの後、デコーダはタグの示す色で再開しなければならず、それは最後のリテラルピクセルと一致するとも限りません。HotPDF のデコーダは、タグが追跡中の色と異なるとき、現在位置で色変化を記録します。次のランが正しい色から始まるようにするためです。タグを無視すれば行の残りが色反転で出てきて、2D モードでは参照線を通じて後続の全行に被害が伝染します

DamagedRowsBeforeError が回復するものとしないもの

DamagedRowsBeforeError は HotPDF に破損行の置換と続行を許しますが、K が 0 以上で EndOfLine が true の場合に限ります。この 2 つの条件は恣意的ではありません。リカバリには、ビットが意味を失った後で次の行がどこから始まるかを見つける手段が必要で、CCITT ストリームで信頼できる目印は EOL コードしかありません。Group 4 データには EOL がなく、EOL を必須としない Group 3 データには存在の保証もないため、どちらの場合も破損行はパラメータの値にかかわらずデコードを終わらせます

行が失敗すると、HotPDF のデコーダは次の EOL——1 が 1 個続く 11 個以上のゼロビットのラン——まで前向きに走査し、代わりの行を差し込みます。正常行の直後の最初の破損行は前の行のコピーで置き換えられます。スキャンされたテキストページなら、たいてい見分けがつかない程度に近いものです。破損行の直後にまた破損行が来た場合は、代わりに全白の行で置き換えます。コピーのコピーは 1 行のテキストを背の高い黒帯に塗り広げるからです。この代用行は次の 2D 行の参照線となり、混合モードのストリームが修復後もデコード可能であり続けるのはこの仕組みのおかげです

見落としやすい境界が 2 つあります。予算は悪い行の連続ごとではなく画像全体で累積です。DamagedRowsBeforeError 3 は合計 3 行の破損行を許容し、4 行目でデコードが失敗します。そして HotPDF におけるデコード失敗はオール オア ノシングです。THotPDF.ExtractLoadedImage は、ギザギザの下端を持つビットマップではなく nil を返します。THPDFLoadedImageInfo.Decodable が報告するのはフィルタがサポートされていることであって、このストリームが無傷であることではありません。ビットマップの nil は必ず確認してください

走査が EOL の始まりで止まらねばならない理由

再同期の走査は、リーダーを EOL の最初のゼロビットの位置に置きます。EOL の後ではありません。デコーダには、EOL を消費し、ブロックを終える 2 連の EOL を確認し、混合モードのタグビットを読む、1 つのフレーミングループがあります。リカバリの走査が EOL 自体を飲み込むと、このループは EOL のない行を目にし、それも破損扱いにして、正常な行を落とした上で予算をもう 1 消費します。さらに悪いことに、タグビットが飛ぶと 2D 行が 1D として(あるいはその逆で)デコードされます。1D のランデコーダが行の途中で予期しない EOL に当たったときも同じ規則です。ランコードの始まりまで巻き戻すので、目印がリカバリの走査のためにそこに残ります

読み込んだ PDF の CCITT 画像の棚卸し

アーカイブ済みファックスの束を信用する前に、どの画像が CCITTFaxDecode を使い、どれがデコードに失敗するのかを一覧しましょう。THotPDF.GetLoadedImageInfo は何もデコードせずに幾何とフィルタ名を教え、THotPDF.ExtractLoadedImage はストリームが生き残ったかどうかを告げます。画像ディスパッチャが各フィルタをどう振り分けるかの全体像は、読み込んだ PDF 画像とそのデコードフィルタの抽出ガイドを参照してください

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('fax-archive.pdf', '') <= 0 then
      Exit;
    for I := 0 to Pdf.GetLoadedImageCount - 1 do
    begin
      if not Pdf.GetLoadedImageInfo(I, Info) then
        Continue;
      if Pos('CCITT', UpperCase(String(Info.Filter))) = 0 then
        Continue;
      Bmp := Pdf.ExtractLoadedImage(I);   // ストリームの破損が激しいときは nil
      try
        if Bmp = nil then
          Writeln(Format('obj %d: %dx%d CCITT failed to decode',
            [Info.ObjectNumber, Info.Width, Info.Height]))
        else
          Bmp.SaveToFile(Format('fax_%d.bmp', [I]));
      finally
        Bmp.Free;                         // ビットマップの所有者は呼び出し側
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

失敗したものは証拠を残します。既定の lismEncoded モードの THotPDF.ExtractLoadedImageToStream は、格納されたストリームバイトを一切手を加えずに書き出します。2 つ目のデコーダに渡すとき、調査のために TIFF コンテナへ包むとき、バグレポートに添付するとき、欲しいのはこれです。破損したファックスを再エンコードするのは、破損の位置を隠すだけです

procedure SaveRawFax(Pdf: THotPDF; ImageIndex: Integer; const FileName: string);
var
  Fs: TFileStream;
begin
  Fs := TFileStream.Create(FileName, fmCreate);
  try
    if not Pdf.ExtractLoadedImageToStream(ImageIndex, Fs, lismEncoded) then
      raise Exception.CreateFmt('Image %d has no stream data', [ImageIndex]);
  finally
    Fs.Free;
  end;
end;

THotPDF.CollectLoadedImageStreams で自分で画像ストリームをたどる場合は、各 THPDFStreamObject を THotPDF.DecodeLoadedImageStream に渡します。同じ CCITT 経路、パラメータ、リカバリ規則が適用されます。解放するのは返されたリストであって、中のストリームオブジェクトではありません。こちらの所有者は文書のままです

自分で管理していないファイルで DamagedRowsBeforeError を上げるべきか

いいえ。PDF を編集してでも、ですし、たいていはまったくやるべきではありません。このパラメータの所有者はプロデューサです。伝送路がビットを落とすかどうかを知っている唯一の当事者だからです。DamagedRowsBeforeError 0 を宣言しておきながら破損行を含むファイルは、アーカイブコピーが壊れていると告げています。誠実な結果は、ログに残せるデコード失敗です。契約書や診療記録の行を黙って継ぎ当てると、検出可能な欠陥が、行を 1 本欠いたもっともらしい見た目の画像に変わります。後でそれを頼りにする者にとって、これはより悪い帰結です

信頼できないアーカイブに対するより安全なパターンは、デコードを厳格に保ち、どのオブジェクト番号が失敗したかを記録し、それらを人間か別のデコーダへ回すことです。アーカイブが大きく多くの情報源から来ているなら、画像デコードをプロセス外で実行することで、敵対的なストリームがホストアプリケーションにできることを限られます。信頼できない PDF を処理するときの画像コーデックの分離の記事がその構成を扱い、JBIG2 バイレベル圧縮ガイドは、同じスキャンアーカイブで出会うもう 1 つのバイレベルフィルタを比較します

FAQ

HotPDF は Group 4 画像の CCITT 非圧縮モードに対応しているのか

はい。HotPDF は 2 次元行の 10 ビット拡張 0000001111 と 1 次元行の 12 ビットの入り口コード 000000001111 を認識し、リテラルのコードワードをデコードし、ラン符号化が再開するときの exit タグビットを尊重します

Group 4 のファックスに DamagedRowsBeforeError が効かないのはなぜか

リカバリには再同期のための EOL マーカーが必要です。HotPDF は K が 0 以上かつ EndOfLine が true のときにのみ DamagedRowsBeforeError を適用します。Group 4 データ(K が負)には EOL がないため、破損行はどれもデコードを終わらせます

破損した CCITT 画像に対して ExtractLoadedImage は何を返すのか

THotPDF.ExtractLoadedImage は、破損が DamagedRowsBeforeError の許容を超えるとき、あるいはまったく回復できないときに nil を返します。部分的なビットマップを返すことは決してないので、nil を判定し、生バイトを残すなら ExtractLoadedImageToStream を使ってください

HotPDF は、読み込んだ PDF の CCITT、JBIG2、JPEG、JPEG 2000、Flate 画像を、素の Delphi と C++Builder コードから処理します。ファックス経路に外部 DLL は不要です。詳細とエディションはHotPDF Delphi PDF コンポーネントのページにあります