Friday, February 20, 2009

Metric bounds versus image bounds

With some fonts, the font glyphs exceed the font metric bounds. I wasn't aware of this fact until recently, and because of this some text would be chopped off at the edges.

The right way to do things is to compute the image bounds of the text and use that for creating the OpenGL texture, only using metric bounds to actually position the text.

To compute image bounds with Core Text, you're meant to use CTLineGetImageBounds(), but this function takes a CTLine as well as a CGContext, which creates a bit of a chicken and egg problem, because I need the image bounds to create the context. The solution I came up with is to keep a dummy 1x1 pixel bitmap context around globally and use that for text measurement.

Getting the text position right was a bit tricky; I must thank Joe Groff for helping me get this right.

Now, CTLineGetImageBounds() returns a rectangle. The origin of the rectangle is the point at the intersection of the baseline and the caret, and the dimension is the size of the glyphs. So the texture size should be the size of the rectangle (with appropriate rounding to ensure your texture has integral dimension).

Inside the texture, the text position should be set to the negation of the image bounds origin, by calling CGContextSetTextPosition. The text position is relative to the CGContext's text co-ordinate system, which has the origin at the bottom-left corner by default.

Now, once you've rendered the texture, you need to compute the texture position. Factor's UI uses a co-ordinate system where the origin is the top-left corner. Suppose you want to diplay the text such that the baseline is at (0,ascent) where ascent is the font ascent, and the image bounds are im.{x,y,w,h}. Then the texture quad should be positioned at (im.x,ascent - im.h - im.y).

I glossed over the details of rounding the texture dimensions and texture position to integral co-ordinates; this is important and a bit tricky to get right, but you can look at the code after I merge it in.

After some pixel tweaking, everything works now. In the below screenshot, I've created an editor gadget that displays text with the Zapfino font, and set a one-pixel black border around it. Also note that the text selection and caret positioning doesn't mess up ligatures.

2 comments:

Wolf550e said...

Is this portable to win32/GTK/etc? Sounds like it's coupled to OS X APIs.

Slava Pestov said...

The UI has a cross-platform abstraction layer so that Factor UI apps can render text without getting into platform specifics. So far I have Core Text and Freetype backends, but I'm going to replace the Freetype backend with Pango, which will give us multilingual text rendering on non-Mac platforms.