2015年1月24日土曜日

WPF用縦書きテキストブロック Tategaki ver.2.1.0

さて、前回はコントロールの更新だったわけですが、今回はロジックの更新になります。

とは言っても、自分はあまりフォント周りに詳しくないうえ、この辺のことは昔からWindowsシステムから上位のAPIが提供されていたこともあり、あまり情報は転がっていません。しかし、かねてから、Uniscribeに頼らず何かしらの方法でフォントファイルを直接読み出してグリフ変換等ができればいいなあと思っておりました。
今までのUniscribeを使ったTategakiTextを使っていただくとわかると思うのですが、MSゴシックやMS明朝、メイリオと言ったような代表的なフォントは何とかちゃんと動くのですが、それ以外のフォントを使うと動かないものもしばしばあり、もっと言えば、TrueType Collectionのフォントファイル(拡張子が.ttcのやつ)の扱い方が分からなかったのでMS Pゴシックなどが使えないという問題もありました。さらに、不定期で発動する、なぜかグリフインデックスを縦書き用に変換できない問題など、非常に悩ましい現象がしばしばありました。

しかし、昨日ネットを巡回していると、非常に興味深いライブラリを見つけました。

TypeLoader

もはやなんで気づかなかったんだというレベルの話ですが、TrueTypeまたはOpenTypeのフォントファイルを直接パースしてグリフインデックスを変換してくれるライブラリになります。まさに求めていたものです。
と、同時に、このトップページを見てもらってもわかりますが、結構このライブラリもWPFを視野に入れたライブラリになっているようなので、こっちに立派なWPF縦書きコントロール機能があったら、このプロジェクト自体完全にリサーチ不足による車輪の再発明だったことになっちゃうじゃんという不安もよぎりました。実際、デモプログラムも付いていましたが、コードを読んでみるとMainWindowのコードビハインドにガッツリグリフインデックス変換まわりのコードが書いてあって、私のこのライブラリのコントロールとしての立ち位置も保てそうで一安心でした。
 

さて、こうと来たらグリフインデックスの変換周りをUniscribeからTypeLoaderに置き換えましょう。
TypeLoaderにはTrueType Collectionフォントを扱う機能もあるようなので、ついでに、使用可能フォントのサーチも強化して、より多くのフォントに対応していくことにしました。

/// <summary>
/// 有効なフォントの一覧を取得するメソッド
/// </summary>
/// <param name="cultures">フォントのカルチャの配列</param>
/// <returns>ファミリ名とstringのDictionary</returns>
internal static IDictionary<string, Uri> SearchFontNamePathPair(IEnumerable<CultureInfo> cultures)
{
    IDictionary<string, Uri> dic = new SortedDictionary<string, Uri>();
    string FontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);

    var uris = Directory.GetFiles(FontDir, "*.ttf").Concat(Directory.GetFiles(FontDir, "*.otf")).Select(p => new Uri(p))
        .Concat(Directory.GetFiles(FontDir, "*.ttc").SelectMany(p => {
            using(var fs = new FileStream(p, FileMode.Open, FileAccess.Read)) {
                return Enumerable.Range(0, TypefaceInfo.GetCollectionCount(fs)).Select(i => new UriBuilder("file", "", -1, p, "#" + i).Uri);
            }
        })
    );

    foreach(Uri uri in uris) {
        try {
            GlyphTypeface gtf = new GlyphTypeface(uri);
            if(cultures.Where(p => gtf.FamilyNames.ContainsKey(p)).Count() > 0) {
                foreach(string FamilyName in gtf.FamilyNames.Values) {
                    if(!dic.ContainsKey(FamilyName))
                        dic.Add(FamilyName, uri);
                }
            }
        }
        catch(NullReferenceException) { }
    }

    return dic;
}

まずはフォントファミリ名とフォントファイルのUriを対応させるお仕事をします。システムディレクトリのフォントフォルダのパスを取得し、そこから.ttf、.otf、.ttcの3つの拡張子のファイルをすべて拾います。.ttcに関してはTypeLoaderのTypefaceInfo.GetCollectionCount()メソッドを使っていくつフォントが格納されているかを調べ、それぞれを登録していきます。
次に、UriをもとにGlyphTypefaceのインスタンスを作成し、ファミリ名を取得し登録していきます。それだけです。

GlyphTypeface gtf = new GlyphTypeface(FontUri);
TypefaceInfo ti = new TypefaceInfo(GlyphTypeface.GetFontStream(), string.IsNullOrEmpty(FontUri.Fragment) ? 0 : int.Parse(FontUri.Fragment.Replace("#", "")));
SingleGlyphConverter vert = ti.GetVerticalGlyphConverter();

ushort[] indices = Text.Select(p => vert.Convert(gtf.CharacterToGlyphMap[gtf.CharacterToGlyphMap.ContainsKey(p) ? p : '?'])).ToArray();

変換はこんな感じになります。GlyphTypefaceのインスタンスからTypefaceInfoとSingleGlyphConverterのクラスを作れば、あとはSingleGlyphConverter.Convert()を通して縦書き用グリフインデックスに変換することができます。

ですが、 TategakiMultilineは1文字ずつTategakiTextインスタンスを作っているので、TategakiMultiline.Text更新時はかなりの回数この変換ルーチンが呼ばれることになります。毎回フォントファイルをパースしていてはコストが大きいので1度生成したインスタンスは保存し、グリフインデックスはキャッシュするようにしています。まあでもそのキャッシュ機構を説明したところでわかりにくくなるだけでしょうから、興味ある人はソースコードでも眺めてください。


気が付いたらあっという間にver.1系のコードが跡形もなくなってしまいましたね。


ダウンロードはこちら
WPF用縦書きテキストブロック Tategaki ver.2.1.0

0 件のコメント:

コメントを投稿