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