文字描画関連その2

フォントと文字描画の扱いで問題になるのはエンコードレンダリング処理である。Ruby1.8用の場合はエンコードは決め打ち、1.9ならRubyの機能で変換すればいい感じにはなる。残る問題はレンダリングだ。
WindowsAPIとかDirectXのD3DXFontとかを使うと文字を指定して描画してもらえるが、これは昔からあるものだからかなんとなく綺麗じゃない。綺麗に描画しようと思ったら自前でどうにかする必要がある。TrueTypeフォントの場合、見た目の情報はベクタデータで保持されているから、そいつをビットマップ情報に変換するわけだが、その処理は大変難しいのでWindowsAPIであるGetGlyphOutlineを使う。それで受け取ったビットマップ情報をもとにして描画する。遅いけど。
このあたりの処理を高速化もしくは更に高品質に、と思うと、やっぱりベクタデータのビットマップ化を自前で実装することになるのだが、それを実装する労力と効果を考えると微妙に怪しい感じになってくる。高速化するのならフォント情報ごとにビットマップ変換後の情報をキャッシュするようにしたほうがいい。高品質化は難しい。
キャッシュすると簡単に言っても、文字のフォントやサイズ、属性によって結果は変わってくるし、日本語の文字など膨大なわけで、キャッシュ情報をいくつ保存するか、いつまで保存するか、というのは大きな問題になってくる。DXRubyでは文字描画が遅いというのは認識しているがキャッシュ関連はまだ実装していない。とりあえず使い方を考えると1フレームだけキャッシュするような形がいいのかなーとか考えているところ。GetGlyphOutline後のデータをキャッシュするのならFontオブジェクトにRubyのハッシュという形で持たせるのがいいのかなーとか、どこにキャッシュを捨てるロジックを入れればいいのかなーとか、まあ、実装面で悩みは多いのだが。

今回はImage#draw_fontの実装の話。

/*--------------------------------------------------------------------
   イメージにフォント描画
 ---------------------------------------------------------------------*/
static VALUE Image_drawFont( int argc, VALUE *argv, VALUE obj )
{
    struct DXRubyFont *font;
    struct DXRubyImage *image;
    VALUE vx, vy, vcolor, vstr, data;
    int cr=255, cg=255, cb=255, x, y;
    LPDIRECT3DSURFACE9 pD3DSurface;
    HDC hDC;
    RECT rc;
    HRESULT hr;
    D3DLOCKED_RECT srctrect;
    D3DLOCKED_RECT dsttrect;
    int i, j;
    int h;
    RECT srcrect;
    RECT dstrect;
    TEXTMETRIC tm;
    GLYPHMETRICS gm;

    LPWSTR widestr;
    VALUE vwidestr;
    MAT2 mat2 = {{0,1},{0,0},{0,0},{0,1}};

    /* 引数取得 */
    rb_scan_args( argc, argv, "41", &vx, &vy, &vstr, &data, &vcolor);

    Check_Type(vstr, T_STRING);

    /* 引数のフォントオブジェクトから中身を取り出す */
    Data_Get_Struct( obj, struct DXRubyImage, image );
    DXRUBY_CHECK_DISPOSE( image, texture );
    DXRUBY_CHECK_TYPE( Font, data );
    Data_Get_Struct( data, struct DXRubyFont, font );
    DXRUBY_CHECK_DISPOSE( font, pD3DXFont );

    x = NUM2INT( vx );
    y = NUM2INT( vy );
    if( vcolor != Qnil )
    {
        get_color( vcolor, &cr, &cg, &cb );
    }

    if( x >= image->width || y >= image->height )
    {
        return obj;
    }

    /* 描画文字のUTF16LE化 */
#ifdef HAVE_RB_ENC_STR_NEW
    if( rb_enc_get_index( vstr ) != 0 )
    {
        vwidestr = rb_funcall( vstr, rb_intern( "encode" ), 1, rb_str_new2( "UTF-16LE" ) );
    }
    else
    {
        char *buf;
        int bufsize;
        bufsize = MultiByteToWideChar(CP_ACP, 0, RSTRING_PTR( vstr ), RSTRING_LEN( vstr ), 0, 0);
        buf = alloca(bufsize * 2);
        MultiByteToWideChar(CP_ACP, 0, RSTRING_PTR( vstr ), RSTRING_LEN( vstr ), (LPWSTR)buf, bufsize);
        vwidestr = rb_str_new( buf, bufsize*2 );
    }
#else
    {
        char *buf;
        int bufsize;
        bufsize = MultiByteToWideChar(CP_ACP, 0, RSTRING_PTR( vstr ), RSTRING_LEN( vstr ), 0, 0);
        buf = alloca(bufsize * 2);
        MultiByteToWideChar(CP_ACP, 0, RSTRING_PTR( vstr ), RSTRING_LEN( vstr ), (LPWSTR)buf, bufsize);
        vwidestr = rb_str_new( buf, bufsize*2 );
    }
#endif
    widestr = alloca( RSTRING_LEN( vwidestr ) + 2 );
    ZeroMemory( widestr, RSTRING_LEN( vwidestr ) + 2 );
    memcpy( widestr, RSTRING_PTR( vwidestr ), RSTRING_LEN( vwidestr ) );

    hDC = GetDC( g_hWnd );
    SelectObject( hDC, font->hFont );
    GetTextMetrics( hDC, &tm );

    dstrect.left = image->x;
    dstrect.top = image->y;
    dstrect.right = image->x + image->width;
    dstrect.bottom = image->y + image->height;

    image->texture->pD3DTexture->lpVtbl->LockRect( image->texture->pD3DTexture, 0, &dsttrect, &dstrect, 0 );

    for( i = 0; i < RSTRING_LEN( vwidestr ) / 2; i++ )
    {
        int bufsize = GetGlyphOutlineW( hDC, *(widestr + i), GGO_GRAY8_BITMAP,
                                        &gm, 0, NULL, &mat2 );

        if( bufsize > 0 )
        {
            unsigned char *buf = alloca( bufsize );
            int v, u;

            GetGlyphOutlineW( hDC, *(widestr + i), GGO_GRAY8_BITMAP, 
                              &gm, bufsize, (LPVOID)buf, &mat2 );

            drawfont_sub( gm.gmBlackBoxX, gm.gmBlackBoxY, x + gm.gmptGlyphOrigin.x, y + tm.tmAscent - gm.gmptGlyphOrigin.y, (gm.gmBlackBoxX + 3) & 0xfffc, buf, &dsttrect, cr, cg, cb, image->width, image->height );
        }
        x += gm.gmCellIncX;
        if( x >= image->width )
        {
            break;
        }
    }

    image->texture->pD3DTexture->lpVtbl->UnlockRect( image->texture->pD3DTexture, 0 );

    ReleaseDC( g_hWnd, hDC );
    return obj;
}

上のほうは省略して、真ん中にあるUTF16LE化という部分。ACP(アスキーコードページ)というのがシステムエンコーディングのことで日本語WindowsではSJISになる。SJISからUTF16へ変換する場合、元のサイズから変換先サイズを算出することができないから、とりあえずAPIでサイズを取得してバッファを確保、変換という手順を取る。
そもそもなんでUTF16に変換しているのかというと、後ろで1文字ずつレンダリングする処理があるためで、SJISだと1文字1byteなのか2byteなのかを判定するのが手間だから、1文字2byte固定で扱えるUTF16に変換して処理しているのだ。WindowsAPIすべてに関してACPで扱う関数とUTF16で扱う関数が用意されているから、変換しておくことでラクができる。
HAVE_RB_ENC_STR_NEWで囲ったところの直後になんか無駄っぽいメモリクリアとコピーがある。Ruby1.9のメソッドでUTF16化すると終端文字が1byteだけついてくるのだが、WindowsAPIでUTF16を渡すと終端文字は2byte必要なので、っていうかUTF16の終端は0が2byte続くもののはずなのでRubyのほうがおかしいんじゃないかと思っているのだが、まあ、そんな理由でこのような処理を行って終端を追加している。
あとはFontオブジェクトから取り出したフォントハンドルを設定したりImageのテクスチャをロックしたりしつつ、GetGlyphOutlineWで文字のビットマップ情報を受け取って自力で描き込むということをしている。
描画処理そのものはdrawfont_sub関数でやっていて、その中身はこんな感じ。

static void drawfont_sub( int blackboxX, int blackboxY, int baseX, int baseY, int pitch,  char *buf, D3DLOCKED_RECT *dsttrect, int cr, int cg, int cb, int width, int height )
{
    int v, u, xx, yy;

    for( v = 0; v < blackboxY; v++ )
    {
        int yy = baseY + v;
        if( yy < 0 )
        {
            continue;
        }
        if( yy >= height )
        {
            break;
        }
        for( u = 0; u < blackboxX; u++ )
        {
            int xx, src;

            xx = baseX + u;
            if( xx < 0 )
            {
                continue;
            }
            if( xx >= width )
            {
                break;
            }

            src = (int)buf[ u + v * pitch ] * 255 / 64;

            if( src == 255 )
            {
                *((LPDWORD)((char*)dsttrect->pBits + xx * 4 + yy * dsttrect->Pitch)) = D3DCOLOR_ARGB(0xff, cr, cg, cb);
            }
            else if( src != 0 )
            {
                struct DXRubyColor d = *((struct DXRubyColor*)((char*)dsttrect->pBits + xx * 4 + yy * dsttrect->Pitch));
                struct DXRubyColor data;
                int temp = (255 - src) * d.alpha + src * 255;

                data.alpha = temp / 255;
                data.red = (src * cr * 255 + (int)d.alpha * d.red * (255 - src)) / temp;
                data.green = (src * cg * 255 + (int)d.alpha * d.green * (255 - src)) / temp;
                data.blue = (src * cb * 255 + (int)d.alpha * d.blue * (255 - src)) / temp;

                *((struct DXRubyColor*)((char*)dsttrect->pBits + xx * 4 + yy * dsttrect->Pitch)) = data;
            }
        }
    }
}

厄介なのはGGO_GRAY8_BITMAPで受け取った65段階グレースケールの文字ビットマップ情報の扱いと、テキストメトリックのTEXTMETRIC構造体、グリフメトリックのGLYPHMETRICS構造体のデータの扱いだ。
これらは地味に独特の構造をしているため、資料とにらめっこしながらコードを書くはめになる。たぶんコードだけ見ても理解し難いんじゃないかと思う。っていうか俺も自分で書いたコードなのによくわからない。フォントまわりの情報はややこしいので覚えるのも難しい。
まあ、グリフ情報を使って自前描画したい人がいた場合に、実際に動作して描画できてるサンプルの一つということで、資料を見ながら読んで頂ければなんとなくわかるんじゃないかしらん。
あと、データを書きこむところの変な計算は半透明を扱う色の合成式だ。グレースケールのビットマップの値をα値として扱っているのでこういう感じの計算になる。