EternalWindows
ビットマップ / アンチエイリアシング

ビットイメージにアクセスしてピクセルを変更する方法は、 ある種のGDIからの脱却であるといえます。 RectangleやEllipseのような矩形や楕円の描画は、 もはや数学的アルゴリズムを用いて解決できることですし、 その図形の内部がブラシで塗りつぶされてしまうなどといった 煩わしい問題も気にすることはなくなります。 しかし、テキストの描画に関していえば、DIB主体のプログラムであっても GDIのTextOutやDrawTextは依然として便利な関数です。 これは、個々の文字の形を取得することが困難であること以外に、 関数の動作が比較的高速であるという点が挙げられますが、 これらの関数で描画された文字は輪郭がギザギザになるという欠点があります。 よって、このような問題を解決するのであれば、やはり文字の形を取得し、 しかるべき処理を行うことになります。

GDIの1つであるGetGlyphOutlineという関数は、 指定された文字を囲む最小の長方形をビットマップとして返すことができます。 このビットマップはビットイメージであり、文字の一部でないピクセルには0が、 文字の部分のピクセルには0以外の値が格納されているため、 0以外のピクセルをバックバッファにコピーすることで、 文字を描画することができます。 GetGlyphOutlineが返すビットマップのピクセルが表すのは 階調(アルファ値)であり、色ではありません。 たとえば、65階調のビットマップを取得する場合、 個々のピクセルに格納される値は0から64までの値であり、 0でない限り、そのピクセルは文字の一部を構成しているわけです。 文字のギザギザをなくすというのは、文字の輪郭の部分を背景となる画像と 合成するということですから、輪郭に近いピクセルほどアルファ値は低くなり、 そうでないピクセルは通常の色で描画すべきですから、 アルファ値は不透明の64が格納されているでしょう。 以上の事を踏まえたうえで、GetGlyphOutlineのプロトタイプを見てみます。

DWORD GetGlyphOutline(
  HDC hdc,
  UINT uChar,
  UINT uFormat,
  LPGLYPHMETRICS lpgm,
  DWORD cbBuffer,r
  LPVOID lpvBuffer,
  CONST MAT2 *lpmat2
);

hdcは、デバイスコンテキストのハンドルを指定します。 このハンドルに選択されているフォントは、グリフのサイズに影響します。 uCharは、ビットマップを取得したい文字を指定します。 uFormatは、ビットマップの取得形式を表す定数を指定します。 基本的には、65階調のビットマップを取得すべくGGO_GRAY8_BITMAPを指定しますが、 17階調のGGO_GRAY4_BITMAPも指定することができます。 階調が少ないということは、それだけ文字の輪郭を表せるアルファ値も少ないため、 アンチエイリアシング(ギザギザをなくす)の精度は落ちますが、 関数の動作は65階調と比べ速くなります。 lpgmは、文字セル内におけるグリフの位置を取得するために、 GLYPHMETRICS構造体のアドレスを指定します。 cbBufferは、関数から得られるビットマップのサイズを指定します。 1回目のGetGlyphOutlineではこの引数に0を指定し、 関数の戻り値を通じてサイズを取得することになります。 lpvBufferは、ビットマップを受けるバッファのアドレスを指定します。 先に述べたように、ここでいうビットマップとはビットイメージのことで、 トップダウン形式の1ピクセル256バイトになります。 lpmat2は、文字を拡大したり回転したりするための行列を記述する MAT2構造体のアドレスを指定します。

グリフというのは、ビットマップに書かれた文字のことを意味しています。 ビットマップの大きさはこのグリフを囲えるだけの最小のサイズであり、 それはGetGlyphOutlineが初期化するGLYPHMETRICS構造体から得ることができます。

typedef struct _GLYPHMETRICS { 
  UINT  gmBlackBoxX; 
  UINT  gmBlackBoxY; 
  POINT gmptGlyphOrigin; 
  short gmCellIncX; 
  short gmCellIncY; 
} GLYPHMETRICS, *LPGLYPHMETRICS; 

gmBlackBoxXは、グリフを囲う最小の長方形の幅が格納されます。 gmBlackBoxYは、グリフを囲う最小の長方形の高が格納されます。 gmptGlyphOriginは、グリフを囲う最小の長方形の左上隅座標が格納されます。 これは一見すると、(0, 0)の値になるように思えますが、 どうやらこの座標は文字セル内におけるグリフの位置を表すようです。 文字セルというのは、おそらく一番大きな文字を囲えるだけのサイズを持った 長方形のことだと思われますが、この左上隅座標を基準とした位置が、 グリフの描画の相対位置となるようです。 たとえば、'A'と'.'という2つの文字があった場合、 .はAと比べて下よりに描画しなければなりませんから、 gmptGlyphOrigi.yにはその分の値が格納されることになります。 gmCellIncXは、次の文字セルまでの距離とされていますが正確にはよく分かりません。 この値を現在の文字の描画位置と加算すれば、次の文字の描画位置を導くことができるのですが、 それならば、ビットマップの幅であるgmBlackBoxXでもよいように思えます。

今回のプログラムは、GetGlyphOutlineを呼び出してテキストを描画します。 関数から得られるビットマップのアルファ値を適切に走査することで、 アンチエイリアシングを実現しています。

#include <windows.h>

void Antialiasing(HDC hdc, HBITMAP hbmp, int xStart, int yStart, LPTSTR lpszText, COLORREF cr);
LPBYTE GetBits(HBITMAP hbmp, int x, int y);
HBITMAP CreateBackbuffer(int nWidth, int nHeight);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;
	RECT       rc;
	DWORD      dwStyle;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;
	
	dwStyle = WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX;
	SetRect(&rc, 0, 0, 640, 480);
	AdjustWindowRect(&rc, dwStyle, FALSE);

	hwnd = CreateWindowEx(0, szAppName, szAppName, dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HFONT   hfont = NULL;
	static HDC     hdcBackbuffer = NULL;
	static HBITMAP hbmpBackbuffer = NULL;
	static HBITMAP hbmpBackbufferPrev = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		HDC hdc;

		hdc = GetDC(hwnd);

		hfont = CreateFont(32, 0, 0, 0, 0, 0, 0, 0, SHIFTJIS_CHARSET, 0, 0, DEFAULT_QUALITY, 0, TEXT("MS ゴシック"));
		if (hfont == NULL) {
			ReleaseDC(hwnd, hdc);
			return -1;
		}

		hdcBackbuffer = CreateCompatibleDC(hdc);
		hbmpBackbuffer = CreateBackbuffer(640, 480);
		if (hbmpBackbuffer == NULL) {
			ReleaseDC(hwnd, hdc);
			return -1;
		}

		hbmpBackbufferPrev = (HBITMAP)SelectObject(hdcBackbuffer, hbmpBackbuffer);

		ReleaseDC(hwnd, hdc);
		
		return 0;
	}

	case WM_PAINT: {
		HDC         hdc;
		HFONT       hfontPrev;
		PAINTSTRUCT ps;

		hdc = BeginPaint(hwnd, &ps);

		hfontPrev = (HFONT)SelectObject(hdcBackbuffer, hfont);
		Antialiasing(hdcBackbuffer, hbmpBackbuffer, 70, 70, TEXT("EternalWindows"), RGB(255, 255, 255));
		SelectObject(hdcBackbuffer, hfontPrev);
		
		BitBlt(hdc, 0, 0, 640, 480, hdcBackbuffer, 0, 0, SRCCOPY);

		EndPaint(hwnd, &ps);

		return 0;
	}

	case WM_DESTROY:
		if (hdcBackbuffer != NULL) {
			if (hbmpBackbuffer != NULL) {
				SelectObject(hdcBackbuffer, hbmpBackbufferPrev);
				DeleteObject(hbmpBackbuffer);
			}
			DeleteDC(hdcBackbuffer);
		}

		DeleteObject(hfont);
		
		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

void Antialiasing(HDC hdc, HBITMAP hbmp, int xStart, int yStart, LPTSTR lpszText, COLORREF cr)
{
	int          i, j;
	int          x, y;
	int          nWidth, nHeight;
	int          nLine;
	BYTE         alpha;
	DWORD        dwBufferSize;
	LPBYTE       lp, lpBuffer;
	GLYPHMETRICS gm;
	MAT2 mat2 = {
		{0, 1}, {0, 0}, {0, 0}, {0, 1}
	};

	while (*lpszText) {
		dwBufferSize = GetGlyphOutline(hdc, *lpszText, GGO_GRAY8_BITMAP, &gm, 0, NULL, &mat2);
		lpBuffer = (LPBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBufferSize);
		GetGlyphOutline(hdc, *lpszText, GGO_GRAY8_BITMAP, &gm, dwBufferSize, lpBuffer, &mat2);

		nLine   = ((gm.gmBlackBoxX + 3) / 4) * 4;
		nWidth  = gm.gmBlackBoxX;
		nHeight = gm.gmBlackBoxY;

		x = xStart + gm.gmptGlyphOrigin.x;
		y = yStart - gm.gmptGlyphOrigin.y;
		
		for (i = 0; i < nHeight; i++) {
			for (j = 0; j < nWidth; j++) {
				alpha = *(lpBuffer + (i * nLine) + j);
				if (alpha) {
					lp = GetBits(hbmp, x + j, y + i);
					lp[0] = (GetBValue(cr) * alpha / 64) + (lp[0] * (64 - alpha) / 64);
					lp[1] = (GetGValue(cr) * alpha / 64) + (lp[1] * (64 - alpha) / 64);
					lp[2] = (GetRValue(cr) * alpha / 64) + (lp[2] * (64 - alpha) / 64);
				}
			}
		}
		xStart += gm.gmCellIncX;
		lpszText++;

		HeapFree(GetProcessHeap(), 0, lpBuffer);
	}
}

LPBYTE GetBits(HBITMAP hbmp, int x, int y)
{
	BITMAP bm;
	LPBYTE lp;
	
	GetObject(hbmp, sizeof(BITMAP), &bm);

	lp = (LPBYTE)bm.bmBits;
	lp += (bm.bmHeight - y - 1) * ((3 * bm.bmWidth + 3) / 4) * 4;
	lp += 3 * x;

	return lp;
}

HBITMAP CreateBackbuffer(int nWidth, int nHeight)
{
	LPVOID           lp;
	BITMAPINFO       bmi;
	BITMAPINFOHEADER bmiHeader;

	ZeroMemory(&bmiHeader, sizeof(BITMAPINFOHEADER));
	bmiHeader.biSize      = sizeof(BITMAPINFOHEADER);
	bmiHeader.biWidth     = nWidth;
	bmiHeader.biHeight    = nHeight;
	bmiHeader.biPlanes    = 1;
	bmiHeader.biBitCount  = 24;

	bmi.bmiHeader = bmiHeader;

	return CreateDIBSection(NULL, (LPBITMAPINFO)&bmi, DIB_RGB_COLORS, &lp, NULL, 0);
}

WM_CREATEで作成されたフォントは、WM_PAINTでAntialiasingという関数を 呼び出す前に選択されることになっています。 Antialiasingの第1引数はテキストを描画するデバイスコンテキストのハンドル、 第2引数はバックバッファのビットイメージを参照するためのビットマップのハンドル、 第3引数と第4引数はテキストの描画位置を表すXY座標、 第5引数は描画する文字列となり、第6引数が文字列の色となります。 次に、関数の主要部分を順に見ていきます。

dwBufferSize = GetGlyphOutline(hdc, *lpszText, GGO_GRAY8_BITMAP, &gm, 0, NULL, &mat2);
lpBuffer = (LPBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBufferSize);
GetGlyphOutline(hdc, *lpszText, GGO_GRAY8_BITMAP, &gm, dwBufferSize, lpBuffer, &mat2);

1回目のGetGlyphOutline時では、ビットマップを受けるバッファを確保していないので、 まずは第5引数と第6引数を0とNULLにして、戻り値を通じてバッファのサイズを取得します。 第7引数のMAT2構造体は文字の拡大や変換を示す行列を表しますが、 これは事前に単位行列という変化をしない値で初期化しているので、 通常の文字の形を囲うビットマップが得られることになります。

nLine   = ((gm.gmBlackBoxX + 3) / 4) * 4;
nWidth  = gm.gmBlackBoxX;
nHeight = gm.gmBlackBoxY;

GetGlyphOutlineで得られたビットマップを走査するための変数を初期化する部分です。 ビットマップは通常のDIBのように一行が4の倍数になる決まりがあるため、 各行の先頭を導くために必要なバイト数を求めることになります。 ビットマップの幅と高さは、それぞれgm.gmBlackBoxXとgm.gmBlackBoxYから得られます。

x = xStart + gm.gmptGlyphOrigin.x;
y = yStart - gm.gmptGlyphOrigin.y;

バックバッファに文字を描画し始める位置を求める部分です。 xStartとyStartの値は、文字セル内におけるグリフの位置を考慮していないため、 これを考慮した値をxとyに代入し、実際の描画に利用します。 グリフのx座標であるgm.gmptGlyphOrigin.xは主として0のようですが、 y座標に関しては、比較的小さい文字の場合は値を持つことになります。 値を持つということは、基点(yStart)より下に描画する必要がありますから、 その分だけyStartに加算するべきといえます。 しかし、不思議なことにこのy座標は負の値で表現されているため、 計算式も減算の形をとることになります。

for (i = 0; i < nHeight; i++) {
	for (j = 0; j < nWidth; j++) {
		alpha = *(lpBuffer + (i * nLine) + j);
		if (alpha) {
			lp = GetBits(hbmp, x + j, y + i);
			lp[0] = (GetBValue(cr) * alpha / 64) + (lp[0] * (64 - alpha) / 64);
			lp[1] = (GetGValue(cr) * alpha / 64) + (lp[1] * (64 - alpha) / 64);
			lp[2] = (GetRValue(cr) * alpha / 64) + (lp[2] * (64 - alpha) / 64);
		}
	}
}

ビットマップを走査し、アルファ値を基にバックバッファへコピーする部分です。 lpBufferは常にビットマップの先頭を指しており、 (i * nLine)を加算することでアクセスする行を、 jを加算することでi行目のj列目のピクセルの値を得ることができます。 この値が0であるということはアルファ値が無い、即ち透明ということですから、 描画処理を行ったとしても、背景のピクセルを代入し直すだけで意味がありません。 マクロを利用してcrから抜き出したRGB成分は、アルファ値が64(不透明)のときには、 そのまま代入すべきですから、全ての計算は最高値である64を基準に行うことになります。 勿論、この64という値はGetGlyphOutlineの第3引数にGGO_GRAY8_BITMAPを指定したからであり、 GGO_GRAY4_BITMAPを指定した場合は、17階調(0から16)ということで、 64を16に置き換えればよいことになります。

xStart += gm.gmCellIncX;
lpszText++;

次の文字を描画するための更新を行う部分です。 gm.gmCellIncXは次の文字セルまでの距離が格納されているため、 これを加算すれば次の文字を描画する基点が得られることになります。 lpszTextは*lpszTextがNULLになるまで、即ち文字の終端に着くまで加算することになりますが、 プログラムがUNICODEとしてコンパイルされていない場合は、 この部分はもう少し複雑なことになります。 平仮名や漢字は2バイトで表現されることになっているため、 lpszText++が1バイト単位のアクセスとなるマルチバイトプログラムは 場合によってはもう一度lpszText++を実行し、2回に渡って取得した値を 2バイトとして一つの変数にまとめる処理が必要になってきます。

HeapFree(GetProcessHeap(), 0, lpBuffer);

GetGlyphOutlineで得られたビットマップを開放する部分です。 これは一重に、ビットマップが文字を囲えるだけの最小サイズということで、 文字毎にサイズが変化するためです。 GetGlyphOutlineを呼び出すプログラムでは、 関数の速度とメモリの確保及び開放が非常に大きなコストとなるため、 可能な限りその呼び出しは削減するのが無難だといえます。 候補としては、文字列が描画される範囲をサイズとした セカンダリバックバッファなどを作成してそこに文字を描画し、 以後はそのメモリをバックバッファにコピーする方法が考えられます。


戻る