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


## 前提知識

本投稿ではCoreTextの描画については記載してません。
基礎的な描画については[github](https://github.com/yusuga/CoreTextSample)の`0. 最小限の描画`などを参考にして下さい。

また別途、
[欧文書体の基礎知識](http://www.akibatec.net/wabunfont/study/basic/basic2.html)
[CoreTextの日本語行間の問題(解決)](http://novis.jimdo.com/2011/07/02/coretextの日本語行間の問題-解決/)

もお読み下さい。

## 本題

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

```objectivec: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のサイズを設定します。

```objectivec:
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](http://f.cl.ly/items/3W3j1B1P1J0l1D1F0H3E/スクリーンショット_2013_03_21_23_41.png)

横幅が足りない為 `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](http://f.cl.ly/items/2R0Y1j3G3k3o0r0n0z3p/スクリーンショット_2013_03_21_23_40.png)

## 縦幅が足りない！

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

```objectivec:
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](http://f.cl.ly/items/1P0y3X3x0B0H0H0S1W1F/スクリーンショット_2013_03_21_23_44.png)

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

***3行目のgjが途切れている***
![ss](http://f.cl.ly/items/120G1P3u3r0C0J0N1503/スクリーンショット_2013_03_21_23_48.png)

ここで注目は**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](http://f.cl.ly/items/0p1S0P1z2v380q1O0F3L/スクリーンショット_2013_03_21_23_56.png)

***解説用色付けした図***
![ss](http://f.cl.ly/items/1v3C0u2u3k0E2W084319/スクリーンショット_2013_03_22_0_00.png)
赤色 Asent
黄色 Descent
黒色 足りないDescent

## まとめ

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

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

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

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

```objectivec: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;
}
```