> blog

雑多に書いていくブログ

CoreTextで文字列の矩形領域を取得する

はじめに

iOSで表示する文字列の矩形領域を取得する必要に駆られたのですが,
CoreTextを使って実現することができたのでメモ.

方法

CTLineRefから取得できるCTRunRefは一文字分の情報を持っているため,
それを利用して全文字列の矩形領域を一文字一文字取得するという処理を行っています.

NSString *content; // 表示したい文字列
CGRect bounds; // 文字列を表示する矩形領域
    
NSMutableDictionary *attr = [[NSMutableDictionary alloc] init];
CTFontRef ctFont = CTFontCreateWithName((CFStringRef)@"Helvetica", 13, nil);
[attr setObject:(__bridge id)ctFont forKey:(NSString *)kCTFontAttributeName];
[attr setObject:[UIColor blackColor] forKey:(NSString *)kCTForegroundColorAttributeName];
    
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:content attributes:attr];
CTFramesetterRef frs = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)(attrString));

CGPath path = CGPathCreateMutable();
CGPathAddRect(path, NULL, bounds);
CTFramesetter ctFrame = CTFramesetterCreateFrame(frs, CFRangeMake(0, attrString.length), path, NULL);
 
CFRelease(frs);

CGContextRef ctx = UIGraphicsGetCurrentContext();

CGAffineTransform transform = CGAffineTransformMake(1, 0, 0, -1, 0, CGRectGetHeight(bounds));
CGContextConcatCTM(ctx, transform);
    
CFArrayRef lines = CTFrameGetLines(ctFrame);
    
CGPoint *origins = malloc(sizeof(CGPoint) * CFArrayGetCount(lines));
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, CFArrayGetCount(lines)), origins);
        
for (int i = 0; i < CFArrayGetCount(lines); ++i) {
        
    CGContextSaveGState(ctx);
    CTLineRef line = CFArrayGetValueAtIndex(lines, i);

    CGPoint origin = *(origins + i);
    CGContextSetTextPosition(ctx, origin.x ,origin.y);
    CGPoint textPosition = origin;

    CFArrayRef runs = CTLineGetGlyphRuns(line);
    CFIndex runCount = CFArrayGetCount(runs);

    CGFloat offsetX = origin.x;

    for (CFIndex runIndex = 0; runIndex < runCount; runIndex++) {

        CTRunRef run = CFArrayGetValueAtIndex(runs, runIndex);
        CFRange runRange = CTRunGetStringRange(run);

        NSString *substring = [content substringWithRange:NSMakeRange(runRange.location, runRange.length)];

        for (CFIndex glyphIndex = 0; glyphIndex < CTRunGetGlyphCount(run); glyphIndex++) {

            CFRange glyphRange = CFRangeMake(glyphIndex, 1);
            CTRunDraw(run, ctx, CFRangeMake(glyphIndex, 1)); // 文字の出力は一文字づつ...

            // Center this glyph by moving left by half its width.
            CGFloat glyphWidth = CTRunGetTypographicBounds(run, glyphRange, NULL, NULL, NULL);
            CGFloat halfGlyphWidth = glyphWidth / 2.0;
            CGPoint positionForThisGlyph = CGPointMake(textPosition.x - halfGlyphWidth, textPosition.y);

            textPosition.x -= glyphWidth;

            CGFloat ascent, descent;
            CTRunGetTypographicBounds(run, glyphRange, &ascent, &descent, NULL);

            // The glyph is centered around the y-axis
            CGRect lineMetrics = CGRectMake(offsetX, positionForThisGlyph.y - descent, glyphWidth, ascent + descent);
            offsetX += glyphWidth;

            unichar c = [substring characterAtIndex:glyphIndex];

            // ここで取得できるcharacterFrameが一文字分の矩形領域
            CGRect characterFrame = CGRectApplyAffineTransform(lineMetrics, transform);
        }
    }
    CFRelease(line);
}

CGContextRestoreGState(ctx);
free(origins);

終わりに

文字列の位置を一つ一つ取得することで色んな面白い機能が作ることができて,
例えばだいぶ前にProcessingで作ったようなこんなの

文字が指の位置を中心にして周りに広がるプロトタイプ - YouTube
ができるようになる.

前に書いた記事ではJavaScriptでの実現方法を紹介しましたが,
今回はiOSで同じ機能を実現する方法を紹介してみました!