DIB ピクセルビット


カラーテーブル

さて、これまでの内容で OS/2 仕様の DIB ならば
プラットフォームに頼らずとも、情報を取得できるようになりました

例えば、OS 非依存環境である Java などはビットマップをサポートする API はありません
Java2 のピュアな API だけで記述するには、ビットマップをバイナリ操作する必要があります
そのような場合に備えても、ビットマップのバイナリ知識は非常に重要です

ビットマップは画像情報であり、画像を表示してナンボです
今回は、表示に関連するカラーテーブルとピクセルビットについて説明しましょう
これが理解できれば、Win32 API のビットマップ専用関数を使わなくても
SetPixel() 関数などでビットマップを表示することも可能になるほど強力な知識基盤になります
それどころか、バイナリエディタで絵を描くことすら不可能ではありません

DIB では、カラービットが 1、4、8 の場合、情報ヘッダの後にカラーテーブルが配置されます
カラーテーブルは RGBTRIPLE 構造体で表現されています
typedef struct tagRGBTRIPLE { // rgbt 
    BYTE rgbtBlue; 
    BYTE rgbtGreen; 
    BYTE rgbtRed; 
} RGBTRIPLE;
rgbtBlue は青、rgbtGreen は緑、rgbtRed は赤を表すメンバで
それぞれ 0〜0xFF までの値で色の強さを決定します
例えば 256 色の DIB ならば、カラーテーブルの配列が 256 要素必要になります

アドレス0123456789ABCDEF
00000000424D4800000000000000200000000C00
0000001000000A000A0001000100FFFFFF000000
0000002000000000000000000000000000000000
0000003000000000000000000000000000000000
000000400000000000000000

このビットマップはオフセット 0x19 までがファイルヘッダと情報ヘッダでした
そして、ピクセルビット数が 1、すなわち 2 色ゆえ、カラーテーブルは2つ必要であり
カラーテーブルのサイズは 3 バイト、2つ合わせて 1A 〜 1F 番地までとなります
そして、オフセットアドレス 20 以降は全てピクセルビットです

上の2色ビットマップは、0xFFFFFF と 0x000000 のカラーテーブルを持ち
これらの値は、色で表現すると「白と黒」です
後者の値を 0x0000FF と変更すれば、イメージの黒い部分が赤に変化するでしょう

このカラーテーブルの考えは 16色、256色 になっても同じです
それぞれ、RGBTRIPLE 構造体の配列としてカラーテーブルが表現されます

アドレス +0+1+2+3+4+5+6+7+8+9+A+B+C+D+E+F
00000000 424D9A000000000000004A0000000C00
00000010 00000A000A0001000400FFFFFF0000FF
00000020 00FF00FF0000FFFF0000FFFFFF00FFAA
00000030 AAAA00AA00000000FFEEEEEEEEFFEEFF
00000040 EEAAAAFF5050504070FF000000000000
00000050 00000000000000000000000000000000
00000060 00000000000000000000000000000000
00000070 00000000000000000000000000000000
00000080 00000000000000000000000000000000
00000090 00000000000000000000

この OS/2 仕様のDIB は 16 色のデータを持っています
オフセットアドレス 1A 〜49 まで 16 個の RGBTRIPLE 配列で表現できます
#include <windows.h>

BITMAPFILEHEADER bmpFileHeader;
BITMAPCOREHEADER bmpCoreHeader;
RGBTRIPLE *prtColor;

LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;
	PAINTSTRUCT ps;
	int iCount = 0 , iXPos = 10 , iYPos = 10;

	switch (msg) {
	case WM_DESTROY:
		free(prtColor);
		PostQuitMessage(0);
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hWnd , &ps);
		for ( ; iCount < (1 << bmpCoreHeader.bcBitCount) ; iCount++) {
			SelectObject(hdc , CreateSolidBrush(
				RGB(	(prtColor + iCount)->rgbtRed ,
					(prtColor + iCount)->rgbtGreen ,
					(prtColor + iCount)->rgbtBlue
				))
			);
			Rectangle(hdc , iXPos , iYPos , iXPos + 90 , iYPos + 20);
			iXPos += 100;
			if (iXPos > 500) {
				iYPos += 25;
				iXPos = 10;
			}
			DeleteObject(SelectObject(hdc , GetStockObject(WHITE_BRUSH)));
		}
		EndPaint(hWnd , &ps);
		return 0;
	}
	return DefWindowProc(hWnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	HWND hWnd;
	MSG msg;
	WNDCLASS winc;
	HANDLE hFile;
	DWORD dwBytes;
	int iColorTableLength;

	hFile = CreateFile(lpCmdLine , GENERIC_READ , 0 , NULL ,
		OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , NULL);
	if (hFile == INVALID_HANDLE_VALUE) return 1;

	ReadFile(hFile , &bmpFileHeader , sizeof (BITMAPFILEHEADER) , &dwBytes , NULL);
	if (bmpFileHeader.bfType != 0x4D42) {
		MessageBox(NULL , TEXT("This is not a bitmap file") , NULL , MB_OK);
		return 1;
	}

	ReadFile(hFile , &bmpCoreHeader , sizeof (BITMAPCOREHEADER) , &dwBytes , NULL);
	iColorTableLength = sizeof (RGBTRIPLE) * (1 << bmpCoreHeader.bcBitCount);
	prtColor = (RGBTRIPLE *) malloc (iColorTableLength);
	ReadFile(hFile , prtColor , iColorTableLength , &dwBytes , NULL);
	CloseHandle(hFile);

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc		= WndProc;
	winc.cbClsExtra		= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return 1;

	hWnd = CreateWindow(
			TEXT("KITTY") , TEXT("Kitty on your lap") ,
			WS_OVERLAPPEDWINDOW | WS_VISIBLE ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			NULL , NULL , hInstance , NULL
	);

	if (hWnd == NULL) return 1;

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}


これは、OS/2 仕様のビットマップをコマンドライン引数で指定すると
そのビットマップのカラーテーブルを色で列挙します
エラー処理をしていないので、24 色ビットマップを指定しないでください

カラーテーブルの数を取得するには、2 の カラービット乗 です
上の C 言語では 1 << bmpCoreHeader.bcBitCount と表現しています

ちなみに、BITMAPCOREINFO 構造体というものもあります
これは、情報ヘッダとカラーテーブルの配列を一つにまとまたものです
typedef struct _BITMAPCOREINFO {    // bmci 
    BITMAPCOREHEADER  bmciHeader; 
    RGBTRIPLE         bmciColors[1]; 
} BITMAPCOREINFO;
RGBTRIPLE 構造体の配列が静的に宣言されていますが
この構造体は malloc() 関数などでメモリに作成したヒープとして使うようにできています
(sizeof (BITMAPCOREINFO) + (1 << bmch.bcBitCount) * sizeof (RGBTRIPLE)) とすることで
適切なサイズの RGBTRIPLE 型メンバを割り当て、ポインタから参照できます


ピクセルビット

さて、ファイルヘッダ、情報ヘッダ、カラーテーブルの後に続くのが
ビットマップ情報の大部分を占めるピクセルビットである
ピクセルビットは、まさにピクセルの色情報を表すイメージの源泉です

カラーテーブルが存在する場合、このような情報をインデックスカラーとも呼び
インデックスカラーは色を表すのではなくカラーテーブルの要素を示します
つまり、ピクセルビットはRGBTRIPLE 配列の添え字そのものです

24 色以上のフルカラーの場合は、インデックスカラーとは異なり
ピクセルビットは直接そのピクセルの色情報を表します

2 色のカラーテーブルのインデックスは、常に 0 か 1 しかありません
2 パターンしかないのだから、当然ですね
つまり、2 色のピクセルビットは 1 ビットが 1 ピクセルを表現しています
同様に 16 色の場合、配列の要素数も 16 となるので 16 通りの表現ができるビット数が必要です
よって、16 色であれば 4 ビットが 1 ピクセルを表現することになります
256 色であれば、256 通りの表現が必要なので 8 ビットで 1 ピクセルを表現します

カラービットが 24 のフルカラーであれば 24 ビットを用いて 1 ピクセルを表現します
先頭の 8 ビットが青、次の 8 ビットが緑、そして赤という順で色の強さを表します

また、イメージの1行に使用されるバイト数は必ず 4 の倍数です
例えば、2 色ビットマップで幅 16 ピクセルであれば 1 行に使用するバイト数は 2 バイトです
しかし、行のバイト数は 4 の倍数でなければならないので、残り 2 バイトは 0 で埋められます
#include <windows.h>

BITMAPFILEHEADER bmpFileHeader;
BITMAPCOREHEADER bmpCoreHeader;
char * chPixelBit;

LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;
	PAINTSTRUCT ps;
	int iCount = 0 , iXPos , iYPos;
	const int WIDTH_BYTE = 
		4 * ((bmpCoreHeader.bcWidth * bmpCoreHeader.bcBitCount + 31)/ 32);

	switch (msg) {
	case WM_DESTROY:
		free(chPixelBit);
		PostQuitMessage(0);
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hWnd , &ps);
		for (iYPos = 0 ; iYPos < bmpCoreHeader.bcHeight ; iYPos++) {
			for (iXPos = 0 ; ; iXPos++) {
				if (iXPos > (bmpCoreHeader.bcWidth - 1)) {
					iCount = (iYPos + 1) * WIDTH_BYTE;
					break;
				}
				SetPixel(hdc , iXPos , iYPos , RGB(
					chPixelBit[iCount + 2] ,
					chPixelBit[iCount + 1] ,
					chPixelBit[iCount]
				));
				iCount += 3;
			}
		}
		EndPaint(hWnd , &ps);
		return 0;
	}
	return DefWindowProc(hWnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	HWND hWnd;
	MSG msg;
	WNDCLASS winc;
	HANDLE hFile;
	DWORD dwBytes;
	int iColorTableLength;

	hFile = CreateFile(lpCmdLine , GENERIC_READ , 0 , NULL ,
		OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , NULL);
	if (hFile == INVALID_HANDLE_VALUE) return 1;

	ReadFile(hFile , &bmpFileHeader , sizeof (BITMAPFILEHEADER) , &dwBytes , NULL);
	if (bmpFileHeader.bfType != 0x4D42) {
		MessageBox(NULL , TEXT("This is not a bitmap file") , NULL , MB_OK);
		return 1;
	}

	ReadFile(hFile , &bmpCoreHeader , sizeof (BITMAPCOREHEADER) , &dwBytes , NULL);
	chPixelBit = (char *) malloc (bmpFileHeader.bfSize - bmpFileHeader.bfOffBits);
	ReadFile(hFile , chPixelBit ,
		bmpFileHeader.bfSize - bmpFileHeader.bfOffBits , &dwBytes , NULL);
	CloseHandle(hFile);

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc		= WndProc;
	winc.cbClsExtra		= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return 1;

	hWnd = CreateWindow(
			TEXT("KITTY") , lpCmdLine ,
			WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME  | WS_VISIBLE  ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			bmpCoreHeader.bcWidth ,
			bmpCoreHeader.bcHeight + GetSystemMetrics(SM_CYCAPTION) ,
			NULL , NULL , hInstance , NULL
	);

	if (hWnd == NULL) return 1;

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}
プログラムは非常に長く、これまでよりも複雑に見えますが
WinMain() 関数で ReadFile() を用いてグローバル変数に必要なデータを読み込ませ
WndProc() 関数の WM_PAINT メッセージの処理でそれを処理しているだけです
そして、ピクセルビットを解析して SetPixel() 関数で 1 ピクセルずつ丁寧に表示します

コマンドライン引数からビットマップファイルを指定してください
ただし、プログラムを簡単に表現するために、このサンプルはカラーテーブルをサポートしません
必ず、指定するビットマップは OS/2 仕様のフルカラービットマップを使ってください

ピクセルビットを解析する時の注意は、1行のバイト数が必ず 4 の倍数であり
余ったビットは 0 で埋められていることを理解しなければいけません
単純にピクセルビットを SetPixel() に変換しても、正しく表示されないでしょう
(偶然的にも、横幅が 4 の倍数のビットマップであれば別ですが)

また、ピクセルビットを管理するために行のバイト数を知る必要もあります
1行のバイト数を取得するには、次の数式を利用しています

4 * ((bmpCoreHeader.bcWidth * bmpCoreHeader.bcBitCount + 31)/ 32);

直接ピクセルビットを扱う場合、行のバイト数はとても重要です
このプログラムを OS/2 仕様のフルカラービットマップを指定して実行すると、次のようになります



ん?どういうわけか上下逆さまになっていますね
そう、実はバイナリレベルのビットマップは逆さまに記録されているのです

ピクセルビットの先頭ビットは、イメージの左下の1ピクセルとなります
これは、他の画像ファイル仕様と異なる DIB の特徴です
通常、ウィンドウ環境の私たちは下方向に向かって座標が進むと考えます
下から上へ座標が進むという考え方は、現代の GUI プログラムでは直感的ではありません

しかし、当時の OS/2 PM は数学的なグラフィックアプローチが中心に考えられ
システムの座標系も含めて左下が原点となっていたのです
OS/2 で生まれたDIB は、このような理由からボトムアップ形式を採用しているのです

先ほどのプログラムを、上から下へではなく
下から上へと描画するように改良すれば、正常に描画できるようになるでしょう
#include <windows.h>

BITMAPFILEHEADER bmpFileHeader;
BITMAPCOREHEADER bmpCoreHeader;
char * chPixelBit;

LRESULT CALLBACK WndProc(HWND hWnd , UINT msg , WPARAM wp , LPARAM lp) {
	HDC hdc;
	PAINTSTRUCT ps;
	int iCount = 0 , iXPos , iYPos , iLine = 1;
	const int WIDTH_BYTE = 
		4 * ((bmpCoreHeader.bcWidth * bmpCoreHeader.bcBitCount + 31)/ 32);

	switch (msg) {
	case WM_DESTROY:
		free(chPixelBit);
		PostQuitMessage(0);
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hWnd , &ps);
		for (iYPos = bmpCoreHeader.bcHeight - 1 ; iYPos >= 0  ; iYPos--) {
			for (iXPos = 0 ; ; iXPos++) {
				if (iXPos > (bmpCoreHeader.bcWidth - 1)) {
					iCount = iLine * WIDTH_BYTE;
					iLine++;
					break;
				}
				SetPixel(hdc , iXPos , iYPos , RGB(
					chPixelBit[iCount + 2] ,
					chPixelBit[iCount + 1] ,
					chPixelBit[iCount]
				));
				iCount += 3;
			}
		}
		EndPaint(hWnd , &ps);
		return 0;
	}
	return DefWindowProc(hWnd , msg , wp , lp);
}

int WINAPI WinMain(HINSTANCE hInstance , HINSTANCE hPrevInstance ,
			PSTR lpCmdLine , int nCmdShow) {
	HWND hWnd;
	MSG msg;
	WNDCLASS winc;
	HANDLE hFile;
	DWORD dwBytes;
	int iColorTableLength;

	hFile = CreateFile(lpCmdLine , GENERIC_READ , 0 , NULL ,
		OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , NULL);
	if (hFile == INVALID_HANDLE_VALUE) return 1;

	ReadFile(hFile , &bmpFileHeader , sizeof (BITMAPFILEHEADER) , &dwBytes , NULL);
	if (bmpFileHeader.bfType != 0x4D42) {
		MessageBox(NULL , TEXT("This is not a bitmap file") , NULL , MB_OK);
		return 1;
	}

	ReadFile(hFile , &bmpCoreHeader , sizeof (BITMAPCOREHEADER) , &dwBytes , NULL);
	chPixelBit = (char *) malloc (bmpFileHeader.bfSize - bmpFileHeader.bfOffBits);
	ReadFile(hFile , chPixelBit , 
		bmpFileHeader.bfSize - bmpFileHeader.bfOffBits , &dwBytes , NULL);
	CloseHandle(hFile);

	winc.style 		= CS_HREDRAW | CS_VREDRAW;
	winc.lpfnWndProc		= WndProc;
	winc.cbClsExtra		= winc.cbWndExtra	= 0;
	winc.hInstance		= hInstance;
	winc.hIcon		= LoadIcon(NULL , IDI_APPLICATION);
	winc.hCursor		= LoadCursor(NULL , IDC_ARROW);
	winc.hbrBackground	= (HBRUSH)GetStockObject(WHITE_BRUSH);
	winc.lpszMenuName	= NULL;
	winc.lpszClassName	= TEXT("KITTY");

	if (!RegisterClass(&winc)) return 1;

	hWnd = CreateWindow(
			TEXT("KITTY") , lpCmdLine ,
			WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME  | WS_VISIBLE  ,
			CW_USEDEFAULT , CW_USEDEFAULT ,
			bmpCoreHeader.bcWidth ,
			bmpCoreHeader.bcHeight + GetSystemMetrics(SM_CYCAPTION) ,
			NULL , NULL , hInstance , NULL
	);

	if (hWnd == NULL) return 1;

	while (GetMessage(&msg , NULL , 0 , 0 )) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return msg.wParam;
}
これで、OS/2 仕様の DIB を API に頼らずとも SetPixel() 関数さえ使えれば描画できます
1 ピクセルずつ描画を繰り返すため、処理速度は極めて遅いのですが
この技術を覚えれば、他のプラットフォームでも DIB を扱うことができるでしょう

ところで、Windows で DIB を扱う全てのプログラマはこのようなプログラムを作っているのでしょうか?
答えは、もちろん NO であり、ピクセルビットを扱う関数が用意されている
しかし、この章で得られる技術概念はグラフィカルプログラミングで非常に重要であり
今後の章を読み進めるために必要な知識でもあります



前のページへ戻る次のページへ