DIBをBMPファイルに保存する

概要:DIBをBMPファイルに保存する関数を作成します

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標準ソフトでも表示できた事で、ファイルの正当性が確認できます。


戻る / ホーム