BMPファイルを32ビットDIBとして読み込む

概要:BMPファイルの構造を理解しよう

BMPファイルの構造は簡単です。
でもプログラムは長くなるので、全体の流れを見失わないようにしましょう。
今回はBMPファイルを32ビットDIBとして読み込む便利関数を作成します。

■BMPファイルの構造

BMPファイルの構造を以下に示します。

アドレス データ
0 BITMAPFILEHEADER構造体
+ sizeof(BITMAPFILEHEADER) BITMAPINFOHEADER構造体
( BITMAPINFO構造体bmiColorsメンバ ) ( カラーテーブル )
+ BITMAPFILEHEADER構造体bfOffBitsメンバ ピクセル列

大きく分けて、BITMAPFILEHEADER 構造体が先頭にあって、
先頭から sizeof(BITMAPFILEHEADER) バイトだけ後ろに BITMAPINFO 構造体、
先頭から BITMAPFILEHEADER構造体bfOffBitsメンバ バイトだけ後ろにピクセル列があります。

カラーテーブルの有無や長さは不定なので、
ピクセル列のアドレスは固定ではない事に注意して下さい。

また、カラーテーブルの有無によって、
BITMAPINFO構造体であるか、BITMAPINFOHEADER構造体であるかの違いもあります。

ファイルには極力無駄な情報は書き込まないようになっているので、
カラーテーブル無しの形式ではBITMAPINFO構造体ではなく、
そのメンバであるBITMAPINFOHEADER構造体だけがが書き込まれています。

■BITMAPFILEHEADER構造体

typedef struct tagBITMAPFILEHEADER {
  WORD bfType;
  DWORD bfSize;
  WORD bfReserved1;
  WORD bfReserved2;
  DWORD bfOffBits;
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;


bfType は必ず BM です。ビットマップである証です。
bfSize はBMPファイル全体のバイト数です。
bfOffBits はピクセル列の先頭アドレスがBMPファイルの先頭から何バイト目にあるかです。
bfOffBits 自体がアドレスになっているわけではありません。

■BITMAPINFO構造体

typedef struct tagBITMAPINFO {
  BITMAPINFOHEADER bmiHeader;
  RGBQUAD bmiColors[1];
} BITMAPINFO;


bmiColors はカラーテーブルの先頭を指すポインタです。
ここから BITMAPINFOHEADER構造体のbiClrUsed 個だけカラーテーブルが続き、
その後にピクセル列が続きます。
でも biClrUsed は
カラー テーブル内のカラー インデックスのうち、ビットマップ内で実際に使うインデックスの数
ですから、実際には使わないカラーテーブルも保存されていないとも限りません。
従って、ピクセル列の先頭アドレスは BITMAPFILEHEADER構造体のbfOffBitsメンバ を使いましょう。

■BITMAPINFOHEADER構造体

typedef struct tagBITMAPINFOHEADER {
  DWORD biSize;
  LONG biWidth;
  LONG biHeight;
  WORD biPlanes;
  WORD biBitCount;
  DWORD biCompression;
  DWORD biSizeImage;
  LONG biXPelsPerMeter;
  LONG biYPelsPerMeter;
  DWORD biClrUsed;
  DWORD biClrImportant;
} BITMAPINFOHEADER;


ファイルの読み込みに使うのは
biWidth(横幅) , biHeight(高さ) , biBitCount(ビット数) , biClrUsed(カラーテーブルの個数)
くらいでしょう。

■BMPファイル読み込みの概要

大きく分けると以下のような手順で読み込みます。

1.ファイルの内容全てをメモリ領域にコピー
2.メモリ領域のBITMAPFILEHEADER構造体、BITMAPINFO構造体、ピクセル列の先頭を指すポインタを設定する
3.構造体から情報を読み取り、ビットマップ作成に必要なバッファを確保する
4.メモリ領域からバッファに必要な情報をコピーしていく

特徴的なのは一旦ファイルの内容全てをメモリ領域にコピーしてしまう事でしょうか。
ファイルのままではアクセスしづらいからです。

また、作成するビットマップはBMPファイルの内容そのままである必要はありません。

■BMPファイルを32ビットDIBとして読み込む

これまでの経験からもBMPファイルを32ビットDIBとして読み込んでおけば後々使いやすそうです。
それでは何ビットからの変換に対応するのか、というのが設計上重要になりますが、
圧倒的なシェアを誇る24ビットと8ビットに対応しておけば問題ないでしょう。
32ビットBMPファイルというのは一般には使われていません。

■関数の設計

非常によく使われる処理ですし、プログラムも長いので関数としてまとめちゃいましょう。

関数名 → CreateDIB32fromFile248
引数 → ファイル名 , ピクセル列のポインタのポインタ , BITMAPINFO構造体のポインタ
戻り値 → 0 (成功) or 負値(失敗)

int CreateDIB32fromFile248(char *lpFileName,LPDWORD *lppPixel,BITMAPINFO *lpBmpInfo)
{
  ……
}

関数はピクセル列のメモリ領域を確保してくれますが、BITMAPINFO構造体のメモリ領域は確保しません。

ポインタのポインタというのが分かりづらいと思いますが、ポインタの値を変更したいからです。
実引数に変数そのものを与えても仮引数に渡されるのはその値のコピーに過ぎません。
その変数の値を関数内部から変更したいのであれば、
変数のポインタを実引数に与えなければなりません。

同様にポインタの指すアドレスを変更してもらいたいのであれば、
ポインタのポインタを実引数に与えればいいのです。

■ファイルの内容全てをメモリ領域にコピー

ファイルを開いて、ファイルと同じサイズのメモリ領域を確保、
そしてオールコピー、ファイルを閉じる、までは説明不要ですよね?

HANDLE fh;
DWORD dwFileSize,dwReadSize;
LPBYTE lpbuf;

fh=CreateFile(lpFileName,GENERIC_READ,0,NULL,
    OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(fh==INVALID_HANDLE_VALUE){
    MessageBox(NULL,"ファイルが開けません",lpFileName,MB_OK);
    return -1;
}
dwFileSize=GetFileSize(fh,NULL);
lpbuf=(LPBYTE)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,dwFileSize);
ReadFile(fh,lpbuf,dwFileSize,&dwReadSize,NULL);
CloseHandle(fh);

■BITMAPFILEHEADER構造体とBITMAPINFO構造体の先頭を指すポインタを設定する

BITMAPFILEHEADER構造体はファイルの先頭にあります。
その後ろにはBITMAPINFO構造体があります。
これらを指すポインタを設定して、
BMPファイルであるか、読み込みに対応しているビット数かどうかを調べます。

BITMAPINFO構造体は、実際にはBITMAPINFOHEADER構造体かもしれませんが、
BITMAPINFO構造体としておけば、どちらにも対応できます。

LPBITMAPFILEHEADER lpbmpfh;
LPBITMAPINFO lpbmpInfo;
int bitCount;

lpbmpfh=(LPBITMAPFILEHEADER)lpbuf;
lpbmpInfo=(LPBITMAPINFO)(lpbuf+sizeof(BITMAPFILEHEADER));
bitCount=lpbmpInfo->bmiHeader.biBitCount;
if(lpbmpfh->bfType!=('M'<<8)+'B' || (bitCount!=24 && bitCount!=8)){
    HeapFree(GetProcessHeap(),0,lpbuf);
    MessageBox(NULL,"24 or 8 ビットBMPファイルしか読み込めません",lpFileName,MB_OK);
    return -2;
}

■情報を読み取る

横幅と高さを取得して、ピクセル列の一行分のバイト数を計算します。

そして、横幅と高さを基に32ビットDIBのピクセル列のメモリ領域を確保します。
32ビットの場合は一行分のバイト数を気にする必要がないので、
BMPファイルの横幅をそのまま計算に使います。

ピクセル列の先頭を指すポインタを設定したら、ピクセル列の詰め替え作業の準備完了です。

LPBYTE lpbmpPixel;
int iWidth,iHeight,iLength;

lpbmpPixel=lpbuf + lpbmpfh->bfOffBits;
iWidth=lpbmpInfo->bmiHeader.biWidth;
iHeight=lpbmpInfo->bmiHeader.biHeight;

if(iWidth*(bitCount/8)%4) iLength=iWidth*(bitCount/8)+(4-iWidth*(bitCount/8)%4);
else iLength=iWidth*(bitCount/8);

*lppPixel=(LPDWORD)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,iWidth*iHeight*4);

■24ビットBMPファイルのピクセル列の詰め替え

今回作るプログラムは24ビットと8ビットからの読み込みに対応していますが、
アルゴリズムの基本は24ビットの読み込みにあります。

24ビットの場合は、横幅にさえ気を付ければ、3バイトずつコピーしていくだけです。

for(y=0;y<iHeight;y++)
    for(x=0;x<iWidth;x++)
        CopyMemory(*lppPixel+x+y*iWidth,lpbmpPixel+x*3+y*iLength,3);

*lppPixel はLPDWORD型、lpbmpPixel はLPBYTE型である事に注意して下さい。

■8ビットBMPファイルのピクセル列の詰め替え

8ビットの場合は、ピクセル列の値の意味を理解していないといけません。
カラーテーブルの番号でしたね。
従って、ピクセル列の値をカラーテーブルに渡せば、RGB値が得られるという事です。

上記の事柄をそのまま表現すれば カラーテーブル[ ピクセル列[ 0 ] ] が最初のRGB値になりますが、
カラーテーブルは RGBQUAD 構造体です。
構造体から他の型への代入はできないので、CopyMemory 関数を使う事になりますが、
それならば下記のようにアドレス計算式に変換した方がスッキリします。

LPRGBQUAD lpColorTable;

lpColorTable=lpbmpInfo->bmiColors;
for(y=0;y<iHeight;y++)
    for(x=0;x<iWidth;x++)
        CopyMemory(*lppPixel+x+y*iWidth,lpColorTable+lpbmpPixel[x+y*iLength],3);

LPRGBQUAD ではなく、LPDWORD でカラーテーブルの先頭を指せば、
次のように代入する事も可能です。

LPDWORD lpColorTable;
(*lppPixel)[x+y*iWidth]=lpColorTable[lpbmpPixel[x+y*iLength]]


lppPixel を LPDWORD* から LPDWORD にキャストしてから配列扱いしている事に注意して下さい。

RGBQUAD構造体 と DWORD型 はRGBの並びが丁度反対になっているので、
RGBQUAD構造体 としてメモリに書き込まれて、バイトの順番が反転したデータ を DWORD型 と見なせば、
上手い事RGB値が DWORD型 として取得できるという寸法です。

typedef struct tagRGBQUAD {
  BYTE rgbBlue;
  BYTE rgbGreen;
  BYTE rgbRed;
  BYTE rgbReserved;
} RGBQUAD;


24ビットと8ビットの処理をまとめると以下のようになります。

LPRGBQUAD lpColorTable;

switch(bitCount){
    case 24:
        for(y=0;y<iHeight;y++)
            for(x=0;x<iWidth;x++)
                CopyMemory(*lppPixel+x+y*iWidth,lpbmpPixel+x*3+y*iLength,3);
        break;
    case 8:
        lpColorTable=lpbmpInfo->bmiColors;
        for(y=0;y<iHeight;y++)
            for(x=0;x<iWidth;x++)
                CopyMemory(*lppPixel+x+y*iWidth,lpColorTable+lpbmpPixel[x+y*iLength],3);
        break;
}

■BITMAPINFO構造体の設定

カラーテーブルは必要なら既にコピーされていますから、
BITMAPINFOHEADER構造体だけコピーします。
ただし今回は32ビットに変換しているので、ビット数だけ後で設定し直します。

これで全ての処理が終了したので、BMPファイルの内容全てをコピーしていたメモリ領域を解放しましょう。

CopyMemory(lpBmpInfo,&lpbmpInfo->bmiHeader,sizeof(BITMAPINFOHEADER));
lpBmpInfo->bmiHeader.biBitCount=32;

HeapFree(GetProcessHeap(),0,lpbuf);
return 0;

■DIBを削除する関数

実際には削除ではなくピクセル列のメモリ領域解放ですが、
関数が作ってくれた得体の知れない物は関数が消すのがセオリーです。
わざわざ関数にするまでもない短い処理かもしれませんが、カプセル化ってやつです。

void DeleteDIB32(LPDWORD *lppPixel)
{
    if(*lppPixel!=NULL){
        HeapFree(GetProcessHeap(),0,*lppPixel);
        *lppPixel=NULL;
    }
}

ここでもポインタのポインタを受け取っています。
いつまでも無効な領域を指されては困るので、ポインタに NULL を設定するためです。

■関数を呼び出す

それでは、関数を呼び出してみましょう。
ただ表示するだけではつまらないので、描画もしてみました。

LRESULT CALLBACK WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    static BITMAPINFO bmpInfo;
    static LPDWORD lpPixel;
    static int width,height;
    int x,y;

    switch(uMsg) {
        case WM_CREATE:
            CreateDIB32fromFile248("Mandrill.bmp",&lpPixel,&bmpInfo);
            width=bmpInfo.bmiHeader.biWidth;
            height=bmpInfo.bmiHeader.biHeight;
            //描画
            for(y=height/5;y<height/2;y++){
                for(x=width/5;x<width/2;x++){
                    lpPixel[x+y*width]=0x0000ff00;
                }
            }
            return 0;
        case WM_DESTROY:
            DeleteDIB32(&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ビットBMPを読み込んでいます。
8ビットBMPは自分で試してみて下さい。
特にカラーテーブルの個数が標準の256個より少ないビットマップが要注意です。
今回はあまり意識する必要ないかもしれませんが。

次節ではもっとすっごい関数を作ります。
ちょっと難しいので次節を読む前に今回の内容をしっかりと理解しておいて下さい。

★☆ ダウンロード ☆★


戻る / ホーム