CoreText ヒラギノフォント(日本語)で正確に描画サイズを取得する

  • 150
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

前提知識

本投稿ではCoreTextの描画については記載してません。
基礎的な描画についてはgithub0. 最小限の描画などを参考にして下さい。

また別途、
欧文書体の基礎知識
CoreTextの日本語行間の問題(解決)

もお読み下さい。

本題

ヒラギノフォントを使用した場合にでも正確に描画に必要なサイズを取得する方法です。
CoreTextでサイズを取得する場合は以下になります。

CoreTextSample.m
/* サイズを取得 */
// 属性
NSDictionary *attrDict = [NSDictionary dictionaryWithObjectsAndKeys:
                              (__bridge id)self.ctFont, kCTFontAttributeName,
                              (__bridge id)self.ctParagraphStyle, kCTParagraphStyleAttributeName, nil];
// 属性文字列の作成
NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:self.text attributes:attrDict];
// CoreTextのフレームセッターの作成
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attrStr);
// 描画に必要なサイズを取得
CGSize contentSize = CTFramesetterSuggestFrameSizeWithConstraints(
                                                                      framesetter,
                                                                      CFRangeMake(0, attrStr.length),
                                                                      nil,
                                                                      CGSizeMake(self.bounds.size.width, CGFLOAT_MAX),
                                                                      nil);
// リリース
CFRelease(framesetter);

しかしヒラギノフォントを使用した日本語とアルファベットが混在する場合、上記だけでは正確に取得することが出来ません。

以降の説明では、サイズ取得は上記を使用します。
viewのサイズをわかりやすくするため

self.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.4];

しています。

横幅が足りない!

テキストには @"あいab" を設定し、取得したサイズでviewのサイズを設定します。

self.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.4];
self.text = @"あいab";        
CGFloat fontSize = 50.f;
self.ctFont = CTFontCreateWithName(CFSTR("HiraKakuProN-W3"), fontSize, NULL);
////////////////
/* サイズを取得 */
////////////////
self.frame = CGRectMake(0.f, 0.f, contentSize.width, contentSize.height);
NSLog(@"size %@", NSStringFromCGSize(contentSize)); // size {155.615, 60}

abが描画されてない
ss

横幅が足りない為 ab が描画されていません。
注目は取得した横幅が 155.615 になっている点です。
実際に描画を行なっている- (void)drawRect:(CGRect)rect:に渡されるRectはピクセルサイズに丸められるのでrectには{{0, 0}, {155.5, 60}}が渡されることになり、描画に必要な横幅が0.115足りなくなります。そのため描画されない文字が発生します。
これは切り上げればいいのでfloatを切り上げる関数の float ceilf(float) を使用して解決です。
※ より厳密に行うなら0.5ポイント単位で切り上げれば良いかと思います。

self.frame = CGRectMake(0.f, 0.f, ceilf(contentSize.width), contentSize.height);

abも描画された!
ss

縦幅が足りない!

テキストには @"あいabgj" を設定し、取得したサイズ(横幅は切り上げて)でviewのサイズを設定します。

self.backgroundColor = [[UIColor blueColor] colorWithAlphaComponent:0.4];
self.text = @"あいab";        
CGFloat fontSize = 50.f;
self.ctFont = CTFontCreateWithName(CFSTR("HiraKakuProN-W3"), fontSize, NULL);
////////////////
/* サイズを取得 */
////////////////
self.frame = CGRectMake(0.f, 0.f, contentSize.width, contentSize.height);
NSLog(@"frame %@", NSStringFromCGRect(self.frame)); // frame {{0, 0}, {207, 50}}

gjが途切れている
ss

こんどは縦幅が足りません。
複数行ではこんな感じです。

3行目のgjが途切れている
ss

ここで注目は1行あたりの縦幅が足りないというわけではない点です。(1行あたりの縦幅が全部違う場合は、3行の場合全体でもっと足りなくなることになります)
何が足りないかというと最後の1行だけ Descent 分高さが足りなくなっています。(理由は不明。バグ?)
ですので、Descent分高さを足せば解決です。

設定しているCTFontのDescentを加え、横幅の場合と同じように切り上げをします。

self.frame = CGRectMake(
                    0.f, 
                    0.f, 
                    ceilf(contentSize.width), 
                    contentSize.height + ceilf(CTFontGetDescent(self.ctFont)));

gjが途切れてない!
ss

解説用色付けした図
ss
赤色 Asent
黄色 Descent
黒色 足りないDescent

まとめ

  • CTFramesetterSuggestFrameSizeWithConstraints()で取得したサイズは縦横共に小数点切り上げる
  • 高さには更に使用フォントのDecentを加える
  • CoreTextについてまとめたものはgithubにまとめてあります。今回のサイズを求めるのは14. sizeToFitです。

その他

CTFontCreateWithName(CFSTR("Helvetica"), fontSize, NULL); などヒラギノフォントを使用しなければ上記の問題は発生しません。
しかし、CoreTextの日本語行間の問題(解決)の問題があるので、明示的にヒラギノフォントでCTParagraphStyleSettingを設定する必要があります。その場合はサイズを取得するために上記の対応が必要です。

その他の解決方法で1行ずつサイズを求める方法があります。
ちゃんとは試してないのですが、以下でCTParagraphStyleSettingで設定したleadingを毎行加えれば求めることが出来ると思います。

http://stackoverflow.com/questions/2707710/core-texts-ctframesettersuggestframesizewithconstraints-returns-incorrect-siz

CoreTextSample.m
/* 以下は使用する場合はCTParagraphStyleSettingで設定したleadingを毎行足せば多分高さが求まる
    [注意] CTLineGetTypographicBounds() はCTParagraphStyleSettingが考慮されず値が返る */
+(CGFloat)heightForAttributedString:(NSAttributedString *)attrString forWidth:(CGFloat)inWidth
{
    CGFloat H = 0;

    // Create the framesetter with the attributed string.
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (__bridge CFMutableAttributedStringRef) attrString);

    CGRect box = CGRectMake(0,0, inWidth, CGFLOAT_MAX);

    CFIndex startIndex = 0;

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, box);

    // Create a frame for this column and draw it.
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(startIndex, 0), path, NULL);

    // Start the next frame at the first character not visible in this frame.
    //CFRange frameRange = CTFrameGetVisibleStringRange(frame);
    //startIndex += frameRange.length;

    CFArrayRef lineArray = CTFrameGetLines(frame);
    CFIndex j = 0, lineCount = CFArrayGetCount(lineArray);
    CGFloat h, ascent, descent, leading;
    NSLog(@"start line -> %ld", lineCount);
    for (j=0; j < lineCount; j++)
    {
        CTLineRef currentLine = (CTLineRef)CFArrayGetValueAtIndex(lineArray, j);
        CTLineGetTypographicBounds(currentLine, &ascent, &descent, &leading);
        h = ascent + descent;// + leading;
        NSLog(@"%f %f %f %f", ascent, descent, leading, h);
        H+=h;
    }

    CFRelease(frame);
    CFRelease(path);
    CFRelease(framesetter);

    return H;
}