BMPファイルの構造を良く理解していれば、読み込みも書き込みも大して変わりありません。
今回はDIBをBMPファイルとして保存する関数を作成します。
■BMPファイルの構造
| アドレス | データ |
| 0 | BITMAPFILEHEADER構造体 |
| + sizeof(BITMAPFILEHEADER) | BITMAPINFOHEADER構造体 |
| ( BITMAPINFO構造体bmiColorsメンバ ) | ( カラーテーブル ) |
| + BITMAPFILEHEADER構造体bfOffBitsメンバ | ピクセル列 |
大雑把に言えば、
BITMAPFILEHEADER構造体、BITMAPINFO構造体、ピクセル列
が順番に記録されています。
カラーテーブルは実際に使われている個数だけ記録します。
また、カラーテーブルが不要ならば、
BITMAPINFO構造体ではなく、BITMAPINFOHEADER構造体だけを記録する事に注意して下さい。
ファイルには極力無駄な情報は記録しません。
■関数の設計
対応するDIBのビット数は32/24/8ビットにします。
保存時にビット数を変換する事もないと思われるので、そのまま保存すればいいでしょう。
ただし32ビットだけは一般的ではないので、24ビットに変換して保存します。
同様に、もし16ビットDIBを保存したい場合は24ビットに変換して保存します(今回は16ビットには対応しません)。
関数名 → SaveDIB
引数 → ファイル名 , ピクセル列のポインタ , BITMAPINFO構造体のポインタ
戻り値 → 0 (成功) or 負値(失敗)
int SaveDIB(char *lpFileName,const BYTE *lpPixel,const BITMAPINFO *lpBmpInfo)
{
……
}
32ビットDIBの場合は24ビットに変換してBMPファイルに保存する関数なので、
BITMAPINFO構造体を書き換えて記録する事になります。
実際にはこの引数が指す値を書き換える事はありませんが、
そのような要らぬ不安を消すために const を付けています。
ポインタに const が付くと、そのポインタを介した値を書き換える事ができなくなります。
■BITMAPINFOHEADER構造体の複製
32ビットDIBは24ビットに変換します。
変換したらBITMAPINFOHEADER構造体のbiBitCountメンバを書き換えなければなりません。
しかし元の構造体を書き換えるわけにはいかないので、
関数内部でローカル変数を作って、元の内容をコピーします。
BITMAPINFOHEADER bmpInfoH; CopyMemory(&bmpInfoH,&lpBmpInfo->bmiHeader,sizeof(BITMAPINFOHEADER));
この時、BITMAPINFO構造体をコピーしても、カラーテーブルを指すポインタは得られません。
bmiColors はカラーテーブルの先頭を指すポインタですが、
構造体のメンバはあくまでもその実体 bmiColors[1] ですから、
ポインタではなく、RGBQUAD構造体の数値そのものがコピーされます。
これらは別々の領域を割り当てられているので、当然ポインタのアドレスも違い、
元のカラーテーブルの先頭を指すポインタは得られません。
つまり、関数に実引数のコピーを渡す事で、
元の情報を変更する事無しに仮引数の値を変更する……という方法は使えません。
■BITMAPINFOHEADER構造体の設定
複製したBITMAPINFOHEADER構造体を調べて、必要なら書き換えます。
int bitCount=bmpInfoH.biBitCount;
if(bitCount!=32 && bitCount!=24 && bitCount!=8){
char str[8];
wsprintf(str,"%d",bitCount);
MessageBox(NULL,"対応していないビット数です",str,MB_OK);
return -1;
}
if(bitCount==32) bmpInfoH.biBitCount=24;
int w=bmpInfoH.biWidth , h=bmpInfoH.biHeight;
DWORD nColorTable=bmpInfoH.biClrUsed;
if(bitCount==8 && nColorTable==0) nColorTable=256;
int len;
if(w*(bitCount/8)%4) len=w*(bitCount/8)+(4-w*(bitCount/8)%4);
else len=w*(bitCount/8);
■BITMAPFILEHEADER構造体の設定
BITMAPFILEHEADER構造体はBMPファイル特有の情報なので、
関数内部でローカル変数として作って、設定していきます。
ここで注意が必要なのは bfOffBits メンバです。
bfOffBits はBMPファイルの先頭からピクセル列の先頭までのバイト数です。
カラーテーブルの無い32/24ビットDIBと、カラーテーブルの有る8ビットでは異なる事に注意して下さい。
BITMAPFILEHEADER bmpfh;
bmpfh.bfSize=sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER)+len*h;
bmpfh.bfType=('M'<<8)+'B';
bmpfh.bfOffBits=sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER);
if(bitCount==8){
bmpfh.bfSize+=nColorTable*4;
bmpfh.bfOffBits+=nColorTable*4;
}
■ファイル作成
書き込み形式でファイルを作成します。
上書きは行わないようにします。
HANDLE fh=CreateFile(lpFileName,GENERIC_WRITE,0,NULL,
CREATE_NEW,FILE_ATTRIBUTE_NORMAL,NULL);
if(fh==INVALID_HANDLE_VALUE){
MessageBox(NULL,"同名のファイルが既に存在しています",lpFileName,MB_OK);
return -2;
}
■ファイル書き込み
ファイルの書き込みは WriteFile 関数を使って、
BITMAPFILEHEADER構造体、BITMAPINFOHEADER構造体、カラーテーブル(存在すれば)
の順番で書き込んでいきます。
ここがファイル書き込みのキーポイントかもしれませんが、わかっちゃえば簡単ですよね。
できるだけまとめて一気に書き込むイメージです。
DWORD dwWriteSize;
WriteFile(fh,&bmpfh,sizeof(BITMAPFILEHEADER),&dwWriteSize,NULL);
WriteFile(fh,&bmpInfoH,sizeof(BITMAPINFOHEADER),&dwWriteSize,NULL);
if(bitCount==8)
WriteFile(fh,lpBmpInfo->bmiColors,nColorTable*4,&dwWriteSize,NULL);
■ピクセル列の書き込み
24/8ビットDIBはそのまま書き込むだけなので簡単ですが、
32ビットDIBは24ビットに変換して書き込まなければならないので、少々面倒です。
従って、32ビットDIBのピクセル列の書き込み処理は別の関数としてまとめました。
24/8ビットDIBの場合は、全部まとめて一気に書き込む事ができます。
if(bitCount==32) Write32(fh,(LPDWORD)lpPixel,w,h); else WriteFile(fh,lpPixel,len*h,&dwWriteSize,NULL);
■32ビットDIBを24ビットとして書き込む
基本は4バイト毎に参照して、先頭から3バイト(下位3バイト)だけ書き込んでいく事の繰り返しですが、
24ビットとして書き込む事で、一行分のバイト数が4の倍数ではなくなる可能性があります。
従って、必要なら4の倍数にするための余分なバイトを書き込む必要があります。
これは見落としやすい処理なので注意して下さい。
void Write32(HANDLE fh,const DWORD *lpPixel,int w,int h)
{
int extra;
if(w*3%4) extra=4-w*3%4;
else extra=0;
DWORD dwWriteSize;
int zero=0;
for(int y=0;y<h;y++){
for(int x=0;x<w;x++)
WriteFile(fh,lpPixel+x+y*w,3,&dwWriteSize,NULL);
if(extra) WriteFile(fh,&zero,extra,&dwWriteSize,NULL);
} //一行分のバイト数を4の倍数に補正
}
■終了処理
ファイルを閉じて、関数を終了します。
CloseHandle(fh); return 0;
■関数の呼び出し
32ビットDIBを作成して、24ビットBMPファイルとして保存してみます。
#define WIDTH 197
#define HEIGHT 100
LRESULT CALLBACK WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static LPDWORD lpPixel;
static BITMAPINFO bmpInfo;
int x,y;
switch(uMsg) {
case WM_CREATE:
lpPixel=(LPDWORD)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,WIDTH*HEIGHT*4);
//DIBの情報を設定する
bmpInfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth=WIDTH;
bmpInfo.bmiHeader.biHeight=HEIGHT;
bmpInfo.bmiHeader.biPlanes=1;
bmpInfo.bmiHeader.biBitCount=32;
bmpInfo.bmiHeader.biCompression=BI_RGB;
//描画
for(y=25;y<50;y++){
for(x=25;x<50;x++){
lpPixel[x+y*WIDTH]=0x00ff0000; //赤
}
}
//BMPファイルに保存
SaveDIB("dib32_bmp24.bmp",(LPBYTE)lpPixel,&bmpInfo);
return 0;
case WM_DESTROY:
HeapFree(GetProcessHeap(),0,lpPixel);
PostQuitMessage(0);
return 0;
case WM_PAINT:
hdc=BeginPaint(hWnd,&ps);
//表画面へ転送
StretchDIBits(hdc,0,0,WIDTH,HEIGHT,
0,0,WIDTH,HEIGHT,lpPixel,&bmpInfo,DIB_RGB_COLORS,SRCCOPY);
EndPaint(hWnd,&ps);
return 0;
}
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

★☆ ダウンロード ☆★
次に、24ビットDIBを作成して、24ビットBMPファイルとして保存してみます。
#define WIDTH 197
#define HEIGHT 100
#define BITCOUNT 24
LRESULT CALLBACK WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static LPBYTE lpPixel;
static BITMAPINFO bmpInfo;
static int length;
int x,y;
switch(uMsg) {
case WM_CREATE:
//4の倍数に補正
if(WIDTH*(BITCOUNT/8)%4) length=WIDTH*(BITCOUNT/8)+(4-WIDTH*(BITCOUNT/8)%4);
else length=WIDTH*(BITCOUNT/8);
lpPixel=(LPBYTE)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,length*HEIGHT);
//DIBの情報を設定する
bmpInfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth=WIDTH;
bmpInfo.bmiHeader.biHeight=HEIGHT;
bmpInfo.bmiHeader.biPlanes=1;
bmpInfo.bmiHeader.biBitCount=BITCOUNT;
bmpInfo.bmiHeader.biCompression=BI_RGB;
//描画
for(y=25;y<50;y++){
for(x=25;x<50;x++){
lpPixel[(x*3 )+y*length]=0; //B
lpPixel[(x*3+1)+y*length]=255; //G
lpPixel[(x*3+2)+y*length]=0; //R
}
}
//BMPファイルに保存
SaveDIB("dib24_bmp24.bmp",lpPixel,&bmpInfo);
return 0;
case WM_DESTROY:
HeapFree(GetProcessHeap(),0,lpPixel);
PostQuitMessage(0);
return 0;
case WM_PAINT:
hdc=BeginPaint(hWnd,&ps);
//表画面へ転送
StretchDIBits(hdc,0,0,WIDTH,HEIGHT,
0,0,WIDTH,HEIGHT,lpPixel,&bmpInfo,DIB_RGB_COLORS,SRCCOPY);
EndPaint(hWnd,&ps);
return 0;
}
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

★☆ ソースファイルの表示 ☆★
次に、8ビットDIBを作成して、8ビットBMPファイルとして保存してみます。
使用するカラーテーブルは100個です。
#define WIDTH 197
#define HEIGHT 100
#define BITCOUNT 8
#define USECOLOR 100
LRESULT CALLBACK WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static LPBYTE lpPixel;
static LPBITMAPINFO lpBmpInfo;
static int length;
int x,y,i;
switch(uMsg) {
case WM_CREATE:
//4の倍数に補正
if(WIDTH*(BITCOUNT/8)%4) length=WIDTH*(BITCOUNT/8)+(4-WIDTH*(BITCOUNT/8)%4);
else length=WIDTH*(BITCOUNT/8);
lpPixel=(LPBYTE)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,
length*HEIGHT+sizeof(BITMAPINFO)+sizeof(RGBQUAD)*(USECOLOR-1));
lpBmpInfo=(LPBITMAPINFO)(lpPixel+length*HEIGHT);
//カラーテーブルを設定する
for(i=0;i<USECOLOR;i++){
lpBmpInfo->bmiColors[i].rgbBlue=(256-i)%256;
lpBmpInfo->bmiColors[i].rgbGreen=0;
lpBmpInfo->bmiColors[i].rgbRed=0;
}
//DIBの情報を設定する
lpBmpInfo->bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
lpBmpInfo->bmiHeader.biWidth=WIDTH;
lpBmpInfo->bmiHeader.biHeight=HEIGHT;
lpBmpInfo->bmiHeader.biPlanes=1;
lpBmpInfo->bmiHeader.biBitCount=BITCOUNT;
lpBmpInfo->bmiHeader.biCompression=BI_RGB;
lpBmpInfo->bmiHeader.biClrUsed=USECOLOR;
//描画
for(i=0,y=25;y<50;y++){
for(x=25;x<50;x++){
lpPixel[x+y*length]=(++i)%USECOLOR;
}
}
//BMPファイルに保存
SaveDIB("dib8_bmp8_1.bmp",lpPixel,lpBmpInfo);
return 0;
case WM_DESTROY:
HeapFree(GetProcessHeap(),0,lpPixel);
PostQuitMessage(0);
return 0;
case WM_PAINT:
hdc=BeginPaint(hWnd,&ps);
//表画面へ転送
StretchDIBits(hdc,0,0,WIDTH,HEIGHT,
0,0,WIDTH,HEIGHT,lpPixel,lpBmpInfo,DIB_RGB_COLORS,SRCCOPY);
EndPaint(hWnd,&ps);
return 0;
}
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

★☆ ソースファイルの表示 ☆★
8ビットの保存に注目してみましょう。
上の画像ではよくわからないので、青い領域を10倍に拡大してみます。

ご覧の通り、100個のカラーテーブルを使って描画しているのがわかります。
全てのカラーテーブルが正しく認識されたという事です。
また、あまり親切ではないWindows標準ソフトでも表示できた事で、ファイルの正当性が確認できます。