Drawing single-line-height text without abandoning CTFramesetterRef and CTFrameRef
Core Text is lightweight and all awesome. But we have this pesky little problem with multilingual layouts — different fonts are created by different vendors and typographers, and they often mismatch in terms of metrics. Some are taller, some shorter; some have huge ascender and descenders, some relatively tiny and tame. Things are quite manageable with just one language, but it’s not the case for me.
We need layouts with even baselines, which runs orthogonal to how the CTFramesetter works. The frame setter calculates typographic bounds, adds up ascender, descender and leading, then use the whole sum as the height of the line. This is straightforward as in it works like movable type, but also painful as it can wreak havoc with baseline alignment.
No, you don’t have to roll your own CTFramesetter implementation. That’s too much pain. Re-implementing something when you can hack and use all the free stuff? No way, that’ll take a whole week.
Creating a well-hacked attributed string
To ensure that your Core Text label draws even line heights, make sure the attributed string you feed into your label is well-hacked:
- (NSAttributedString *) attributedStringForString:(NSString *)aString font:(UIFont *)aFont color:(UIColor *)aColor {
if (!aString)
return nil;
float_t lineHeight = aFont.leading;
id fontAttr = [NSMakeCollectable(CTFontCreateWithName((CFStringRef)aFont.fontName, aFont.pointSize, NULL)) autorelease];
id foregroundColorAttr = (id)(aColor ? aColor.CGColor : [UIColor blackColor].CGColor);
id paragraphStyleAttr = ((^ {
CTParagraphStyleSetting paragraphStyles[] = (CTParagraphStyleSetting[]){
(CTParagraphStyleSetting){ kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(float_t), (float_t[]){ 0.01f } },
(CTParagraphStyleSetting){ kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(float_t), (float_t[]){ lineHeight } },
(CTParagraphStyleSetting){ kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(float_t), (float_t[]){ lineHeight } },
(CTParagraphStyleSetting){ kCTParagraphStyleSpecifierLineSpacing, sizeof(float_t), (float_t[]){ 0.0f } },
(CTParagraphStyleSetting){ kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(float_t), (float_t[]){ 0.0f } },
(CTParagraphStyleSetting){ kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(float_t), (float_t[]){ 0.0f } }
};
CTParagraphStyleRef paragraphStyleRef = CTParagraphStyleCreate(paragraphStyles, sizeof(paragraphStyles) / sizeof(CTParagraphStyleSetting));
return [NSMakeCollectable(paragraphStyleRef) autorelease];
})());
NSAttributedString *returnedString = [[[NSAttributedString alloc] initWithString:aString attributes:[NSDictionary dictionaryWithObjectsAndKeys:
fontAttr, kCTFontAttributeName,
foregroundColorAttr, kCTForegroundColorAttributeName,
paragraphStyleAttr, kCTParagraphStyleAttributeName,
[NSNumber numberWithInt:kCTUnderlineStyleSingle], kCTUnderlineStyleAttributeName,
nil]] autorelease];
return returnedString;
}
Think of the many 0.0f as CSS Reset.
Drawing the attributed string with even baselines
If you’re creating an UILabel subclass, this would probably work. Without drawing adjustments, fonts with slightly different ascender and descender values will twiddle around. Which makes an unholy, unsightly mess.
Note that ctFrame is a private property and irCTFrameEnumerateLines is a simple convenience wrapping around functions that operate on a CTFrameRef:
- (void) drawTextInRect:(CGRect)rect {
if (![self isShowingRichText]) {
[super drawTextInRect:rect];
return;
}
CTFrameRef usedFrame = self.ctFrame;
if (!usedFrame)
return;
CFRetain(usedFrame);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextConcatCTM(context, CGAffineTransformMake(
1, 0, 0, -1, 0, CGRectGetHeight(self.bounds)
));
__block CGFloat usableHeight = CGRectGetHeight(self.bounds);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
irCTFrameEnumerateLines(usedFrame, ^(CTLineRef aLine, CGPoint lineOrigin, BOOL *stop) {
usableHeight -= self.font.leading;
CGContextSetTextPosition(context, lineOrigin.x, usableHeight - self.font.descender);
CTLineDraw(aLine, context);
});
CFRelease(usedFrame);
}