はいはい縦書き縦書き・・・・・ッ!?


”縦書き”ってどうやるの?

調べてみました。

エセ縦書き

【縦書き文庫】はじめに(管理人)

下に載っているのは全部エセ縦書きです。
縦書きサイト普及委員会


やっていることは

 いな           新
 いん    ,─--.、
 言と   ノ从ハ从   ス
 葉聞   .リ ´∀`§
 かこ    X_@X    レ
 |え   U|_____|U
 |の    ∪ ∪
 ! !

と同じです。横向きを無理やり縦に見せてるわけですね。これを改善するには以下が焦点となります。

  • センターラインからのズレ
  • 横向きと縦向きで形が違う文字 ex)「」ー

これをfreetypeとtypeface.jsで改善して見ます。

ズレを直す

 そもそもの問題として、フォントによって縦書き出来るかどうかが分かれます。縦書きが可能なフォントには、縦書きした時のグリフ位置情報(縦書き用メトリクス)が含まれています。
このようなフォントは、bearingXやbearingY、advence(送り幅)がvertical(縦書き)とhoraizontal(横書き)で固有に用意されています。以下に縦書きメトリクスと横書きメトリクスの概略図を示します(参考資料1)。


freetype

freetypeを使って縦書き用メトリクスを取得します。face->glyph->metricsより取得できますが、値はスケーリングされているのでDOUBLE_FROM_26_6するのを忘れないでください。

//メトリクスを読み込み
//DOUBLE_FROM_26_6については参考資料2を参照のこと
fprintf(fw,"\"hbX\":%.2f,",DOUBLE_FROM_26_6(face->glyph->metrics.horiBearingX));
fprintf(fw,"\"hbY\":%.2f,",DOUBLE_FROM_26_6(face->glyph->metrics.horiBearingY));
fprintf(fw,"\"vbX\":%.2f,",DOUBLE_FROM_26_6(face->glyph->metrics.vertBearingX));
fprintf(fw,"\"vbY\":%.2f,",DOUBLE_FROM_26_6(face->glyph->metrics.vertBearingY));
fprintf(fw,"\"w\":%.2f,",DOUBLE_FROM_26_6(face->glyph->metrics.width));
fprintf(fw,"\"h\":%.2f,",DOUBLE_FROM_26_6(face->glyph->metrics.height));
fprintf(fw,"\"va\":%.2f,",DOUBLE_FROM_26_6(face->glyph->metrics.vertAdvance));
fprintf(fw,"\"ha\":%.2f,",DOUBLE_FROM_26_6(face->glyph->metrics.horiAdvance));

ちなみにFT_Load_Glyphのオプションに

#define FT_LOAD_NO_HINTING                   0x2

というのが有りますが、現時点では何も起きません。APIの説明にも「Load the glyph for vertical text layout. Don't use it as it is problematic currently.」と有ります。設定しない方が無難です。


注意:曲線のパスは横書きメトリクス原点のままです。縦書き用にパス位置を直すのに、ここでは行わないで、typeface.jsに任せます。横書きメトリクス情報も必ず入れておいてください。


主題とは関係ないですが、FT_New_Memory_Faceの説明をしておきます。これはファイルからメモリに展開されたフォントバッファからfaceを読み込みます。freadとmallocを使ってファイルをメモリに展開した後に使用してください。

//参考資料3より拝借
//フォントファイルをメモリに写す
int errcode = 0;                  //  an error code
char *buff = NULL;               //  pointer to loaded font file
size_t length = 0;                //  length of buffer
size_t count = 0;                 //  length of file (obviously should be equal to above)
FILE *fp;

fopen_s(&fp,"[path]\\FONT.TTF","rb");
if (fp){
	errcode = fseek(fp, 0L, SEEK_END);
	if (!errcode){
		length = (unsigned long)ftell(fp);
		errcode = fseek(fp, 0L, SEEK_SET);
		if (!errcode){
			buff = (char*)malloc(sizeof(char) * length);
			if (buff){
				count = fread(buff, sizeof(char), length, fp);
				if (ferror(fp) == 0 && count == length){
					errcode = fclose(fp);
				}
			}
		}
	}
}

error=FT_New_Memory_Face(library,(FT_Byte*) buff,(FT_Long) length,0,&face);
typeface.js

 freetypeで横書き、縦書きメトリクス情報と曲線パスをjsファイルに押し込めたら、typeface.jsで縦に文字を組んでいきます。とりあえず本格的な組みエンジンを作るのは後回しにして、文字を縦に並べます。曲線パスは横向きメトリクス基準なので、これを縦書き基準にずらします。パス自体をずらさずに座標をずらします(参考資料4)。以下のコードはバックエンドがcanvasの場合です。IEの場合はVMLで描画されるのでその処理も同様に追加しておく必要があります。

//_typeface_js => vectorBackends => canvas => _renderGlyphにおいて

//glyph.hbX : そのグリフのhorizontal bearing X
//glyph.hbY : そのグリフのhorizontal bearing Y
//glyph.vbX : そのグリフのvertical bearing X
//glyph.vbY : そのグリフのvertical bearing Y

var glyph = face.glyphs[char.charCodeAt(0)];
ctx.translate(-(glyph.hbX-glyph.vbX),-(glyph.hbY+glyph.vbY));//追加:座標をずらす
if (!glyph) {
	//this.log.error("glyph not defined: " + char);
	return this.renderGlyph(ctx, face, this.fallbackCharacter, style);
	〜コード〜
}

ctx.translate((glyph.hbX-glyph.vbX),(glyph.hbY+glyph.vbY));//追加:座標を元に戻す
}

if (glyph.va) {
	var letterSpacingPoints = 
		style.letterSpacing && style.letterSpacing != 'normal' ? 
			this.pointsFromPixels(face, style, style.letterSpacing) : 
			0;
	ctx.translate(0,-(glyph.va + letterSpacingPoints));//修正:送り幅を縦にする
}

グリフ置き換え

 「ー」のように横書きと縦書きで位置や形が違う文字は、置き換える必要があります。OpenType/TrueTypeにおいて、ある文字に置き換え文字が存在するかどうかはGSUBテーブルで定義されています(参考資料5)。このテーブルはAdvanced Typographic Tables(参考資料6)の一部であり、主に複雑な文字組みとなる場合に照会されます。

GSUBは以下のような構造をしています。ScriptList、FeatureList、LookupList、Coverageの説明は参考資料7に有ります。

このテーブルから縦書きグリフのIDを求めるには参考資料8が詳しいです。引用します。

肝心の表示処理のフローですが特に難しいところはありません。

FT_Get_Char_Index でキャラクタのグリフを取得して、

それを読み込んだデータで(対応はまずいのだけれども)Scriptはさておき、FeatureからFeatureTagが'vert'のものをとり、 LookupListIndexで対応付けられているLookupの中からLookupTypeが1のものをとり、SingleSubstFormat1 か SingleSubstFormat2 になりますが、それの Coverage をみて、glyphIDが範囲内か確認し、SingleSubstFormat1 ならDeltaGlyphIDをもとのグリフ番号に足してやればいいし、SingleSubstFormat2 ならば Coverage 内のglyphのインデックスをもらって、Substituteの対応するインデックスのものをそのまま使えばいいのです。

freetypeではこのテーブルのパーサーを用意してくれていません。同氏は以上の解析を行うC++Classを作成して公開していますので、使わせてらうことにしました(参考資料9のaoview20060317src.lzh)。

freetype

 C++でファイルを読み込むとシステムはそれをリトルエンディアンで解釈します。一方でフォントファイルはビッグエンディアンで格納されています。freetypeはGSUBのパーサーは用意していませんが、GSUBをバリデータするFT_OpenType_Validate関数を用意しています。ただし、標準では使えません。以下の操作を必ず行ってください。

  1. freetypeのソリューションファイルを開きます
  2. Source FilesのFT_MODULESに「freetype-2.3.12\src\otvalid\otvalid.c」を追加します。cファイルですが、問題有りません。
  3. ftmodule.hに「FT_USE_MODULE( FT_Module_Class, otv_module_class )」の一文を追加します。
  4. ビルドしてエラーがでなければ、既存のlibに代わってこれを使用します。

GSUBテーブルをバリデータします。

#include FT_OPENTYPE_VALIDATE_H
〜〜
FT_Bytes BASE=NULL;
FT_Bytes GDEF=NULL;
FT_Bytes GPOS=NULL;
FT_Bytes GSUB=NULL;
FT_Bytes JSTF=NULL;
〜〜
error = FT_OpenType_Validate(face, FT_VALIDATE_GSUB,&BASE,&GDEF,&GPOS,&GSUB,&JSTF);

参考資料9のaoview20060317src.lzhのttgsubtable.hpp、ttgsubtable.cpp、common.hをプロジェクトに追加します。GSUBをパースします。

CTTGSUBTable cttgt;
printf("%-50s","Parse GSUB Table...");
if(!cttgt.LoadGSUBTable(GSUB)){
	printf("%-10s\n","[No]");
}else{
	printf("%-10s\n","[Yes]");
}

グリフに縦書きグリフがあれば、グリフインデックスを置き換えます。

while ( gindex != 0 ){
	//縦書きグリフがあるかをチェック
	if(cttgt.GetVerticalGlyph(gindex,(uint32_t *) &vgindex)){
		gindex=vgindex;
	}
	error=FT_Load_Glyph(face,gindex,FT_LOAD_NO_HINTING);
	〜〜〜
}
typeface.js

変更点は有りません。

サンプルコード

省略します。必要であれば、コメント欄に申し出てください。

結果

今回作成したプログラムで、MS PGothicをJSFontに変換し、typeface.jsで表示したものをキャプチャしました。

ズレや向きの間違いなく表示されました。禁則処理や組版をtypeface.jsに実装するのは次回以降とします。

typeface.jsで日本語を

 Web製作者がクライアントのフォントレンダリングに手を加えることは今のところ出来ない。しかし、typeface.jsを使うとそれを擬似的に実現することができる。

typeface.js -- Rendering text with Javascript, <canvas>, and VML


With typeface.js you can embed custom fonts in your web pages so you don't have to render text to images.

仕組みはこうである。使用するフォントのFontファイルより輪郭のベクターデータ、メトリクス、著作情報等を抜き出し、それをJSON形式にして、jsファイルに書き込む。使用したいhtmlでそれを読み込み、jsのレンダリングエンジンが、対象テキストから分解された文字をcanvasに描く。最後にcanvasを対象テキストと置き換える。

typeface.jsはフォントを変換する際に、予め大きいポイント(以下pt)で行い、それをjsの方で縮小することでアンチエイリアスを実現する。

なんとも強引な方法ではあるが、一応の解決を見る事はできる。問題はFontファイルからJSON形式のフォントに変換するところだが、このあたりはオンラインの変換ツールを用意することで解決している。ただし、このオンライン変換ツールは日本語には対応していない。変換は出来るのだが、実際に試してみると上手くいかない。配布されている変換スクリプトも、動作させることは難しい(前回の記事参照のこと)。

そこで今回は、FontファイルからJSON形式のフォントを作成し、実際に表示させるところまでを行った。

準備

 フォントライブラリには定番のFreeType2(以下FreeType)を、言語にはC++を使用し、CUIのアプリケーションを作成する。FreeTypeは参考資料1を見て、環境にセットアップしておく。変換するフォントはMS Pゴシックを使用する。TTCファイルの分割には参考資料2を利用する。

typeface.jsの修正

 typeface.jsではグリフのインデックスに文字そのものを利用している。これではUnicode対応のエディタ等でしか開けない等、不便が多いので10進のUnicodeにする。以下のようにface.glyphsのインデックスにはすべて、charCodeAt(0)を付ける。

//var glyph = face.glyphs[char];
var glyph = face.glyphs[char.charCodeAt(0)];

文字サイズの設定

 ライブラリ、フォントファイルの読み込みのコードの後には、文字サイズの設定が必要である。これをしないとパスの取得時におかしくなる。また、ここで設定する大きさでフォントからベクター、ラスターのどちらが読み込まれるかが決定する。

	error = FT_Set_Char_Size(
		face, /* handle to face object */
		100*64, /* 文字幅:ptの1/64の大きさを設定する。今は100ptに設定してある。 */
		100*64, /* char_height in 1/64th of points */
		1000, /* horizontal device resolution */
		1000); /* vertical device resolution */

グリフを読み込み

 任意のグリフのベクターデータを読みだすには以下の手順を取る。

  1. cmapで支持されたエンコーディングで文字を文字コードに変換する。 //Unicode: A -> 65
  2. FT_Load_Char関数で文字コードを元に、faceにグリフをセットする。
  3. faceのグリフスロットよりFT_Get_Glyph関数でグリフgを取り出す。
  4. g->formatにてグリフのフォーマットを確認し、アウトランであることを確認する。
  5. グリフgをアウトライングリフ(FT_OutlineGlyph)ogにキャストする。
  6. og->outlineにてアウトラインデータを取り出す。
  7. FT_Outline_Decompose関数を使用し、パス形式で出力する。同関数の使い方は参考資料3にサンプルがある。注意点として、文字サイズを64倍したものを設定しているので、出力時には64で割ったものを出す必要がある。

 格納されているグリフを順次処理するには上記の処理を以下の処理で括ればいい。

  1. FT_Get_First_Char関数で最初の文字コードとグリフインデックス(cmapにて文字コードにつけられているインデックス番号のこと)を読みだす。
  2. 上記の処理を行う。
  3. FT_Get_Next_Char関数を使用して次の文字の文字コードとグリフインデックスを読みだす。
  4. グリフインデックスが0になるまで処理を繰り返す。

グリフ固有のメトリクス

 メトリクスとはグリフの位置関係情報のことである。アセンダやディセンダ、バウンディングボックス(文字に内接する長方形のこと)といったものがそれに当たる。このあたりの解説はFreeTypeチュートリアルの2(参考資料4)に詳しい。

以下の画像はwikipediaの「書体」の項目より引用させてもらっている。


typeface.jsでは各グリフのバウンディングボックス情報と送り幅(advance)を必要とする。これは以下のようなコードでとることができる。

FT_BBox bbox;
FT_Glyph_Get_CBox( g, FT_GLYPH_BBOX_UNSCALED, &bbox );
fprintf(fw,"\"x_min\":%.2f",DOUBLE_FROM_26_6(bbox.xMin));

この方法でとるバウンディングボックス情報とface->glyph->metricsでとることのできる情報から求まるバウンディングボックス情報は同じである。送り幅はこちらの方法でとる。

fprintf(fw,",\"ha\":%.2f,\n",DOUBLE_FROM_26_6(face->glyph->metrics.horiAdvance));

全体のメトリクス

 typeface.jsはディセンダやアセンダ、行間、全グリフの最小、最大バウンディングボックス情報といったものを求める。ここで注意するのはスケーリングされた情報を取る必要がある点である。スケーリングされたディセンダやアセンダ、行間はface->size->metricsより取得出来る。

//face->ascender; 
DOUBLE_FROM_26_6(face->size->metrics.ascender);

JSON形式のフォントには最小、最大バウンディングボックス情報の項目もあるが、これらはtypeface.jsでは必要とされない。その為適当で良い。

著作情報及びその他の情報

 familyName、css〜のみが必要とされる。original_font_informationにある著作情報はレンダリング時には必要とされない。時間があれば付け加えればいい。

サンプルコード

 長いので省略。必要とあればコメント欄へ。

結果

 MSPゴシックの大きさは約8MB、これを今回プログラムで変換したところ約20MBもの大きさとなった。これを修正したtypeface.jsと共に読み込み、適当なテキストを充てがった結果が以下である。

16pxで表示(上がネイティブ、下がレンダリングテキスト)

12pxで表示(上がネイティブ、下がレンダリングテキスト)

レンダリングの結果は上々といったところだ。感触としてはPDFで文字を表示したときに似ている。少なくともネイティブで表示した時よりも好感が持てるだろう。ただ、フォントサイズがあまりにも巨大なために実用性は無きに等しい。これを解決するには、テキストごとにフォントを作成するしかないが、これはそんなに難しくはないだろう。

次回以降の課題としては、これを利用した縦書きへの挑戦が上げられる。JSでの縦書き処理を行うスクリプトは幾つかあるが、フォントの縦書きレンダリングにまで踏み込んだ物は現在確認出来ていない。期待しておいて欲しい。

C++でWindowsのIMEを作ろう( ゚∀゚) その4

1.前回

第1回目:C++でWindowsのIMEを作ろう( ゚∀゚) その1 - Webと文字
前回:C++でWindowsのIMEを作ろう( ゚∀゚) その3 - Webと文字

前回は候補ウィンドウと変換候補の取得についてやりました。今回はレンジ(テキストの一定の範囲)に表示属性(文字の色や下線のタイプ)を設定します。

2.サンプル

 参考資料1のCompositionStringUnderline.zipがそれです。以下の画像の用にコンポジションにレンジをかけて、そこに表示属性を設定します。
入力状態:

変換状態:

確定:

解凍したフォルダからプロジェクトを作成したら、念のためglobals.cppにあるGUIDの値は参考資料2を利用して全て書き換えておきます。また、以下の一文をglobals.hの頭に加えておきます(参考資料3)。

#pragma warning ( disable : 4996 )

3.TSFにおける表示属性

 TSFにおいてレンジに表示属性を当てるにはかなり煩雑な手間が必要です。HTMLとCSSのように簡単にはいきません。以下順を追って表示属性の説明を行い、其れに伴って新しい表示属性を1つ作成します。完全には理解しておらず、推測も含まれています。間違っている可能性が十分にあります。基本的な情報は参考資料4にあります。

a.表示属性のGUIDを用意

 表示属性は一つのオブジェクトとして扱われるため、其れを示すGUIDが必要です。globals.cppのc_guidDisplayAttributeInput、c_guidDisplayAttributeConvertedが其れです。globals.hにその宣言があります。以下のコードを追加します。

//////globals.h
extern const GUID c_guidDisplayAttributeConverted_no_target;
/////globals.cpp
static const GUID c_guidDisplayAttributeConverted_no_target = 
{ 0x37c776ba, 0x5d24, 0xb3e1, { 0x6e, 0x25, 0xc2, 0x76, 0x29, 0xdf, 0x13, 0x54 } };
b.表示属性を用意する

 表示属性はITfDisplayAttributeInfoを実装したクラスで表現されます。サンプルではDisplayAttributeInfo.hでITfDisplayAttributeInfoを実装したCDisplayAttributeInfoクラスを作り、個々の表示属性(入力状態とか変換状態とか)は其れを継承したクラスで表現してあります。細かい処理などはCDisplayAttributeInfoにあるので、新たに表示属性を作るのは簡単です。クラス名とインスタンス名を変更して、_pguidにaで作成したGUIDのアドレスを入れるようにします。

//コードはコピペ
class CDisplayAttributeInfoConverted_no_target: public CDisplayAttributeInfo{
public:
    CDisplayAttributeInfoConverted_no_target(){
        _pguid = &c_guidDisplayAttributeConverted_no_target;
        _pDisplayAttribute = &_s_DisplayAttribute;
        _pszDescription = _s_szDescription;
        _pszValueName = _s_szValueName;
    }
    static const TF_DISPLAYATTRIBUTE _s_DisplayAttribute;
    static const WCHAR _s_szDescription[];
    static const TCHAR _s_szValueName[];
};

作成したクラスの中身はDisplayAttributeInfo.cppで定義されています。_s_szValueNameと_s_szDescriptionは適当に書いておきます。_s_DisplayAttributeは実質的な表示属性を表す構造体になります。其々の意味は参考資料5とコメントを見てください。

const TF_DISPLAYATTRIBUTE CDisplayAttributeInfoConverted_no_target::_s_DisplayAttribute ={
    { TF_CT_COLORREF, RGB(0, 255, 0) }, // text color 文字の色
    { TF_CT_NONE, 0 }, // background color (TF_CT_NONE => app default) 背景色
    TF_LS_DASH,                             // underline style 下線のスタイル
    FALSE,                                  // underline boldness 下線を太くするか
    { TF_CT_COLORREF, RGB(255, 255, 0) },   // underline color 下線の色
    TF_ATTR_TARGET_NOTCONVERTED             // attribute info 属性情報
};
c.表示属性をTSFマネージャー(以下TSFMgr)に提供する

 作成した表示属性はTSFMgrに提供しないと使える様になりません。TSFMgrは以下の用にして表示属性を知ります。

  1. テキストサービスがITfCategoryMgr::RegisterCategory(ITfCategoryMgrはTSFMgrが実装、実質的にTSFMgr)で表示属性プロバイダとして登録されている。
  2. テキストサービスにに実装されているITfDisplayAttributeProviderのEnumDisplayAttributeInfo関数を呼び出して、テキストサービスが提供している表示属性の一覧情報(IEnumTfDisplayAttributeInfo)を取得する。
  3. 取得したIEnumTfDisplayAttributeInfoのNext関数を呼び出して、表示属性オブジェクトの配列を入手する。

サンプルではITfDisplayAttributeProviderとEnumDisplayAttributeInfoは其々ITfDisplayAttributeProvider.cppとEnumDisplayAttributeInfo.cppで実装されています。ただし、EnumDisplayAttributeInfo.cppのNext関数は新しく作成した表示属性のCDisplayAttributeInfoConverted_no_targetを返しませんから、whileループの中を以下の用に修正します。

        if (_iIndex > 2) break;

        if (_iIndex == 0){
            if ((pDisplayAttributeInfo = new CDisplayAttributeInfoInput()) == NULL)
                return E_OUTOFMEMORY;
        }else if (_iIndex == 1){
            if ((pDisplayAttributeInfo = new CDisplayAttributeInfoConverted()) == NULL)
                return E_OUTOFMEMORY;
        }else if(_iIndex ==2){
            if ((pDisplayAttributeInfo = new CDisplayAttributeInfoConverted_no_target()) == NULL)
                return E_OUTOFMEMORY;
       }

これで、TSFMgrに新しく作成した表示属性を知らせることができました。またITfDisplayAttributeProvider.cppのGetDisplayAttributeInfo関数はIEnumTfDisplayAttributeInfoを実装したので使う必要はありませんが、一応修正しておいてください。

d.TSFMgrにある表示属性をGuidAtomとして取得

 cまででTSFMgrには表示属性が登録されたことになります。今度はTSFMgrから表示属性を示すGuidAtom(TSFオリジナルなGUID見たいなもの)を取ってくる必要があります。
TSFMgrから表示属性GAを取ってくるにはCategoryMgrのRegisterGUID関数を使います。推測ですがこの関数は以下のような動作になっていると思われます。

  1. TSFMgrに登録されている表示属性(ITfDisplayAttributeInfo)のGetGUIDを呼び出す。
  2. 表示属性は自身のGUIDを返すので、引数のGUIDと一致しているかチェックする
  3. 一致したら表示属性を示すGuidAtomを返す。

サンプルでは新しく表示属性を作ったのでTextService.hにGuidAtomを用意し、それにRegisterGUIDで表示属性のGuidAtomを入れます。

//TextService.h
TfGuidAtom _gaDisplayAttributeConverted_no_target;

//DisplayAttribute.cppの_InitDisplayAttributeGuidAtom関数
hr = pCategoryMgr->RegisterGUID(c_guidDisplayAttributeConverted_no_target, &_gaDisplayAttributeConverted_no_target);
e.表示属性をレンジに充てる

 ここまでくれば後はSetValue関数にレンジとdのGuidAtomが入ったVARIANTを入れて呼び出すだけです。詳細は参考資料4と参考資料6を見てください。
 サンプルのコードではDisplayAttribute.cppの_SetCompositionDisplayAttributes関数が其れを一手に担っています。変換時にレンジを少し動かし*1、新しく作った表示属性を適応させるコードに書き換えます。

        VARIANT var;
	VARIANT var2;
        var.vt = VT_I4; // set a TfGuidAtom
        var.lVal = _gaDisplayAttributeConverted_no_target;
		var2.vt =VT_I4;
		var2.lVal = gaDisplayAttribute;
		if(splitflag){ //変換時にはTRUE
			pRangeComposition->ShiftEnd(ec,-5,&cch,NULL);
			hr = pDisplayAttributeProperty->SetValue(ec, pRangeComposition, &var);
			pRangeComposition->Collapse(ec,TF_ANCHOR_END);
			pRangeComposition->ShiftEnd(ec,+3,&cch,NULL);
			hr = pDisplayAttributeProperty->SetValue(ec, pRangeComposition, &var2);
		}else{
			hr = pDisplayAttributeProperty->SetValue(ec, pRangeComposition, &var2);
		}
        pDisplayAttributeProperty->Release();

こんな感じになりました:

4.次回

レンジの下にウィンドウ表示

         ∧ ∧
       ヽ(・∀ ・)ノ <次回もサービス、サービス!!
       (( ノ(  )ヽ ))
         <  >

*1:レンジの動かし方はその2を見てください。

C++でWindowsのIMEを作ろう( ゚∀゚) その3

1.前回やったこと

 第1回目:C++でWindowsのIMEを作ろう( ゚∀゚) その1 - Webと文字
 第2回目:C++でWindowsのIMEを作ろう( ゚∀゚) その2 - Webと文字


前回はコンポジションのテキスト入力とキャレットについてやりました。今回は候補ウィンドウと変換候補の取得についてです。

2.候補ウィンドウ

候補ウィンドウ(CandidateWindow)はコンポジションエリアのすぐ下に現れて、変換候補の提示をするものです。候補窓を使ったサンプルは参考資料1のサイトのCandidateList.zipがそうです。ファイル郡からプロジェクトを作成し、ビルドして登録すれば動作するサンプルが得られます。

この候補ウィンドウはコンポジションの用にTSF特有の物ではありません。標準的なWindowsアプリケーションで作成する様にしてウィンドウを作成する必要があります(参考資料2)。ウィンドウにタイトルバーが無いのや、タスクバーに現れないのは拡張スタイルやスタイルなどでそう設定されているからです。(CandidateWindow.cpp)。

3.簡単なウィンドウを作る

 候補ウィンドウはTSFに依存しないので、独自につくっておいて後でくっつけることにします。その方がデバッグも簡単になります。また、VC2008ExにはMFCは入っていないので直接APIを叩いて描いていきます。以下が出来上がったウィンドウです。

サンプル(バイナリとソース)(人柱注意):http://www28095u.sakura.ne.jp/windows_ime/test2.zip

a.ウィンドウの基本的な作り方

参考資料3の1から14までを見てください。

b.文字と矩形の描画

文字はTextOut関数(参考資料4)で、矩形はRectangle関数で描きます。文字色や背景を変えるのは参考資料3を見てください。書くのではなくて描くのでドラッグで選択をすることはできません。選択番号にある場所は色を変えて矩形を描画します。

c.キーボード入力

 上下の矢印キーを押すと選択範囲を変更するようにします。参考資料5をみてキーボードイベントをキャッチするようにし、選択番号をインクリメントもしくはデクリメントします。その後、再描画させるためにInvalidateRect関数を呼びます(参考資料6)。

d.ちらつきを防ぐ

 キーイベントによって再描画されるため、ちらつきが発生します。ちらつきを防ぐために「裏画面(ダブルバッファリング)」という手法を使います。これはメモリ上に用意した画面オブジェクト(ビットマップ)に対して書き込み、全てが終わった後に其れを実画面に反映させる方法です。参考資料7に具体的な方法とソースがありますので参照してください。またcのInvalidateRectの3つ目の引数はFALSEにする必要があります(参考資料8)。


サンプルではフォントや、文字列の幅、ウィンドウの大きさなどに注意を払っていません。これらを設定する必要があります。

4.変換候補の取得

 IMEとして動作するにはネットから変換候補を取ってこなくてはいけません。httpを使って通信するにはWinInetを使用します。この部分もTSFとは独立に考える事ができるので、ひらがな文字列を入力して候補を表示するコンソールアプリケーションを作成してみます。以下が出来上がったソフトです。変換サーバーはSocialIMEを使用しています。

サンプル(バイナリとソース)(人柱注意):http://www28095u.sakura.ne.jp/windows_ime/test4.zip

a.コマンドプロンプト

 コマンドプロンプトが標準で使用する文字コードはCp932(≒SJIS)です。フォントやウィンドウの幅を変更するには参考資料9を参照してください。WebAPIの文字コードに気をつける必要があります。

b.WinInet

 基本的な通信方法は参考資料10を参考にしてください。VC2008Exでやるとリンクエラーが出ました。その場合はコードの先頭に

#pragma comment(lib, "wininet.lib") 

と入力してください(参考資料11)。

c.URLエンコード

 ひらがな文字列はURLエンコードされて送信される必要があります。URLエンコード(参考資料12)を行う関数はC++には標準で用意されていませんでした。一文字一文字変換する簡易なコードを以下に示します。仕様にあいません。

for(i=0; i<strlen(b);i++){
	TCHAR c[10];
	_stprintf_s(c,"%%%x",b[i] & 0xFF);
	_tcscat_s(w,1000,c);
}

5.次回

レンジの処理。色をつけたり、下線を引いたり。

C++でWindowsのIMEを作ろう( ゚∀゚) その2

1.前回

 前回C++でWindowsのIMEを作ろう( ゚∀゚) その1 - Webと文字はアイコンの変更とテキストの変更をやりました。

2.コンポジション

 コンポジションとはテキスト入力における一時的な入力エリアです(参考資料1)。

コンポジションを使ったサンプルはTSFmarkにあります。このサンプルのコンポジションは純粋にTSFに対応した様なアプリケーション(WordとかWordPadなど)にしかあたりません。其れを解消するには登録言語を変更する必要があります。globals.hのMARK_LANGIDマクロを以下の用に変更します。このマクロは以前に説明したAddLanguageProfileで使用されます。

#define MARK_LANGID    MAKELANGID(LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN)

こうすることでテキストサービスが日本語用として登録されます。言語バーには日本語キーボードに追加されます。

これでTSFに対応していないようなアプリケーションでもコンポジションが描画される様になります。正し、レンジの伸張など幾つかの動作が異なることと、アプリケーションでコンポジションを描画しているFireFoxなどでは表示属性(テキスト色など)がうまく当たりません。

 コンポジションを開始するにはStartCompositionで、終了するにはEndCompositionを呼び出します。詳しくは参考資料1とサンプルのcompose.cppを参照してください。

3.レンジ

 非常にとっつきにくい考えとしてRangeがあります。これはある文章の一部分を示すオブジェクトで、網掛けのない範囲選択と考えると分かりやすいです。範囲内に入っているテキストを捜査することができます。これについては非常に分かりやすい解説が参考資料2にあります。テキストがレンジ要素によって構成されていると考えるのは間違いです。Rangeは単に範囲のオブジェクトで、その中にある要素を操作することができるだけです。レンジの範囲を変えてもテキストに変わりはありません。
 一方でテキストの最初の文字からのオフセットで文字を管理する方法をACPと言います。アプリケーションはこれを元にテキストを操作しているようです。これについては参考資料3に分かりやすい解説があります。しかし、コンポジションではACPは使えない?ようですのでレンジを使った文字操作をしていくことになります。

4.レンジの幾つかのメソッド

 ShiftStartはレンジの始端を動かすことができるメソッドです(参考資料4)。二つ目の引数cchReqは動かす文字数です。負数にすれば左に、正数にすれば右に動きます。4つ目の引数はNULLで使わないのならNULLでかまいませんが、3つ目の引数は使わなくても指定して置いてください。ShiftEndは同様の方法で終端を動かすことができるメソッドです。

ShiftStart
・cchReq = 1  : ABC[DE]FG → ABCD[E]FG
・cchReq = -1 : ABC[DE]FG → AB[CDE]FG
ShiftEnd
・cchReq = 1  : ABC[DE]FG → ABC[DEF]G
・cchReq = -1 : ABC[DE]FG → ABC[D]EFG

 GetTextはレンジの範囲にある文字列を取得することができるメソッドです(参考資料5)。SetTextはレンジの範囲の文字列を書き換えることができるメソッドです。二つ目の引数のフラグにはTF_ST_CORRECTIONを入れておけばいいと思います。

 Collapseはレンジの始端と終端をくっつけるメソッドです。つまりレンジの範囲が空になります。二つ目の引数によって始端を終端に合わせるか、終端を始端に合わせるかを選べます。これは次で述べるキャレット位置の調整の際に使われます(参考資料6)。

・aPos=TF_ANCHOR_START: ABC[DE]FG → ABC[]DEFG
・aPos=TF_ANCHOR_END  : ABC[DE]FG → ABCDE[]FG

5.セレクションとキャレット

 網掛けの範囲選択部分を一般的にセレクションと言います。文字入力をする時に点滅しているカーソルを一般的にキャレットと言います。TSFに独立したキャレットと言う概念はありません。範囲0のセレクションが自動的にキャレットになります。セレクションは何かを選択している状態なので、即ちキャレットは空の範囲を選択している状態です。空の範囲とは空のRangeにあたります。ShiftStartもしくはEndでレンジを動かし、Collapseでレンジを空にし、其れをドキュメントのITfContextのSetSelectionで選択することでキャレット位置を変えます。

6.WCHARとwstring

 ほとんどの関数は文字列にWCHAR s[]を要求します。これはケツに\0を要求するので書き換えが面倒くさいです。wstringを使うと書き換えを楽にできます。wchar*もしくはwcharからwstringにはassignメソッドを、wstringからwchar*にはdataメソッドを使用します。wchar*に文字列を吐き出す関数には配列を渡してケツに\0を入れてその配列からassignでwstringを精製します。

7.ひらがな文字列をにゅうりょくする

 コンポジションでローマ字によるひらがな文字列の入力を考えます。まずはローマ字ひらがな変換テーブルです。以下の用に定義しました。カタカナとかも定義できそうです。

#if !defined (DATA_H)
#define	DATA_H

typedef struct tagTable{
    LPCWSTR	pRoma;
    LPCWSTR	pHiragana;
} RTABLE;

static RTABLE hiragana[] = {
 { L"a", L"あ"},
 { L"i", L"い"},
 { L"yi", L"い"},
 〜

一部を省いた変換部分のコードです。

	ptt1[2]='\0';
	tfSelection.range->ShiftStart(ec,-2,&tm1,NULL);
	tfSelection.range->GetText(ec,TF_TF_MOVESTART,ptt1,2,&tm2);

	//ptt1=XY\0

	str1.assign(ptt1);//str1=XY

	if(!iswalpha(str1.at(1))){ //Is Y alphabet ?
		i=0; //No
	}else if(!iswalpha(str1.at(0))){ //Is X alphabet?
		i=1;
	}else{
		i=2;
	}

	str1.append(1,ch);//str1=XYA


	switch(i){
		case 2:
			if(roma2hiragana(str1,&d)) break;
		case 1:
			i=1;
			if(roma2hiragana(str1.substr(1,2),&d)) break;
		case 0:
			i=0;
			if(!roma2hiragana(str1.substr(2,1),&d)) d.assign(1,ch);
	}
	if (tfSelection.range->ShiftStart(ec,-1*i,&tm1,NULL) !=S_OK) goto Exit;
	if (tfSelection.range->SetText(ec, TF_ST_CORRECTION,d.data(), d.size()) != S_OK) goto Exit;

    tfSelection.range->Collapse(ec, TF_ANCHOR_END);
	pContext->SetSelection(ec, 1, &tfSelection);

    // apply our dislay attribute property to the inserted text
    // we need to apply it to the entire composition, since the
    // display attribute property is static, not static compact
    _SetCompositionDisplayAttributes(ec);

バックスペースの処理関数も同じように書けます。


8.次回

 候補Windowの描画

C++でWindowsのIMEを作ろう( ゚∀゚) その1

1.目標

 JavaScriptで日本語IMEを作ろう( ゚∀゚) - Webと文字で作ったJavaScriptIMEをWindowsに移植する。

2.必要な実装

  • 文字列の挿入と一時的な入力エリアの挿入
  • 一時的な入力エリアにおけるテキストの変更とキャレット位置の取得とキャレット位置の設定と書式変更
  • 一時的な入力エリアの下に出すWindowとその書式設定
  • キーイベントの取得
  • WwbAPIより変換候補を取得する

3.準備

 今までに以下に示す関連するエントリを書きました。

 TSFの説明が日本語資料としてMSDNにあります。

4.サンプルを改造する

 アイコンの変更を行います。rcファイルをいじると、なぜかコンパイルできなくなります。また、VC++ExpressEdisionにはリソースエディタが入っていないので不便です。参考資料1のResEditをダウンロードして、参考資料2を参考にしてResEditをRCファイルに関連づけます。元の"resource.h"とcase.rcは削除します。

 言語バー用に16*16の拡張子がicoのアイコンファイルを用意します。テキストサービスを表すアイコンを一つ、ボタン用のが一つで二つ用意します。今回は参考資料3のminiアイコンを使用しました*1。ResEditを開いて新規プロジェクトで適当な場所にrcファイルを作成し、先ほどの二つのアイコンを追加します。保存すると、rcファイルとresource.hファイルができているのでVC+EEのプロジェクトに両方追加します。
 テキストサービスを表すアイコンはregister.cppのCCaseTextService::RegisterProfilesのpInputProcessProfiles->AddLanguageProfileの最後の引数によって決定されます。この引数はリソースのアイコンの通し番号です。0だと初めの一個目のアイコンになります。ボタン用のアイコンはlangbar.cppのCLangBarItemButton::GetIconのLoadImageによって決定されます。参考資料4を参考にして以下の用に書き換えました。

(HICON)LoadImage(g_hInst, MAKEINTRESOURCE(IDI_ICON2), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);

ここでIDI_ICON2はResEditによって自動的に決定されたアイコンの名前です。g_hInstはDLLのヒンスタンスです。


 hello.cppを変更して挿入するテキストの変更を行います。言語バーからテキストサービスを選ぶとITfTextInputProcessorを実装したオブジェクトのActivateメソッドが呼ばれます。その際に言語バーにボタンを追加する処理をします。ボタンはトグル、ポップアップメニュー、普通のボタンの3種類選べます。サンプルではメニューを出すものに設定されています。追加されたボタンをクリックするとそのボタンのInitMenuが呼ばれ、メニューの項目をクリックするとOnMenuSelectが呼ばれ、c_rgMenuItemsの二つの目の引数の関数が呼ばれます。hello.cppではCCaseTextService::_Menu_Helloが呼ばれて、その中でITfEditSessionを継承したインスタンスを作成しています。インスタンスからコンテクストを取得し、RequestEditSessionを呼ぶと、インスタンスのDoEditSessionが呼び出されます。実際の処理はこの中で行います。エディットセッションについては参考資料5を参考にしてください。
 DoEditSessionの処理を以下の用に変更しました。

static WCHAR s[] = L"         ∧ ∧\n       ヽ(・∀ ・)ノ <\n       (( ノ(  )ヽ ))\n         <  >";
InsertTextAtSelection(ec, _pContext, s, (ULONG)wcslen(s));

\nが改行コードであることを考慮すると上の処理は以下のような結果になることを期待しています。

         ∧ ∧
       ヽ(・∀ ・)ノ
       (( ノ(  )ヽ ))
         <  >|←キャレット


GUIDの変更を行います。参考資料6を使用してglobal.cppの全てのGUIDを変更します。

5.テキストサービスをアンレジストする

 regsvr32で登録したDLLを登録除去するにはregsvr32に-uオプションをつけてDLLを指定するだけです。これは実際にはDLLにあるDllUnregisterServer関数を読んでいるだけなので、レジストリの除去及びTSFからの除去の処理は登録の時と同じように処理を書く必要があります。

6.試す

できたDLLをビルドし、regsvr32して登録して試してみました。アイコンの変更は行われていました。

テキストの挿入は幾つかのソフトウェアで動作が異なりました。

  • ワードパット:意図した結果になりました。
  • (´д`)Edit (参考資料7):一行になってしまいました。
  • terapad:複数行になりましたが、キャレットの位置がずれました。

7.次回

 一時的なテキスト入力エリア(Composition)とその中でのキャレット位置取得と設定

8.わからないとこ

TSFのサンプルのTSFmarkのコンポジションがwordpadやWordなどのTSF対応アプリケーションでしか動作しない。その2を参照のこと

*1:gifで用意されているので適当なファイルでicoに変換する必要があります。