2012-02-02

---- Updated 2014-10-26 ---- There is a difference between CTLineGetTypographicBounds() used below and CTLineGetImageBounds(). See this entry.

When using a CATextLayer, you cannot position text vertically within the layer, only horizontally. If you specify a height that is too large for the text, the text will be placed at the top of the frame. You cannot specify an arbitrary size and have the text be centered exactly within the box. To get that effect, you need to create a container layer, then position the text layer within it. Therefore, you need a quick and dirty way to get the dimensions of the desired text. Alas, CATextLayer has no method to determine its own desired size. I wrote some code that uses CoreText directly.

---- Updated 2012-03-02 ---- CTLineGetTypographicBounds() will return a size in fractional pixels. This produces really bad results when given to CATextLayer. Applying ceilf() to the size components significantly improves the text display. Also, force the width to be even. Otherwise the frame of the layer could be placed on a half-pixel boundary which would prevent any attempt to antialias the text. I tried to fix these problems using CGContextSetShouldSmoothFonts(), but it does not help when the frame is fractional. After I forced an integral layer frame, the text quality was perfect and CGContextSetShouldSmoothFonts() was unnecessary.

// Use this if you are already storing your data as attributed strings.
-(CGRect) dimensionsForAttributedString: (NSAttributedString *) asp {
	CGFloat ascent = 0, descent = 0, width = 0;
	CTLineRef line = CTLineCreateWithAttributedString( (CFAttributedStringRef) asp);
	width = CTLineGetTypographicBounds( line, &ascent, &descent, NULL );
	CFRelease(line);
	width = ceilf(width);			// Force width to integral.
	if (((int)width)%2) width += 1.0;	// Force width to even.
	return CGRectMake(0, -descent, width, ceilf(ascent+descent));	
}

/*	Obsolete version:

	-(CGRect) dimensionsForAttributedString: (NSAttributedString *) asp {
		CGFloat ascent = 0, descent = 0, width = 0;
		CTLineRef line = CTLineCreateWithAttributedString( (CFAttributedStringRef) asp);
		width = CTLineGetTypographicBounds( line, &ascent, &descent, NULL );
		CFRelease(line);
		return CGRectMake(0, -descent, width, ascent+descent);	
	}
*/

// Use this if you are using plain strings and you need to calculate a new size for a change you are about to make.
-(CGRect) dimensionsForString: (NSString *) s font: (NSString *) fontName size: (CGFloat) fontSize {
	NSFont *font = [NSFont fontWithName: fontName size: fontSize];
	NSDictionary *attribs = [NSDictionary dictionaryWithObject: font forKey: NSFontAttributeName];
	NSAttributedString *asp = [[NSAttributedString alloc] initWithString: s attributes: attribs];
	CGRect R = [self dimensionsForAttributedString: asp];
	[asp release];
	return R;
}

// Use this to calculate dimensions for a text layer that already exists.
-(CGRect) dimensionsForTextLayer: (CATextLayer *) layer {
	CFStringRef fontName = CTFontCopyPostScriptName(layer.font);
	return [self dimensionsForString: layer.string font: (NSString *) fontName size: layer.fontSize];
}


// Example:
	CATextLayer *p = [[CATextLayer alloc] init];
	p.string = @"Wiggly wabbit!";
	p.font = CTFontCreateWithName( (CFStringRef)@"Verdana", 0.0, NULL);
	p.fontSize = 18.0;
	CGRect TR = [self dimensionsForTextLayer: p];
	p.anchorPoint = CGPointMake(0, -TR.origin.y/TR.size.height);	// left side of baseline
	p.bounds = TR;
	p.position = CGPointMake( 150, 100 );
	[rootLayer addSublayer: p];
	[p release];

If you do not care about the baseline of the text, such as when you want a vertical centering, you could leave the anchor point at the default (0.5,0.5) and clear the returned CGRect's origin.y value.