BMPファイルをDIBとして読み込む

概要:24/8ビットBMPファイルから32/24/8ビットDIBを作成する

前節で作成した関数は24/8ビットBMPファイルから32ビットDIBを作成するものでしたが、
今回はこれをかなり拡張して、24ビットBMPファイルなら32/24ビットから、
8ビットBMPファイルなら32/24/8ビットから、作成するDIBのビット数を選択できるようにします。

今回の解説を読む前に、前節の内容をきちんと理解しておいて下さい。

■関数の設計

関数名 → CreateDIBfromFile248
引数 → ファイル名 , ピクセル列のポインタのポインタ , BITMAPINFO構造体のポインタ ,
       作成するDIBのビット数を指定するCDF列挙定数
戻り値 → 一行分のバイト数(成功) or 負値(失敗)

int CreateDIBfromFile248(char *lpFileName,LPVOID *lppPixel,LPBITMAPINFO *lppBmpInfo,CDF type)
{
  ……
}

ピクセル列のポインタには LPDWORD型 と LPBYTE型 の両方を使えるように LPVOID型 を引数とします。
void 型の変数には、全ての型の変数を代入する事ができます。
その逆、void 型の変数から他の型の変数への代入にはキャストが必要です。

関数が確保するメモリ領域は ピクセル列+BITMAPINFO構造体+カラーテーブル(指示があれば) です。

CDF列挙型は次のように定義しました。

typedef enum tagCDF{DEF=0,CT=8,BY=24,DW=32}CDF;
// DEF : BMPファイルの色数が 24bitsならBY , 8bitsならCT
// CT  : *lppPixel は LPBYTE型 である(8bitsDIB作成 / カラーテーブルの作成&コピー) ,
//       BMPファイルの色数が(8bitsではなく)24bitsならBY ,
// BY  : *lppPixel は LPBYTE型 である(24bitsDIB作成)
// DW  : *lppPixel は LPDWORD型 である(32bitsDIB作成)

この列挙定数で *lppPixel の型が何であるか、そして何ビットのDIBを作成して欲しいのかを指示します。

DEF ならBMPファイルそのまま、CT なら8ビット、BY なら24ビット、DW なら32ビット と覚えて下さい。
当然ですが、8ビットDIBを作成する場合はカラーテーブルの作成とコピーも行われます。

*lppPixel の型が指定したものと違ってもエラーではありませんが、その後の使用に十分な注意が必要です。
どうなるかは、プログラムを見てから考える事にしましょう。

ところで、列挙型を使った理由は「意図した値以外を渡されたらコンパイル段階でエラーにする」ためです。
よく #define で定義した定数を引数にとる関数がありますが、
その引数には定数以外にも、その型の値であればなんでも渡す事ができます。
しかし、列挙型の定数の代わりに他の型の値を渡す事はできません。
暗黙の型変換に失敗して、型変換できないという旨のエラーが発生します。
もちろん明示的にキャストすればエラーは発生しませんが、それは意図した操作であるに違いありません。
仮にそれが間違った値であったとしても、そこまで面倒見る事はできませんね。

■BMPファイルの情報を取得するまで

前節と同じ所までのプログラムを以下に示します。

HANDLE fh;
DWORD dwFileSize,dwReadSize;
LPBYTE lpbuf;

LPBITMAPFILEHEADER lpbmpfh;
LPBITMAPINFO lpbmpInfo;

LPBYTE lpbmpPixel;
int bitCount,iWidth,iHeight;

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);

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;
}

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

■ユーザーの指示をチェック

CDF列挙型の引数からユーザーがどのようなビット数のDIBを作成して欲しいのかがわかります。
しかし、CT を指定しているのにBMPファイルが8ビットじゃなければ BY であると解釈し直す必要がありますし、
DEF を指定していれば、BMPファイルのビット数を調べてそれに対応する列挙定数に置き換えなければなりません。

CT は8ビットDIB作成、BY は24ビットDIB作成を意味しています。

if(type==CT && bitCount!=8) type=BY;
else if(type==DEF){
    if(bitCount==24) type=BY;
    else if(bitCount==8) type=CT;
}

■一行分のバイト数

BMPファイルの一行分のバイト数の計算は前節でもやりましたが、
今回は作成するDIBの一行分のバイト数も別途計算しなくてはなりません。
前節のDIBは32ビットに限定されていましたが、今回は32ビットの他に24/8ビットの事もあるからです。

一行分のバイト数は同じになるのでは?そう思ったかもしれませんが、
8ビットから24ビットへの変換もある事を忘れないで下さい。
もちろんBMPファイルとDIBのビット数が同じなら、これらの値も同じになりますが。

一行分のバイト数は関数が成功した時の返却値です。

int iLenSrc,iLenDst;

if(iWidth*(bitCount/8)%4) iLenSrc=iWidth*(bitCount/8)+(4-iWidth*(bitCount/8)%4);
else iLenSrc=iWidth*(bitCount/8);
if(iWidth*(type/8)%4) iLenDst=iWidth*(type/8)+(4-iWidth*(type/8)%4);
else iLenDst=iWidth*(type/8);

■ピクセル列のメモリ領域確保

作成するDIBのビット数と一行分のバイト数が確定したので、ピクセル列のメモリ領域を確保しましょう。

そしてBITMAPINFO構造体のメモリ領域を確保します。
この時、8ビットDIBを作成する場合はカラーテーブルのメモリ領域も同時に確保します。

カラーテーブルは必ず255個分作成します(BITMAPINFO構造体が別途 1個所有)。
ファイルには256個以下しか記録されていないかもしれませんが、
ユーザーが未使用のカラーテーブルに色を設定するかもしれないので、
とりあえず最大数の255個分を作成します。

*lppPixel=HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,iLenDst*iHeight);
if(type==CT) *lppBmpInfo=(LPBITMAPINFO)HeapAlloc(GetProcessHeap(),
    HEAP_ZERO_MEMORY,sizeof(BITMAPINFO)+255*4);
else *lppBmpInfo=(LPBITMAPINFO)HeapAlloc(GetProcessHeap(),
    HEAP_ZERO_MEMORY,sizeof(BITMAPINFO));

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

24ビットBMPファイルから作成されるDIBは32/24ビットのどちらかです。
両者の違いは4バイト毎に使うか3バイト毎に使うかだけなので、処理を統一できそうです。

lppPixel は LPVOID* 型なので、これを LPBYTE 型にキャストします。
こうすれば、24ビットDIBの場合も、32ビットDIBの場合も、
アドレス計算式において、3を掛けるか、4を掛けるかの違いだけで処理を統一する事ができます。

この時、iLenDst にはそれぞれのビット数に対応した一行分のバイト数が格納されている事に注意して下さい。

int x,y;

for(y=0;y<iHeight;y++)
    for(x=0;x<iWidth;x++)
        CopyMemory(*(LPBYTE*)lppPixel+x*(type/8)+y*iLenDst,
            lpbmpPixel+x*3+y*iLenSrc,3);

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

8ビットBMPにはカラーテーブルが使われているので、
カラーテーブルの先頭を指すポインタの設定と実際に使われている個数の取得が必要です。
実際に使われている個数はBITMAPINFOHEADER構造体の biClrUsed メンバに格納されていますが、
0 の時は最大数(8ビットの場合は256)を意味している事に注意して下さい。

ファイルに記録されているカラーテーブルは実際に使われている個数分だけです。

LPRGBQUAD lpColorTable;
int bitCount;

nColorTable=lpbmpInfo->bmiHeader.biClrUsed;
if(nColorTable==0) nColorTable=256;
lpColorTable=lpbmpInfo->bmiColors;

作成するDIBが32/24ビットの場合は、前節の方法、
そして上記24ビットBMPの方法により以下のように処理を統一できます。

for(y=0;y<iHeight;y++)
    for(x=0;x<iWidth;x++)
        CopyMemory(*(LPBYTE*)lppPixel+x*(type/8)+y*iLenDst,
            lpColorTable+lpbmpPixel[x+y*iLenSrc],3);

作成するDIBが8ビットの場合は、カラーテーブルの番号のコピー、
そしてカラーテーブルそもののコピーを行います。
カラーテーブルは実際に使われている個数だけコピーします。

今回は合計で256個のカラーテーブルを使えるように設計しましたが、
ファイルに保存する時は未使用のカラーテーブルは記録しないようにしましょう。

for(y=0;y<iHeight;y++)
    for(x=0;x<iWidth;x++)
        (*(LPBYTE*)lppPixel)[x+y*iLenDst]=lpbmpPixel[x+y*iLenSrc];
for(x=0;x<nColorTable;x++)
    (*lppBmpInfo)->bmiColors[x]=lpColorTable[x];

これらの処理をまとめると以下のようになります。

switch(bitCount){
    case 24:
        for(y=0;y<iHeight;y++)
            for(x=0;x<iWidth;x++)
                CopyMemory(*(LPBYTE*)lppPixel+x*(type/8)+y*iLenDst,
                    lpbmpPixel+x*3+y*iLenSrc,3);
        break;
    case 8:
        nColorTable=lpbmpInfo->bmiHeader.biClrUsed;
        if(nColorTable==0) nColorTable=256;
        lpColorTable=lpbmpInfo->bmiColors;
        switch(type){
            case DW: case BY:
                for(y=0;y<iHeight;y++)
                    for(x=0;x<iWidth;x++)
                        CopyMemory(*(LPBYTE*)lppPixel+x*(type/8)+y*iLenDst,
                            lpColorTable+lpbmpPixel[x+y*iLenSrc],3);
                break;
            case CT:
                for(y=0;y<iHeight;y++)
                    for(x=0;x<iWidth;x++)
                        (*(LPBYTE*)lppPixel)[x+y*iLenDst]=lpbmpPixel[x+y*iLenSrc];
                for(x=0;x<nColorTable;x++)
                    (*lppBmpInfo)->bmiColors[x]=lpColorTable[x];
                break;
        }
        break;
}

■BITMAPINFO構造体の設定

BMPファイルのBITMAPINFOHEADER構造体をコピーしてから、ビット数だけ再設定します。

CopyMemory(*lppBmpInfo,&lpbmpInfo->bmiHeader,sizeof(BITMAPINFOHEADER));
(*lppBmpInfo)->bmiHeader.biBitCount=type;

■関数の終了処理

BMPファイルの内容全てをコピーしていたメモリ領域を解放して、iLenDst を返却します。

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

■DIBを削除する関数

ピクセル列のメモリ領域解放の他に、
BITMAPINFO構造体(8ビットDIBならカラーテーブルを含む)のメモリ領域解放も行います。

void DeleteDIB(LPVOID *lppPixel,LPBITMAPINFO *lppBmpInfo)
{
    if(*lppPixel!=NULL){
        HeapFree(GetProcessHeap(),0,*lppPixel);
        *lppPixel=NULL;
    }
    if(*lppBmpInfo!=NULL){
        HeapFree(GetProcessHeap(),0,*lppBmpInfo);
        *lppBmpInfo=NULL;
    }
}

■関数を呼び出す

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

LRESULT CALLBACK WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    static LPBITMAPINFO lpBmpInfo;
    static LPBYTE lpPixel;
    static int width,height,length,x,y;

    switch(uMsg) {
        case WM_CREATE:
            length=CreateDIBfromFile248("TOKOPUYO200.bmp",(void**)&lpPixel,&lpBmpInfo,BY);
            width=lpBmpInfo->bmiHeader.biWidth;
            height=lpBmpInfo->bmiHeader.biHeight;
            //描画
            for(y=height/5;y<height/2;y++){
                for(x=width/5;x<width/2;x++){
                    lpPixel[(x*3  )+y*length]=0x80;
                    lpPixel[(x*3+1)+y*length]=0x80;
                    lpPixel[(x*3+2)+y*length]=0x80;
                }
            }
            return 0;
        case WM_DESTROY:
            DeleteDIB((void**)&lpPixel,&lpBmpInfo);
            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);
}

ここでは200個のカラーテーブルが記録されているBMPファイルを24ビットDIBとして読み込ませています。
返却値は一行分のバイト数なので、これを使えばピクセル列のアドレス計算もすぐに出来ます。

一般的にはファイルから読み込んだBMPに対して描画したりしないと思いますので、
GDI関数も使えるDIBSectionより、お手軽なDIBで十分かもしれません。
また、8ビットDIBSectionは作成できませんが、8ビットDIBなら作成できるという利点もあります。

■不正な指示

CDF列挙型の引数に不正な指示を与えたらどうなるでしょうか?
例えば、*lppPixel に LPDWORD 型を与えていながら、これが LPBYTE 型であると伝えた場合、
本来は使われないはずの最上位バイトにまで値がコピーされてしまいます。
もちろんアドレス計算もそれを了承した上で行わなければなりません。
っていうか、プログラムはそれを知らないので、BITMAPINFO構造体を参照する関数は使えなくなるでしょう。
たぶん何の意味もない裏技なので、素直に正しい指示を与えてやって下さい。

★☆ ダウンロード ☆★


戻る / ホーム