Mistakes Were Made: Integral Bounds

Here’s another mistake from the day job. (Why do they pay us? Because we do eventually find and correct our errors?)

“Misaligned” CATextLayers

As you may know, the Core Animation instrument has a flag to “Color Misaligned Images“. This is somewhat poorly named; in fact, it will color misaligned layers in magenta, whether or not they contain images. (It will also color layers containing stretched images in yellow, even if they are aligned correctly.) This is useful for two reasons. First, drawing misaligned layers is a performance hit. The GPU has to do blending to antialias the fractional pixels on the misaligned edge; blending tends to be very expensive on iOS devices. Second, because everything is shifted a fractional pixel and antialiased, it will all look a little bit blurry — problematic when you want crisp, clear images.

But there’s a third issue I just discovered. When using CALayer subclasses that draw content (at least CATextLayer, and possibly others), the actual created content can be wrong! Not just the appearance onscreen, but the bitmap backing the layer! This is particularly pernicious because the position does not even have to be misaligned; it’s enough simply to make the height non-integral. Observe:

See a difference? Well, the first line looks a little less crisp. But there’s more to it than that. Take a look at the top of the capital letters, particularly the curved ones.

It’s not just overly antialiased, it’s actually missing pixels! Now, how do I know that the actual content is wrong, not just the display? There are a couple of options. Since the backing store is an opaque type, I can’t just write it out to file and hope to get a usable image (although I can get a clue from the pixel dimensions — they’re rounded down to the next integral pixel). But I can have the layer render itself in an image context I create, and write that out. More amusingly, I can take advantage of Core Animation’s OpenGL underpinnings and use the contentsRect property, noticing that “If pixels outside the unit rectangles are requested, the edge pixels of the contents image will be extended outwards.” And indeed, I get something fun:

This makes it clear that the top row of pixels from the correct image has been cut off. The extended row is what should be the second row of pixels.

What and Why?

The hint I got from examining the contents seems to tell the story. If the size of the backing store is smaller than the size of the bounds, that fractional pixel simply won’t be drawn. The solution for you is to make sure your bounds are integral. The solution for Apple? That’s a tougher question; there are a lot of options with different tradeoffs. I’m not even sure what they’ve chosen is the wrong one, although it’s unexpected behavior and should be documented. Other options include rounding the dimensions up for the backing store, and scaling the content image back down, or continuing to scale the dimensions down but then rendering scaled and at an offset.

Post Script: Code

This can go in a simple view controller-based app. You’ll need to add and include the QuartzCore and CoreGraphics frameworks.

- (void)viewWillAppear:(BOOL)animated
{
#ifdef RENDER_IMAGE
	UIGraphicsBeginImageContext(CGSizeMake(200, 370));
	[[UIColor whiteColor] set];
	UIRectFill(CGRectMake(0, 0, 200, 370));
#endif
    [super viewWillAppear:animated];
	CATextLayer *textLayer = [CATextLayer layer];
	textLayer.position = CGPointMake(120, 220);
	textLayer.bounds = CGRectMake(0, 0, 200, 50.394);
	textLayer.fontSize = 14.f;
	textLayer.foregroundColor = [UIColor blackColor].CGColor;
	textLayer.string = @"Sample Sentence With Curves";
#ifdef CONTENTS_RECT
	textLayer.contentsRect = CGRectMake(0.f, -.1f, 1.f, 1.2f);
#endif
	[self.view.layer addSublayer:textLayer];
	
#ifdef RENDER_IMAGE
	CGContextTranslateCTM(UIGraphicsGetCurrentContext(), 5, 50);
	[textLayer renderInContext:UIGraphicsGetCurrentContext()];
#endif
	
	textLayer = [CATextLayer layer];
	textLayer.position = CGPointMake(120, 270);
	textLayer.bounds = CGRectIntegral(CGRectMake(0, 0, 200, 50.394));
	textLayer.fontSize = 14.f;
	textLayer.foregroundColor = [UIColor blackColor].CGColor;
	textLayer.string = @"Sample Sentence With Curves";
#ifdef CONTENTS_RECT
	textLayer.contentsRect = CGRectMake(0.f, -.1f, 1.f, 1.2f);
#endif
	[self.view.layer addSublayer:textLayer];
	
#ifdef RENDER_IMAGE
	CGContextTranslateCTM(UIGraphicsGetCurrentContext(), 0, 50);
	[textLayer renderInContext:UIGraphicsGetCurrentContext()];
	UIImage *render = UIGraphicsGetImageFromCurrentImageContext();
	UIGraphicsEndImageContext();
	[UIImagePNGRepresentation(render) writeToFile:@"/path/to/render.png" atomically:NO];
#endif
}

About Joel Kin

Developing on Apple platforms for, holy shit, like twenty years now. Find me on linkedin and twitter. My personal website is joelk.in.
This entry was posted in Code and tagged , , , , , , , , . Bookmark the permalink.