Unicodeの特殊事例に対して対処できたのはサロゲートペアだけで、結合文字や絵文字に対してはGetGlyphOutlineでは扱えずビットマップが取得できない。
この時はDirect2DとGDIのどちらにするか迷ってGDIを選択したけど、絵文字も表示するとなるとDirect2D(DirectWrite)しか選択肢がなかった。
IDWriteTextRenderer
GDIのGetGlyphOutlineに当たる機能はDirect2Dでは何になるかを調べたところ下記になるらしい。
グリフ メトリック -- IDWriteFontFace::GetDesignGlyphMetrics
実際のアウトライン情報 --IDwriteFontFace::GetGlyphRunOutline
グリフ ビットマップ -- IDWriteRenderBitmapRenderTarget::DrawGlyphRun
更に調べていくと、マイクロソフトのサンプルのDWriteHelloWorld/CustomTextReadererにたどり着いた。
CustomTextReadererはIDWriteTextRendererを継承して作られたクラス。
前回、WicBitmapRenderTarget経由でIWICBitmapに書き込むことは出来ていたけど、今回はCustomTextReadererで書き込むことに成功。
今回はビットマップのフォーマットをGUID_WICPixelFormat8bppAlphaにすることにより、GetGlyphOutlineで得られるデータに近いものが得られるようにした。
で、Bitmap自体は取得できたけど、グリフ情報は得られていない。
IDWriteFontFace::GetDesignGlyphMetricsで得られるらしいけど、IDWriteFontFaceを得る方法がかなり難しそうで、ファイルを指定しないといけなかったり、IDWriteFontFamilyやIDWriteFontCollectionからもたどり着くのが難しい。
一体どうすればいいのか悩んでいたけど、CustomTextReadererの中に答えがあった。
CustomTextReaderer::DrawGlyphRun関数の引数内にglyphRunがあり、glyphRun.fontFaceがIDWriteFontFaceだった。
これを利用すれば、GetDesignGlyphMetricsが呼び出せてDWRITE_GLYPH_METRICSが取得できる。
IDWriteFontFace::GetMetrics関数でDWRITE_FONT_METRICSが取得でき、この2つを組み合わせると下記の情報が得られる。
fm:DWRITE_FONT_METRICS
gm:DWRITE_GLYPH_METRICS
gm.advanceWidthが次の文字までの距離で、GLYPHMETRICSで言うところのgmCellIncXに当たる値。
書き始めの基準が薄い赤の枠の左端で、次の文字の書き始めが右端になる。
左端はDraw関数に指定した位置で、右端はadvanceWidthを足した位置になる。
上辺は、Draw関数に指定した位置にfm.ascentを足して、gm.verticalOriginYを引いた位置で、下辺は、Draw関数に指定した位置にfm.ascentとfm.descentを足した位置になる。
実際の文字のデータが含まれる範囲(濃い赤枠)を得るには、薄い枠の範囲にleftSideBearing、rightSideBearing、topSideBearing、bottomSideBearingを引いた値となる。
Aの場合はすべての値がプラスで薄い赤枠の内側になる。
jの場合は、leftSideBearing、topSideBearingはマイナスで薄い青枠の外側になり、描画開始位置が前の文字と被ることになる。
(GLYPHMETRICSのgmptGlyphOrigin.xがマイナスの場合と同じ)
また、これらの値の単位が違うので変換する(fm.designUnitsPerEmで割って、glyphRun->fontEmSizeを掛ける)必要がある。
得られるのは小数点を含むデータでピクセル単位に変換する際、top、leftは切り捨て、bottom、rightは切り上げした。メイリオの通常ではこれで問題ないけど、別フォントでイタリックにすると、縦が1ドット足りない場合はあった。必要に応じてマージンを設定するといいかも。
絵文字対応
通常の文字であれば、1つのグリフ情報だけが得られてそのSideBearingを計算すればよかったけど、例えば「👨👩👧👦」の絵文字の場合得られるグリフ情報が4つ返却され、それぞれの絵文字のSideBearingを見るだけだと計算が狂ってしまう。
glyphRunにはglyphAdvancesとglyphOffsets(※)があり、glyphOffsetsにはさらにadvanceOffsetとascenderOffsetが含まれている。
これらの値は、前述の単位変換は不要。
※glyphOffsetsはnullの場合があり、その時は0扱いとする。
メイリオ24で「👨👩👧👦」のDrawを行うと下記のグリフ情報が得られる。
| X | Y | W | H | A | OX | OY | |
|---|---|---|---|---|---|---|---|
| 父 | 2.0 | 4.6 | 14.4 | 17.0 | 16.4 | 4.9 | 0.0 |
| 母 | 13.1 | 4.7 | 15.2 | 16.9 | 13.6 | 0.0 | 0.0 |
| 娘 | 1.9 | 15.7 | 13.9 | 15.1 | 0.0 | -25.4 | 0.0 |
| 息子 | 13.9 | 16.0 | 13.5 | 14.9 | 0.0 | -13.6 | 0.0 |
X:advanceOffset + leftSideBearing
Y:ascent + topSideBearing - verticalOriginY - ascenderOffset
W:advanceWidth - leftSideBearing - rightSideBearing
H: advanceHeight - topSideBearing - bottomSideBearing
A:glyphAdvances
OX:advanceOffset
OY:ascenderOffset
父の描画範囲はadvanceOffset(4.9) + leftSideBearing(-2.9) = X(2.0)、ascent(25.8) + topSideBearing(-3.7) - verticalOriginY(17.4) - ascenderOffset(0.0) = Y(4.6)、advanceWidth(9.8) - leftSideBearing(-2.9) - rightSideBearing(-1.6) = W(14.4)、advanceHeight(22.5) - topSideBearing(-3.7) - bottomSideBearing(9.2) = H(17.0)という感じで算出される。それぞれの範囲を計算して、left、topの最小、right、bottomの最大を求めたのが絵文字全体の描画範囲となる。
上の方で「gm.advanceWidthが次の文字までの距離」と書いたが、実際にはglyphAdvancesの合計が次の文字までの距離となる。通常の1文字であればadvanceWidth=glyphAdvancesとなっているが、複数要素の絵文字の場合advanceWidthは、その要素単位の幅であって全体の幅には使えない。
結合文字対応
基本的には絵文字対応の内容で問題ないけど、よく5chなどでこんな書き込みがある。
m9(ด็็็็็็็็็็็็็็็็็Дด็็็็็็็็็็็็็็็็็)プギャーw
ブラウザ、VisualStudioのエディタはこの文字列の表示に対応してた。
![]() |
| VisualStudioエディタ |
メモ帳は1行の範囲でクリッピングされていた。
![]() |
| メモ帳 |
いくらでも文字を合成できてしまうため、メモ帳と同じように制限を加える。
一時領域に描画しているため、水平方向に関してはマイナス、一時領域の幅以上の場合はクリッピング、垂直方向はマイナス、ascent + descentを超えた分はクリッピングする。
こうして見ると、ブラウザとメモ帳でadvanceOffsetの解釈が逆になっていて、VisualStudioはadvanceOffsetを無視してるっぽい。
スペース対応
最後にスペースの文字列について。
スペースはビットマップの範囲が0になっていたため、ビットマップ参照の為のLock関数でエラーになっていた。幅0の場合はビットマップ参照はスキップして、文字情報だけ書き込むようにする。
また描画の際、幅0の場合は何もせず次の文字までの幅だけを足すようにする。


