BMP 画像の扱いかた


1. ビットマップ描画の基本

 Windows では画像をビットマップとして扱い、ビットマップはビットマップハンドルによって表わされる。ビットマップを画面に描画するためには、デバイスコンテキストにビットマップを設定してやらなければならない。リスト 1-1 に簡単なビットマップ描画の関数を示す。この関数は ウィンドウ hWnd にビットマップ hBmp を描画するもので、描画位置・描画サイズは固定(0, 0, 100, 100)になっている。

リスト 1-1 ビットマップ描画の基本

void DrawBitmap(HWND hWnd, HBITMAP hBmp)
{
	HBITMAP	hBmpPrev;
	HDC	hDC;
	HDC	hDCBmp;

	hDC = GetDC(hWnd);
	hDCBmp = CreateCompatibleDC(hDC);
	hBmpPrev = SelectObject(hDCBmp, hBmp);
	BitBlt(hDC, 0, 0, 100, 100, hDCBmp, 0, 0, SRCCOPY);
	SelectObject(hDCBmp, hBmpPrev);
	DeleteDC(hDCBmp);
	ReleaseDC(hWnd, hDC);
}
GetDC()
指定されたウィンドウに対するデバイスコンテキストを取得。Windows ではウィンドウに直接描画することはできず、すべてデバイスコンテキスト(DC)を経由して行わねばならない。

CreateCompatibleDC()
 転送元ビットマップのための DC を用意している。ここで得られるのは俗にメモリ DC と呼ばれる、画面表示とは直接関係のない DC。Windows はオブジェクト指向とか自称しておきながら DC の扱いに関しては複雑であり、事実上三種類の DC が存在する。
  • BeginPaint() による DC
    WM_PAINT メッセージのハンドラでウィンドウを再描画するときに用いる DC。EndPaint によって解放する。
  • GetDC() による DC
    WM_PAINT 以外のときにウィンドウ描画に用いる DC。ReleaseDC によって解放する。
  • CreateCompatibleDC() による DC
    ウィンドウ描画以外の用途に用いる DC。DeleteDC によって解放する。

  •  もうひとつ CreateDC() という API もあるのだが、これは印刷用くらいにしか用いない。私は一度も使ったことがないので解説はパス。

    SelectObject()
     転送元 DC にビットマップを「貼りつけて」いる。SelectObject() の概念は理解しにくいのだが、ビットマップは DC にとっての「キャンバス」であり、DC はキャンバスを操作する「画家」と考えてもらいたい。つまり GetDC() や BeginPaint() の場合はキャンバスが直接ウィンドウにつながっており、操作の結果が逐一画面に反映されるというわけだ。CreateComatibleDC() で作られた DC の「キャンバス」はメモリ上のビットマップである(だからメモリ DC と呼ぶわけ)。ここで hBmp を hDCBmp にセットしているのは、既に絵の描かれたキャンバスを画家に渡すようなものと考えてほしい。なお、GetDC() や BeginPaint() で取得した DC に対して SelectObject() で HBITMAP を渡すと動きがおかしくなるので注意。
    また、SelectObject() の返値は「直前に選択されていた描画オブジェクト」が返されることになっており、DC を削除または返却する前に、設定されていたオブジェクトはすべて初期状態に戻されなければならないことになっている。実際には SOLID BRUSH だの PEN だのは元に戻さなくても悪影響はないようなのだが、とりあえず SelectObject() をかけた後は必ず返値を覚えておいて元に戻しておいた方が無難ではある。しかし SelectObject() 一つで PEN, BITMAP, BRUSH, FONT, REGION 等が設定でき、各描画オブジェクトごとに別々のオブジェクトハンドルが返されるのでいちいち初期値を覚えておいて元に戻すのは非常に面倒くさい。

    BitBlt()
     BitBlt は DC から DC へビットマップの内容をコピーする API。引数は
  • 描画先デバイスコンテキスト
  • 描画先座標
  • 描画サイズ
  • 描画元デバイスコンテキスト
  • 描画元座標
  • 描画モード

  • となっている。「サイズ」の指定は「描画先」になっているが、例えば 100x100 のビットマップを 50x50 で描画しても縮小されるわけではなく、左上 1/4 が描画されるに過ぎない(拡大縮小したいときは StretchBlt() を使う)。それなら何故サイズ指定が「描画先」の引数になっているのは奇妙に感じるが、BitBlt には「描画元デバイスコンテキストを NULL にして描画モードを BLACKNESS とか WHITENESS に指定することで黒または白で矩形を塗り潰す」という機能を兼ねているためだ。矩形塗り潰し専用の FillRect() があるのに何をわざわざ…と思うが、この手の御都合主義にいちいち目くじらを立てていたら Windows プログラムなんてやってられない。「描画モード」には塗りつぶしやら論理演算やら色々種類があるが、普通のビットマップを普通に描画するには SRCCOPY さえ覚えておけば用は足りる。

     リスト 1-1 ではビットマップサイズを固定で扱ったが、関数 GetObject() を用いることにより hBmp からビットマップ画像に関する情報を得ることができる。ビットマップ画像のサイズや色数情報は BITMAP 構造体で返される。

    リスト 1-2 ビットマップサイズを取得

    	BITMAP	bmp;
    
    	GetObject(hBmp, sizeof(BITMAP), &bmp);
    	BitBlt(hDC, 0, 0, bmp.bmWidth, bmp.bmHeight, hDCBmp, 0, 0, SRCCOPY);
    
    GetObject()
     GetObject も紛らわしい GDI 関数のひとつで、渡されたハンドルの種類によって異なる動作をする。例えば HBITMAP を渡した場合は BITMAP 構造体(この名前も十分紛らわしいが…)を取得するのだが、HFONT を渡した場合は LOGFONT 構造体の値を取得するのだ。昔の Windows API のマニュアルはこれを誇らしげに「オブジェクト指向のポリモフィズム」なんて書いていた。Microsoft 社では DC が三種類もあって取得/削除の API 関数が違ったりするのも「ポリモフィズム」なのだろうか。


    2. ビットマップハンドル読み出しの方法

     さて順序が逆になるが、BitBlt を使ったビットマップ描画の方法がわかった(と強引に仮定)ところで hBmp の作成方法を見てみよう。普通に考えたら *.bmp ファイルから読み込むのが一番手っ取り早いようだが、あきれたことに Windows API には BMP ファイルに対する直接読み書きのサポートは無いのである。ちなみに VC++ の MFC クラスにも BMP ファイルの読み書きはなく、サンプルプログラムのソースとして提供されているに過ぎない。が、こんな所で挫折してられないのでトットと先に進もう。HBITMAP を返す API には次のものが用意されている。

    CreateBitmap
    CreateBitmapIndirect
    CreateCompatibleBitmap
    CreateDIBitmap
    CreateDiscardableBitmap
    LoadBitmap

     ここでは Create 何とかを後回しにして LoadBitmap() を見てみよう。これは実行ファイル(EXE や DLL)にリソースとして格納されたビットマップを取り出しハンドルを返す API である。リソースは *.rc ファイルからコンパイルされるが、VC++ を使う限り rc ファイルの中身はなるべく知らなくて済むようにはなっている(もちろん知っておいたほうが便利だし、知っておかなければならない場合もある)。
     例えばビットマップファイル test.bmp をリソースに追加する場合、VC++ の場合は RES ディレクトリの下にファイルをコピーし、「プロジェクトにリソースを追加」でビットマップを選択しファイルを読み込めばいい。この時リソース識別子(リソース ID) IDB_BITMAP1 が勝手に追加される。デフォルトの名前が気に入らなければリソースのプロパティで変えればいい(リソース ID の定義は resource.h にまとめて置かれる)。rc ファイルには次のような定義が追加されることになる。

    /////////////////////////////////////////////////////////////////////////////
    //
    // Bitmap
    //
    
    IDB_BITMAP1             BITMAP  DISCARDABLE     "res\\test.bmp"
    

     リソースが定義できれば、リソース ID を使ってアクセスすることができる。
    HBITMAP	hBmp;
    hBmp = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1));
    
     hInst はプログラムのインスタンスハンドル。DLL からロードする場合は DLL のモジュールハンドルを指定する。MAKEINSTANCESOURCE というのは昔のウィンドウの盲腸のようなもので、Windows 3.0 未満ではリソースの識別を「文字列」で行っていたのを、それ以降ではメモリ節約及び高速化のためにリソース ID「も」使えるようにしたのである。そのため LoadBitmap(に限らずリソースアクセス API の全て)が引数宣言が char ポインタなのにハンドル(ID)「も」渡せるという奇っ怪な仕様になってしまい、その辺のゴマカシをマクロで何か細工していたらしい。VC++ ではふつう文字列を使ったリソース識別は行わないが、互換性のためコンパイルできるようにはなっている。MFC の CBitmap クラスを使うなら
    CBitmap	bmp;
    bmp.LoadBitmap(IDB_BITMAP1);
     という風に hInst を省略することができ、MAKEINSTANCESOURCE などというオマジナイも不要になる(だからと言って格別便利になっている訳でもないが -_-;)。

     LoadBitmap() したハンドルは Load しっ放だとメモリリークの原因になるので、要が済んだら DeleteObject() で解放してやらなければならない。
    DeleteObject(hBmp);
     ちなみに CBitmap クラスの場合もデストラクタがハンドル解放の面倒を見てくれるわけではなく、DeleteObject() メソッドを呼んで明示的にハンドルを解放しなければならないのである。一体何のために C++ 言語を使っているのやらよくわからん。

     LoadBitmap(), CreateCompatibleDC(), SelectObject(), DeleteBitmap() を毎回描画前後にやるとオーバーヘッドが大きいので、プログラム初期化時・終了時にまとめて行った方がいいと思う。このまでの経緯をまとめたプログラムの例をリスト 2-1 に示す。これは昔懐かしい SDK 風のウィンドウプロシジャ。MFC を使う場合は WM_CREATE, WM_PAINT, WM_DESTROY がそれぞれ OnCrate, OnPaint, OnDestroy のメンバ関数になるだけで、基本的な操作には変わりがない(MFC は SDK からの移行を容易にするため、あえて単細胞に作られているらしい)。

    リスト 2-1 リソースを用いたビットマップの描画

    HBITMAP	hBmp;
    HBITMAP	hBmpPrev;
    HDC	hDCBmp;
    
    /* ウィンドウのコールバック関数 */
    LRESULT CALLBACK MainWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
    	HDC	hDC;
    	PAINTSTRUCT	ps;
    
    	switch (uMsg) {
    
    	case WM_CREATE:
    		/* ビットマップおよび DC の準備 */
    		hBmp = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1));
    		hDC = GetDC(hwnd);
    		hDCBmp = CreateCompatibleDC(hDC);
    		hBmpPrev = SelectObject(hDCBmp, hBmp);
    		ReleaseDC(hwnd, hDC);
    		return (0);
    
    	case WM_PAINT:
    		/* 画面再描画 */
    		hDC = BeginPaint(hwnd, &ps);
    		BitBlt(hDC, 0, 0, 100, 100, hDCBmp, 0, 0, SRCCOPY);
    		EndPaint(hwnd, &ps);
    		return (0);
    
    	case WM_DESTROY:
    		/* ビットマップおよび DC の後始末 */
    		SelectObject(hDCBmp, hBmpPrev);
    		DeleteObject(hBmp);
    		DeleteDC(hDCBmp);
    
    		/* アプリケーション終了要求 */
    		PostQuitMessage(0);
    		return (0);
    	}
    
    	return (DefWindowProc(hwnd, uMsg, wParam, lParam));
    }
    

    3. BMP ファイルについて

     さて、本題に入る前に BMP ファイルについて解説しておかねばならない。単に BMP ファイルと言っても単純ではなく、マニュアルを見ても BITMAPFILEHEADER, BITMAPINFO, BITMAPFILEINFO, BITMAPCOREINFO, BITMAPCOREHEADER, RGBTRIPLE, RGBQUAD といった構造体の定義が入り乱れていて、DIB がどうしたの RLE がこうしたの書いてあって何が何だかよくわからない。実はいわゆる「BMP ファイル」には新旧2種類のフォーマットがあるのだ。旧フォーマットは OS/2 互換で通称 DIB とも呼ばれるが、マニュアルでは "DIB" という言葉が3種類くらいの違った意味で混用されているので甚だわかりにくい。BITMAPCOREINFO というのはこの旧フォーマットの構造を表わし、新フォーマットに対応するのが BITMAPINFO なのである。
     1ピクセルあたりの色数を表わすビット深度として、旧 BMP には 1bit(2 色), 4bit(16 色), 8bit(256 色), 24bit(1677 万色) の4種類があり、新 BMP にはこれに加えて 16bit(65536 色) や 32bit(4 億色)もサポートされることになっている。「なっている」というのは 16bit と 32bit の BMP フォーマットは全然普及しておらず対応ソフトも少ないためだ(少なくとも僕は一度も見たことがない)。また、新 BMP では 4bit と 8bit に関して RLE と呼ばれる連長圧縮フォーマットをサポートしており、Windows 起動ロゴに応用されている。

     DIB も BMP も 8bit 以下のフォーマットはパレット・カラー・モデルと呼ばれ、ピクセル値はファイルヘッダ内にある色テーブル(パレット)へのインデックスで現わされる。注意すべきは 1bit 画像が必ずしも「白黒」とは限らず2色のパレットを持つこと。また DIB と BMP ではどういう訳か色情報の形式が違い、DIB の RGBTRIPLE は1色あたり3バイトで Blue, Green, Red だが、BMP の RGBQUAD は1色あたり4バイトで Blue, Green, Red, Dummy(reserved) の配列となることも注意。
     8bit より大きなビット深度(と言っても事実上 24bit だけ)ではピクセル値がそのまま色数を現わすため、ヘッダにパレットデータは含まれない。ピクセルの色並びはパレットと同様 Blue, Green, Red の順番である。表 3-1 に DIB および BMP のファイル構造について簡単に示す。

    表 3-1 DIB および BMP ファイルの構成
    DIB(<=8bit)DIB(>8bit)BMP(<=8bit)BMP(>8bit)
    BITMAPFILEHEADER
    BITMAPCOREINFO
      BITMAPCOREHEADER
      RGBTRIPLE(x2, 16 or 256)
    BITMAP DATA...
    
    BITMAPFILEHEADER
    BITMAPCOREINFO
      BITMAPCOREHEADER
    BITMAP DATA...
    
    BITMAPFILEHEADER
    BITMAPFILEINFO
      BITMAPFILEHEADER
      RGBQUAD(x2, 16 or 256)
    BITMAP DATA...
    
    BITMAPFILEHEADER
    BITMAPFILEINFO
      BITMAPFILEHEADER
    BITMAP DATA...
    

     ビットマップデータは画面左→右の順序で格納され、水平方向の1ライン分をスキャンラインと呼ぶ。スキャンラインは 32bit すなわち4の整数倍に切り上げられたバイト数で完結し、空いたスペースにはゴミが入る(普通はゼロで埋めておくのが良心的だろう)。数ある DIB/BMP の不思議のなかで最たるものは、スキャンラインが画像の下から上の順番で格納されていること。印刷用の PostScript 言語が座標原点を用紙の左上に置いているのは理解できるのだが、何故画面表示を主目的とする DIB/BMP の原点が下なのか理解に苦しむ。というか、Windows の仕様についていちいち理解しようとしてはいけないのかも知れない。
     ビットマップデータ本体はいわゆるパックド・ピクセルであり、1bit 画像なら1バイトあたり8ピクセル、4bit 画像なら2ピクセルの情報が含まれる。BMP ファイルのビット配列はどういう訳かビッグエンディアン、すなわち高位→低位の順が左→右の順番になる。intel 系 CPU はリトルエンディアンが標準なのに?と首を傾げるが、これは元祖 IBM-PC(intel 8088) が CGA の色並びをビッグエンディアンで設計してしまった仕様(とゆーかバグだぜこれは)に基づいているのだ。
     BITMAPFILEHEADER, BITMAPCOREHEADER, BITMAPINFOHEADER 構造体それぞれについて表 3-2 〜表 3-4 に示す。

    表 3-2 BITMAPFILEHEADER 構造体について
    メンバ型メンバ名解説
    WORDbfType識別バイト。BM(0x45 0x4d)でなければならない。
    DWORDbfSizeファイル自身の大きさ(*1)。
    WORDbfReserved1未使用(0x00)。
    WORDbfReserved2未使用(0x00)。
    DWORDbfOffBitsビットマップデータまでのオフセット(*2)。
    *1 bfSize
    ゼロだったりインチキな値が入っていることがあって信用できない。GetFileSize() 等を使ってファイルサイズを直接取得した方が確実。

    *2 bfOffBits
    オフセットというと難しく聞こえるが、要するにファイル先頭からビットマップデータ(スキャンデータ)直前までのヘッダバイト数の合計。ヘッダバイト数には BMPFILEHEADER 自身も含まれる。

    表 3-3 BITMAPCORHEADER 構造体について
    メンバ型メンバ名解説
    DWORDbcSizeBITMAPCOREHEADER 自身の大きさ(=16)。
    LONGbcWidth画像の横幅、ピクセル単位。
    LONGbcHeight画像の高さ、ピクセル単位。
    WORDbcPlanesプレーン数。常に1(*1)。
    WORDbcBitCountピクセルあたりのビット深度(*1)。
    *1 bcPlanes, bcBitCount
    昔の Windows では 4bit(16 色) の時のみ bcPlanes=4, bcBitCount=1 に設定する必要があった(VGA ハードウェアの VRAM 構成に合わせる必要があった?)が、面倒くさいんで Win95 以降では常に1で良くなったらしい。しかし BMP ファイルの中には bcPlanes=4, bcBitCount=1 のまま流通しているものもあるので要注意。両者は乗算しビット深度として扱ったほうが無難。

    表 3-4 BITMAPINFOHEADER 構造体について
    メンバ型メンバ名解説
    DWORDbiSizeBITMAPINFOHEADER 自身の大きさ(=40)。
    LONGbiWidth画像の横幅、ピクセル単位。
    LONGbiHeight画像の高さ、ピクセル単位。
    WORDbiPlanesプレーン数。常に1(*1)。
    WORDbiBitCountピクセルあたりのビット深度(*1)。
    DWORDbiCompression圧縮の有無。
    BI_RGB(0x00) 無圧縮通常形式
    BI_RLE8(0x01) 8bit 連長圧縮形式
    BI_RLE4(0x02) 4bit 連長圧縮形式
    BI_BITFIELDS(0x03) 無圧縮拡張形式(16/32bit)
    DWORDbiSizeImageビットマップデータの大きさ、バイト単位(*2)。
    LONGbiXPelsPerMeter画像水平方向解像度、ピクセル/m(*3)。
    LONGbiYPelsPerMeter画像垂直方向解像度、ピクセル/m(*3)。
    DWORDbiClrUsed画像で使用されている色数(*4)。
    DWORDbiClrImportant重要とされる色数(*5)。
    *1 biPlanes, biBitCount
    bcPlanes, BcBitCount と同様。

    *2 biSizeImage
    多くの場合はゼロが入っており「不明」を意味している。ゼロでなくてもインチキな値が入っているファイルが時々あるため、この値は無視した方が無難である。

    *3 biXPelsPerMeter, biYPelsPerMeter
    本当はゼロにしちゃいけない値なのだろうが、大抵の BMP ファイルではゼロになっている。まともな値が入っているファイルも少なければ、この値を何かに使うアプリケーションも皆無。ほとんど…と言うか全く意味がない。

    *5 biClrUsed
    非パレット形式の場合は常にゼロ。パレット形式の場合はこの後に続く RGBQUAD の配列数を示しているが、パレット形式なのにゼロが入っていた場合は「デフォルト」すなわち biPlanes と biBitCount によって決定される色数(1bit=2, 4bit=16, 8bit=256)を意味する。

    *5 biClrImportant
    これもほとんど無意味。とりあえずゼロにしておけば問題なし。


    4. BMP ファイルの読み込み

     ようやく本題である。上で述べたように BMP ファイルには種類が多くしかもモノによってヘッダの長さが違うので、段階的に読み込んでやらなければならない。読み出した BITMAPFILEINFO と BITMAPHEADERINFO から hBmp を作成するには CreateDIBitmap() を使う。リスト 4-1 に BMP ファイル読み込み関数を示す。なおここでは LoadBitmapFile というありがちな名前を使っているが、ありがちな関数名は往々にして Win32API や MFC のメンバ関数名と衝突するので本音を言えば避けたほうがいい。関数に限らず、グローバルシンボルの頭には自分専用のプレフィクス(czLoadBitMapFile とか)を付けておけば、将来の Win や VC でトラブルが起きる可能性を下げることができる(かも知れない)。

    リスト 4-1 ビットマップファイルの読み込み

    HBITMAP LoadBitmapFile(const char *szFileName)
    {
    	HANDLE	hFile;
    	HDC	hDC;
    	HBITMAP	hBmp;
    	DWORD	dwFileSize;
    	DWORD	dwHeaderSize;
    	DWORD	dwScanDataSize;
    	DWORD	dw;
    	BITMAPFILEHEADER	bmpFileHeader;
    	BYTE	*pHeaderBuffer;
    	BYTE	*pScanDataBuffer;
    	BITMAPINFOHEADER	*pBmpInfoHdr;
    
    	hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    
    	dwFileSize = GetFileSize(hFile, NULL);
    
    	ReadFile(hFile, &bmpFileHeader, sizeof(BITMAPFILEHEADER), &dw, NULL);
    	dwHeaderSize = bmpFileHeader.bfOffBits - sizeof(BITMAPFILEHEADER);
    	dwScanDataSize = dwFileSize - bmpFileHeader.bfOffBits;
    
    	pHeaderBuffer = new BYTE[dwHeaderSize];
    	pScanDataBuffer = new BYTE[dwScanDataSize];
    
    	ReadFile(hFile, pHeaderBuffer, dwHeaderSize, &dw, NULL);
    	ReadFile(hFile, pScanDataBuffer, dwScanDataSize, &dw, NULL);
    
    	CloseHandle(hFile);
    
    	hDC = GetDC(NULL);
    	pBmpInfoHdr = (BITMAPINFOHEADER*)pHeaderBuffer;
    
    	hBmp = CreateDIBitmap(hDC, pBmpInfoHdr, CBM_INIT, pScanDataBuffer, (BITMAPINFO*)pBmpInfoHdr, DIB_RGB_COLORS);
    
    	ReleaseDC(NULL, hDC);
    	delete[] pScanDataBuffer;
    	delete[] pHeaderBuffer;
    
    	return hBmp;
    }
    
    CreateFile()
    Win32 では読み出し用にファイルを開くのも CreateFile() である。Win3.1 時代には OpenFile() という API があったのだが、何故か CloseFile という API はなく _lclose で閉じるという変則的なものだった。Win32 以降ではファイルハンドルの型も HFILE から HANDLE に変更されており、両者を混ぜるとコンパイルエラーの嵐になるので要注意。


    ReadFile()
    これも Win32 で新設されたファイル読み出し API、以前は _lread() とか _hread() を混ぜて使っていた(_hread は悪名高い HUGE ポインタへの読み込みである)。C 言語標準の fread() 関数とは雰囲気が違い、返値 BOOL がエラーの有無、第四引数 DWORD* に「実際に読み出したバイト数」が返される。

    CloseHandle()
    前述したように Win3.1 以前では _lclose(HFILE) で閉じていたのだが、Win32 以降では CloseHandle(HANDLE) で閉じるようになった。

    GetDC()
    この後で呼び出す CreateDIBitmap() API には引数として DC を渡す必要がある。自分の hwnd が特定できる場合はそこから DC を取って使えばいいが、そうでない場合は NULL から取ればいい。昔はこれをシステム DC とか呼んでいたが、実はデスクトップウィンドウの DC なのである。NULL と書かずに HWND_DESKTOP(winuser.h で定義)と書いたほうが正しいのかも知れないが、Win32 のマニュアルでは何故かどちらの表現も削除されている。

    CreateDIBitmap()
    hBmp 作成の骨子となる API。例によって引数の組み合わせにより複数の機能を合わせ持つが、普通は CBM_INIT を指定して「初期化データを持つ」ことと、DIB_RGB_COLORS によって「初期化データの内容が RGB 値」であることを示す。マニュアルには DIB_PAL_COLORS という選択肢も出てくるが、これは見なかったことにしておいたほうがいい。

     意外にシンプルなプログラムだが、エラーチェックは全然行っていないので注意。CreateDIBitmap の第二引数は BITMAPINFOHEADER*、第五引数は BITMAPINFO* を渡すことになっているが、BITMAPCOREHEADER* および BITMAPCORE* を渡しても動いてくれるので DIB/BMP の違いを識別する必要はない(確か Win3.1 の時は処理が必要だったのだが…)。
     さて今時の普通のパソコン環境であればリスト 4-1 そのままで問題ないのだが、256 色画面モードだと派手に色化けを起こしてしまう。256 色モードでは「パレット」を扱わないと色を正しく扱えないのだが、このパレットという奴は異常に手強い(マニュアルは意味不明だし嘘が書いてあるし実際の動作はバグだらけだし)のでここでは説明しない。とりあず256 色パレットグラフィックモードの存在は忘れておいたほうが身のためとだけ言っておこう(^_^;)。

     BMP ファイルの書き出しも大体リスト 4-1 の順序を逆にしたものになる。CreateDIBitmap() の逆に相当する API が GetDIBits() であるが、引数の渡し方は若干異なるので注意が必要。また、スキャンデータに必要な領域の大きさを得るには GetDIBits() をスキャンラインポインタ = NULL として呼び出し、返された BITMAPINFOHEADER 構造体のメンバを参照する必要があるため、GetDIBits() は都合二回呼び出されることになる。
     リスト 4-2 に BMP ファイル書き出しの関数を示す。この関数の第三引数は保存する BMP の色数を示し、0 のときは 24bit(1677 万色) を示す仕様にしてある。

    リスト 4-2 ビットマップファイルの書き出し

    BOOL SaveBitmapFile(const HBITMAP hBmp, const char *szFileName, const int iColors)
    {
    	HANDLE	hFile;
    	HDC	hDC;
    	DWORD	dw;
    	DWORD	dwHeaderSize;
    	DWORD	dwScanDataSize;
    	BITMAPFILEHEADER	bmpFileHeader;
    	BITMAPINFOHEADER	*pBmpInfoHdr;
    	BYTE	*pHeaderBuffer;
    	BYTE	*pScanDataBuffer;
    	BITMAP	bmp;
    
    	GetObject(hBmp, sizeof(BITMAP), &bmp);
    
    	dwHeaderSize = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * iColors;
    	pHeaderBuffer = new BYTE[dwHeaderSize];
    	memset(pHeaderBuffer, 0, dwHeaderSize);
    
    	pBmpInfoHdr = (BITMAPINFOHEADER*)pHeaderBuffer;
    	switch (iColors) {
    	case 2:
    		pBmpInfoHdr->biBitCount = 1;
    		break;
    	case 16:
    		pBmpInfoHdr->biBitCount = 4;
    		break;
    	case 256:
    		pBmpInfoHdr->biBitCount = 8;
    		break;
    	case 0:
    		pBmpInfoHdr->biBitCount = 24;
    		break;
    	}
    
    	pBmpInfoHdr->biSize = sizeof(BITMAPINFOHEADER);
    	pBmpInfoHdr->biWidth  = bmp.bmWidth;
    	pBmpInfoHdr->biHeight = bmp.bmHeight;
    	pBmpInfoHdr->biPlanes = 1;
    	pBmpInfoHdr->biCompression = BI_RGB;
    	pBmpInfoHdr->biSizeImage = 0;
    	pBmpInfoHdr->biXPelsPerMeter = 0;
    	pBmpInfoHdr->biYPelsPerMeter = 0;
    	pBmpInfoHdr->biClrUsed = 0;
    	pBmpInfoHdr->biClrImportant = 0;
    
    	hDC = GetDC(NULL);
    	GetDIBits(hDC, hBmp, 0, bmp.bmHeight, NULL, (LPBITMAPINFO)pBmpInfoHdr, DIB_RGB_COLORS);
    	dwScanDataSize = pBmpInfoHdr->biSizeImage;
    	pScanDataBuffer = new BYTE[dwScanDataSize];
    	GetDIBits(hDC, hBmp, 0, bmp.bmHeight, pScanDataBuffer, (LPBITMAPINFO)pBmpInfoHdr, DIB_RGB_COLORS);
    	ReleaseDC(NULL, hDC);
    
    	bmpFileHeader.bfType = 0x4d42;	/* "BM" */
    	bmpFileHeader.bfReserved1 = 0;
    	bmpFileHeader.bfReserved2 = 0;
    
    	bmpFileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + dwHeaderSize;
    	bmpFileHeader.bfSize = dwScanDataSize + bmpFileHeader.bfOffBits;
    
    	hFile = CreateFile(szFileName, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    	WriteFile(hFile, &bmpFileHeader, sizeof(BITMAPFILEHEADER), &dw, NULL);
    	WriteFile(hFile, pHeaderBuffer, dwHeaderSize, &dw, NULL);
    	WriteFile(hFile, pScanDataBuffer, dwScanDataSize, &dw, NULL);
    	CloseHandle(hFile);
    
    	delete[] pScanDataBuffer;
    	delete[] pHeaderBuffer;
    
    	return TRUE;
    }
    
    GetDIBits()
    hBmp から BITMAPINFO およびスキャンデータを作成する API。CreateDIBitmap() と対を成すのだが、CreateDIBitmap() が BITMAPINFO と BITMAPINFOHEADER を別々に渡していたのに対し、何故か BITMAPINFO 一つしか渡さない。その代わり?スキャンデータを取得する「開始走査線(第二引数、リストでは 0)」と「走査線総数(第三引数、リストでは bmp.bmHeight」を渡すことで「画像の一部を切り抜く(ただし垂直範囲にだけ)ことができる」仕様になっている。この仕様が特に便利だとは思わないのだが…。この関数の返値は「コピーされた走査線の数を返し == 0 はエラーを示す」ことになっているのだが、使用するビデオドライバによっては成功しても 0 を返す実装があったりするので当てにならない。
     BITMAPINFO 用のヘッダ領域を確保するにあたり、色数×sizeof(RGB) の領域を加えていることに注意。これによって必要なパレット領域を確保している。フルカラーの場合パレットは必要ないが、これは iColors = 0 で示されるのでパレット領域は確保されないという仕組みになっている。
     例によってエラーチェックは省略しているが、上にも書いたように GDI 関数の返値によるエラー通知というのはあまり信用できず、あまり仕様書通りにエラーチェックを厳密にしてしまうと動かないことが往々にしてあるので要注意である。また、Win3.1 時代の経験では一度目の GetDIBits() 呼び出しで biSizeImage の値を正しくセットしてくれず(0 になってしまう)、自分でスキャンデータの大きさを推定する必要のあるドライバもあった(Tsueng ET-4000/256…今時こんなもん使ってる奴ぁ居ないだろうけど)。
     なお、GetDIBits() は「hBmp がどの DC にも選択されていない状態」で呼び出すべきらしい(少なくとも Win3.1 の時はそうだった)ので、リスト 4-2 の関数を呼び出すときには
    	SelectObject(hDCBmp, hBmpPrev);
    	SaveBitmapFile(hBmp, "test2.bmp", 256);
    	hBmpPrev = SelectObject(hDCBmp, hBmp);
    
    とやっておいた方が無難だと思う。


    5. 総括

     以上の手順を全て内包したテストプログラムを添付しておく。プロジェクトファイルは VitualC++ 5.0 のもの。左クリックで BMP ファイルの読み込み、右クリックで書き出しを行う。なお、1/4/8/24bit それぞれの BMP および DIB、4/8bit の RLE それぞれのフォーマットで作成した BMP ファイルのサンプルも添付する。ファイル名末尾に "p" の付くファイルはパレット配置を「最適化」したもの。画面を 256 色モードにして表示させれば、これら最適化パレットファイルが色化けするのがわかると思う。こういったファイルを 256 色モードでも表示させようと思ったら、Winodws GDI 悪夢中の悪夢と言われる「パレットマネージャ」の虎口をくぐり抜けなければならない。

    bmptest.lzh
    BMP ファイル読み出し・保存プログラム(15.8Kbyte)

    testbmp.lzh
    各種 BMP フォーマット例(76.4Kbyte)

    [WINSDK] [INDEX]
    Copyright 2000 by 'Crazy' Y.Sasaki