CoreTextを調べてみた

CoreTextというのは、OSXやiOSで使えるテキスト描画用のフレームワークです。

iOS3.2の頃から使えていたらしいので特に新しいフレームワークというわけではありません。またiOSの場合はUIKitがテキスト描画もいい感じに面倒みてくれるのであまり使う機会もないように思います。

ただし、CoreGraphicsを使って画像を出力する目的でビットマップコンテキストへ日本語を描画しようとすると文字化けしてしまうので、そのような時にはCoreTextを使うと日本語も描画することができます。(画面への描画は日本語もちゃんと出力されます。また単にテキスト画像を生成する目的であれば、iPhoneで利用できるフォントを調べる(その2)で使ったやり方でも日本語の出力が可能(しかも簡単!)ですが、画像や文字を一緒にビットマップコンテキストへ描画する場合は、CoreTextフレームワークを使った方が便利です。またCoreTextを使うと、矩形だけでなく任意のパスを指定してその範囲に文字列を描画することができます。)

 

文字化けについてどういうことかというと、英数字等の半角文字は次のようにしてそのままビットマップコンテキストに描画できます。


CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
size_t imageWidth = ...
size_t imageHeight = ...
CGContextRef ctx = CGBitmapContextCreate(NULL,
                                         imageWidth,
                                         imageHeight,
                                         8,
                                         4 * imageWidth,
                                         colorSpace,
                                         kCGImageAlphaPremultipliedFirst);
CGContextSelectFont(ctx, "Helvetica",  30, kCGEncodingMacRoman);
CGContextShowTextAtPoint(ctx, 10.f, 10.f, "I am a cat.", 11);
// CGContextShowTextAtPoint(ctx, 10.f, 10.f, "吾輩は猫である。", 11);

CGImageRef imageRef = CGBitmapContextCreateImage(ctx);
CGColorSpaceRelease(colorSpace);
CGContextRelease(ctx);
    
UIImage *image = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);
iamacat.png

 

ここで、"I am a cat."の部分を"吾輩は猫である。"と日本語にすると、文字化けします。

waganeko.png     Σ(゚Д゚;)ガーン

 

これは、CoreGrphicsでネイティブにユニコードがサポートされていないためですが、CoreTextフレームワークはユニコードをサポートしており、ビットマップコンテキストにも普通に日本語を描画することができます。

 

CoreTextで使う主要な型(注1)には以下のようなものがあります。

(注1)CoreTextはC言語で実装されており、クラスではなくクラスと似たような機能をC言語で実現できる不透過型というデータ型が使われています。

 

CTFramesetter
CoreTextの型の階層のトップレベルに位置する型で、内部的にCTTypesetterやCGPath等を使用し、文字列がレイアウトされたCTFrameを生成します。この型を使うことで例えば、複数行に跨がる様々な属性をもった文字列を任意のパスに出力することができます。属性付き文字列を出力する大体の用途はこの型を使うだけで事足りるかも知れません。

CTTypesetter
文字列の属性に従って文字(グリフ)を配置し、1行分の出力内容を表すCTLineを生成します。長い文字列を渡して適切な改行位置を調べることもできます。CTFramesetterの内部で使用されますが、CTTypesetterを単独で使用して、生成されたCTLineをグラフィックコンテキストに出力することもできます。Typesetとは植字とか活字という意味らしいです。

CTLine
文字通り1行の文字列を管理する型です。CTFramesetterやCTTypesetterから使用されますが、この型を単独で生成して文字列を出力することもできます。ラベルみたいに1行の文字列しか出力しないならこの型を使うだけでも可能です。

CTRun
同じ属性をもった一連の文字列を管理します。CTLineは1つ以上のCTRunで構成されます。そしてCTFrameは1つ以上のCTLineで構成されます。この型もグラフィックスコンテキストへ直接描画できますが、普通はこのクラスを直接は操作しません。Runは走るという意味の他に、流れとか、小川とか、種類といった意味がありますね。

CGGlyph
CoreTextで扱う文字情報の中で最も単位の小さいものになります。プレフィックスがCTではなくCGとなっているので、CGGlyphはCoreTextではなくCoreGraphicsに属しています。そしてCGGlyphは、
typedef unsigned short CGGlyph;
と定義されていて、実体は整数値です。各Glyphは1つの文字を表しますが、文字のエンコードではなく、実際に描画される文字の字形を指します。この内容はフォント毎に異なります。CTRunはCGGlyphの配列を管理します。

CTFont
CoreTextで使用するフォント型です。UIFontとは別物でtoll-free brided(相互に変換コスト無しで代用できる)でもない(でもNSFontとはtoll-free bridgedらしい)ので、UIFontとは別に生成する必要があります。

CFAttributedString
描画する対象となる属性付き文字列です。CocoaのクラスであるNSAttributedStringとtoll-free bridgedです。ただし、NSAttributedStringの属性の中にはCoreFoundationのクラスとtoll-free bridgedでないものもあるので、最初からCFAttributedStringを使った方が良いかもしれません。

CGPath
これもCoreGraphicsに属する型ですね。CoreTextでは文字列の描画範囲を指定するためにCGPathを使用します。CGPathなので、矩形に限らず、円やハート型を指定してその中に文字列を埋めるということもできます。

 

上の主要な型の関係を図で表すと次のようになります。

 

relations.png

 

実際に使ってみます。

※CoreTextを使うには、プロジェクトにCoreText.frameworkを追加し、CoreText.hをインクルードする必要があります。


- (UIImage *)makeImage
{
    // ビットマップコンテキストを生成
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    size_t imageWidth = 300;
    size_t imageHeight = 200;
    CGContextRef ctx = CGBitmapContextCreate(NULL,
                                             imageWidth,
                                             imageHeight,
                                             8,
                                             4 * imageWidth,
                                             colorSpace,
                                             kCGImageAlphaPremultipliedFirst);

    // 文字列を生成
    CFStringRef string = CFSTR("吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。");
    CFMutableAttributedStringRef attrString =
    CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
    CFAttributedStringReplaceString(attrString, CFRangeMake(0, 0), string);
    // フォントを設定
    CTFontRef font = CTFontCreateWithName(CFSTR("HiraKakuProN-W6"), 20.f, NULL);
    CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFStringGetLength(string)), kCTFontAttributeName, font);
    CFRelease(font);
    // 色を設定
    CGFloat components[] = { 0, 0, 1.f, 0.8f };
    CGColorRef blue = CGColorCreate(colorSpace, components);
    CFAttributedStringSetAttribute(attrString, CFRangeMake(8, 8), kCTForegroundColorAttributeName, blue);
    CGColorRelease(blue);
    // 描画先の矩形パスを生成
    CGMutablePathRef path = CGPathCreateMutable();
    CGRect bounds = CGRectMake(0, 0, imageWidth, imageHeight);
    CGPathAddRect(path, NULL, bounds);
    
    // CTFramesetterを使ってCTFrameを生成
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
    CFRelease(attrString);
    CGPathRelease(path);
    // ビットマップコンテキストにCTFrameを描画
    CTFrameDraw(frame, ctx);
     
    // ビットマップコンテキストから画像を生成
    CGImageRef imageRef = CGBitmapContextCreateImage(ctx);
    UIImage *image = [UIImage imageWithCGImage:imageRef];

    CGColorSpaceRelease(colorSpace);
    CGContextRelease(ctx);
    CGImageRelease(imageRef);
    return image;
}
frame.png

CFAttributedStringは、CocoaのクラスであるNSAttributedStringのCoreFoundation版です。どうせなら記述が簡単なNSAttributedStringの方を使いたいところです。実際、CFAttributedStringとNSAttributedStringは、toll-free bridgedなので、NSAttributedString自体は そのまま使用することができるのですが、NSattributedStringで属性を保持するNSDictionaryに設定する値が、toll-free bridgedでない場合があります。例えば、UIColorとCGColorRefはtoll-free bridgedではありません。そのためUIColorを使って文字列の色を指定したNSAttributedStringをCoreTextで使っても、設定した色が反映されません。このようにいろいろと使用上の制限があるため、CoreTextで使う場合は、CoreFoundationの型だけを使った方が混乱しなくて良いと思います。

 

CTFramesetterCreateFrameに渡すpathは矩形でなくても構いません。pathを次のように三角形を作成して渡すと、指定した三角形の範囲に沿って文字列が描画されます。


CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0, 0);
CGPathAddLineToPoint(path, NULL, imageWidth/2.f, imageHeight);
CGPathAddLineToPoint(path, NULL, imageWidth, 0);
CGPathCloseSubpath(path);
triangle.png

 

通常は上で示したように、CFFramesetterと、それを使って生成されるCFFrameを使うことでほとんどの用途に使えると思いますが、CTtypesetterや、CTLineなどの型を単独で使用することもできます。例えば、1行しかない短い文字列を出力するは場合は、CTLineだけを使って出力できます。


// フォント属性を設定して文字列を生成
CTFontRef font = CTFontCreateWithName(CFSTR("HiraKakuProN-W6"), 20.f, NULL);
CFStringRef keys[] = { kCTFontAttributeName };
CFTypeRef values[] = { font };
CFDictionaryRef attributes =
CFDictionaryCreate(kCFAllocatorDefault, (const void**)&keys,
                   (const void**)&values, sizeof(keys) / sizeof(keys[0]),
                   &kCFTypeDictionaryKeyCallBacks,
                   &kCFTypeDictionaryValueCallBacks);
CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, CFSTR("吾輩は猫である。"), attributes);
CFRelease(font);
CFRelease(attributes);
// 文字列を渡してCTLineを生成
CTLineRef line = CTLineCreateWithAttributedString(attrString);
CFRelease(attrString);
// 描画
CGContextSetTextPosition(ctx, 10.0, 10.0);
CTLineDraw(line, ctx);
CFRelease(line);
line.png

 

とりあえず基本的な部分を紹介しましたが、CoreTextフレームワークにはもっといろいろな機能があります。文字列の描画についてはCocoaやUIkitだけでも大体のことはできますが、レイアウト等ちょっと凝ったことをしようと思ったらCoreTextフレームワークの使用を考えてみてもいいかもしれません。よりローレベルで細かい制御ができるのでその分記述量が多くなってしまうのが難点ですが。