4バイト境界パディング処理

[C++][GDI][Windows]4バイト境界パディング処理

今日はWindowsでの画像プログラミングにおける、4バイト境界パディング処理の話題だよ。CreateDIBSection()を使うときや、BITMAPINFOHEADER、BITMAPFILEHEADERを使ってBMPファイルを作成したりするときにお役立ちだ。
DIBを作成するときに、24bitか32bitで迷ったなら、24bitにしておいた方が無難だ。一部の環境下における古いMSオフィス系のアプリでは、クリップボード上の画像は、32bitDIBではなく24bitDIBでないとちゃんと貼り付け出来ないらしいという事例もある事だし、24bitDIBを簡単に作成できると何かと便利だ。24bitの方がメモリサイズも少なくて済むしね。
ところが、ピクセルあたりR,G,Bそれぞれ8ビットずつ、合計24ビット深さの情報を持つ画像データから、WindowsのDIB(例えばBMP形式ファイル)を作成したり、HBITMAP GDIビットマップオブジェクトをDIBセクションとして作成したりする場合に避けて通れないのがこの4バイト境界(LONG BOUNDARY)問題だ。
DIBまたはDIBセクションを作成する際に入力として渡す、BGRBGR…とひたすら連なる画像データのバイト並びについては厄介な掟があって、それはすなわち「水平1ラインあたりのバイト数は必ず4の倍数になっているべし」というものだ。
ピクセルあたり32ビットの画像形式を選択するなら、画像サイズによらず1ラインあたりのバイト数は常に4の倍数なのでこの問題は素通りできるのだが、ピクセルあたり24ビットの場合は、画像サイズ幅が4の倍数でない限り、この条件を満たすために特殊な処理が必要となってくる。そう24bitDIB作成は、まさに茨の道なのだ。。
簡単な例をあげて具体的に説明すると、幅3ピクセルの白画像データがあった場合、水平1ライン分の画像データは、下図のように9バイトとなるが、9は4の倍数になっていない。9を4の倍数に持っていくにはこれを12バイトにしたく、つまりあと3バイト追加してやればよいことになる。このために、最後に0を3バイト入れるということをする。(本稿ではこの挿入処理をパディングと呼んでいる。)

b  g  r  b  g  r  b  g  r  
ff ff ff ff ff ff ff ff ff
ff ff ff ff ff ff ff ff ff 00 00 00  //←3バイト追記して12バイトに。
                           ^^^^^^^^

このようにスキャンライン末尾に適切な数のゼロを追加してから、次のラインの画像データを記述する、という具合にバイト並びを作成する必要がある。このため、24ビットDIBの画像データ部の総バイト数は単純に、ピクセルあたりのビット数に画像サイズ幅と高さを掛け算したものとはならないのが常だ。これはDIB生成時に限らず、24ビットDIBセクションとして作成されたHBITMAPからGetObject()で取得した、BITMAP構造体のbmBitsバッファの中身も必ずそうなっている。
にもかかわらず、画像の幅としてはパディング考慮などされていない生のそのままの値が保持されているのみなので、この4バイト境界パディングルールというものは、画像ファイル出力プログラムを書くにも、DIBセクションデータを使った各種処理を記述するのにも、どうにも面倒くさい状況を生み出していて、よろしくないということがあった。
そこで今日紹介するFourByteBoundaryPadding(FBBP)クラスの満を持しての登場となる。しかも、よくありがちな典型の転送処理に使えるの便利なおまけファンクタつきだぞ。ソースは以下のようになったよ。

struct FourByteBoundaryPadding
{
  FourByteBoundaryPadding( const int width, const int height, const int depth3or4 )
    :_width(width), _height(height), _depth3or4(depth3or4)
  {
    _lnsz = depth3or4 * width; 
    _num_padding = (4-_lnsz%4)%4;
    _padded_imsz =  (_lnsz+_num_padding) * height;
    _padded_lnsz = _lnsz + _num_padding;
  };
  ~FourByteBoundaryPadding(){}
  template< class CopyToDstFunc >
  void DoPadding( void* src, bool is_src_padded, CopyToDstFunc& cpy )
    // concept: cpy::operator()( char* src, size_t sz )
    // cpy::operator()には、転送先のバッファにsrcの内容をszバイトだけ
    // 転送する処理を記述します。
  {
    unsigned char* p = (unsigned char*)src;
    unsigned char z[]={0,0,0};
    for( int i=0; i<_height; ++i )
    {
      cpy( p, _lnsz );
      p += _lnsz;
      cpy( z, _num_padding );
      if( is_src_padded )
        p += _num_padding;// ★
    }// i
  }
  int _width, _height, _depth3or4;
  size_t _lnsz;
  size_t _num_padding;// 1ライン最後尾において挿入されるべき0の数(バイト数)
  size_t _padded_imsz;// padding処理後の実質のイメージサイズ
  size_t _padded_lnsz;// padding処理後の実質の1ラインあたりのバイト数
};
struct Func_fwrite{
  Func_fwrite( FILE* fp ):_fp(fp){}
  void operator()( void* src, size_t sz ){
    fwrite( src, sizeof( char ), sz, _fp );
  }
  FILE* _fp;
};
struct Func_memcpy
{
  Func_memcpy( char* dst ):_dst(dst){ _cnt=0;}
  void operator()( unsigned char* src, size_t sz ){
    memcpy( _dst+_cnt, src, sizeof(char)*sz );
    _cnt+=sz;
  }
  char* _dst;
  size_t _cnt;
};

FourByteBoundaryPadding(FBBP)クラスがやってくれることは大きく分けて以下の二つだ。

  1. 画像本来の幅と高さ、ピクセルあたりのバイト数を入力すれば即座にパディング関連の各種パラメータ(padding後のラインあたりバイト数やイメージ総サイズ他)をコンストラクタにて計算完了しいつでも取得可能。
  2. パディング処理が施されていないプレーンな24ビット並び画像データあるいはすでにパディング処理された画像データについて、パディング処理を行いながらの任意の転送処理(他のバッファやストリームに流し込む処理)を、たった一行で実行させることができる。

では使い方を見ていこう。

簡単のため、3x3ピクセルの白画像の場合を考えよう。1ピクセルあたり24ビットとすると、以下のようなバッファが必要になる。

  int w = 3;
  int h = 3;
  int depth = 3;
  // 
  int imsz = w * h * 3;
  char* buf = new char[ imsz ];
  {
    std::fill( buf, buf + imsz, 1 );
  }
  ::check_buf( buf, imsz, w*depth );

バッファbufの中身は以下のような関数で確認できる。

void check_buf( char* buf, size_t sz, int numcr )
{
  cout<<"0   4   8   b   0   4   8   b"<<endl;
  cout<<"+---+---+---+---+---+---+---+"<<endl;
  for( int i=0; i<sz; ++i ){
    cout<< (int)buf[ i ];
    if( (i+1)%numcr==0 ) cout<<endl;
  }// i
  return;
}

そして以下が確認した結果だ。

さてこのようなpaddingされていない白画像を4バイトpaddingしたい場合にはどうすればいいだろう。それこそはまさに今回のFBBPクラスによって、もはや簡単にできる処理だ。

  FourByteBoundaryPadding padding( w, h, 3 );
  // w:画像幅  h:画像高さ  ピクセルあたり3バイト(24bit)
  
 char* buf2 = new char[ padding._padded_imsz ];// buf2確保
  {
    padding.DoPadding( buf, 0, Func_memcpy(buf2) );// padding
  }
  ::check_buf( buf2, padding._padded_imsz, padding._padded_lnsz );

たったこれだけだ!驚いて欲しい。
上記のように、まずFBBPクラスのインスタンスを作成する。画像サイズ、ピクセル毎のバイト数を引数に与えてくれ。そうすると、padding後の総バイト数は、_padded_imszメンバの値として取れるのでそのサイズのバッファbuf2を確保し、最後に、DoPadding メソッドで、bufからbuf2への、paddingコピーを行う。
先に作った、check_buf関数でbuf2の中身の結果を見てみると、、

このように、見事にpadding処理がなされていることがわかる。

最後に、以前本日記において、プレーンな画像データのバイト並びをBMPファイルフォーマットで簡単に出力してくれるwrite_bmpというものを紹介していたが、画像幅サイズは4の倍数という制限をつけていたのだった。しかし今回のFBBPを利用することにより、この制限が見事とっぱらわれたことを、ここでご報告させてもらう。ちなみにwrite_bmpのソースの中では、上記で使用例を紹介していないもう一つのおまけファンクタである、Func_fwrite の典型的な使用例を見ることができるだろう。