Tuesday, April 07, 2009

Rendering text on Windows via Uniscribe

My original goal was to use Pango to render Unicode text in the Factor UI on Windows. This didn't pan out, for a number of reasons:

  • Pango, Cairo and all of their dependencies add up to around 7Mb of DLLs that we'd need to ship with every Factor binary, as well as every standalone app binary deployed from Factor. That's too much when a 'Hello world' compiles down to a 500kb image.
  • Pango has various bugs on 64-bit Windows.
  • Pango doesn't play well with Microsoft's ClearType, and anti-aliased text is clipped for some reason.

I'm sure these problems will get fixed eventually (except for the first one perhaps) and Pango is open source so I could always send a patch, but I'd rather spend my time working on interesting things instead, so I've decided to bypass Pango on Windows altogether and use Microsoft's native Uniscribe API instead. Uniscribe ships as a standard part of Windows XP (actually it's been around since IE 5.0) and so Factor can depend on it being installed. On X11, I continue to use Pango; most *nix systems have at least part of the GNOME platform installed, so Pango and Cairo will be there, and I haven't seen any rendering issues in Pango with X11 either.

I'm using the Uniscribe script string API, which is intended to be used for laying out and rendering a piece of text with a single font and color. It is directly analogous to CTLine in Core Text and PangoLayout in Pango.

The function to create a script string, ScriptStringAnalyze, takes a device context handle as a parameter. The device context must be provided if the string is going to be rendered or measured.

Before creating the string, the font and text color are set in the device content. To set the font, I look up a font handle with CreateFont, and pass it to the SelectObject() function. There's an important caveat with CreateFont(); if you want your font size to be specified in points (rather than pixels), you must pass a negative size. I noticed that 12-point font was rendered way too small; changing the 12 to a -12 fixed the problem, so now the Factor UI does this for you on Windows.

The text color is set by calling SetTextColor, and the background color (which is only rendered if a flag is passed to ScriptStringOut(); see below) with SetBkColor.

For both fonts and colors, the Uniscribe text rendering uses Factor's cross-platform font and color types.

Size measurement is done by calling ScriptString_pSize(). Unlike Core Text and Pango, Windows Uniscribe makes no distinction between metric and ink bounds. There is a provision for oversize glyphs, such as those in the Zapfino font (see my blog post on ink and metric bounds), where each glyph has an associated ABC metrics structure. I'll implement support for this later.

When rendering to an offscreen context that is intended to be used as an OpenGL texture, as in the Factor UI, a bit of a chicken-and-egg problem occurs, because we need to know the size of the resulting text before we can create a bitmap. Fortunately, Windows GDI separates the process of creating a memory (off-screen) DC and allocating the bitmap storage for it, into two functions, CreateCompatibleDC() and CreateDIBSection(). The trick is to create a DC, create a script string, get its size, then allocate the bitmap for the DC, and finally render the string.

Script strings can be rendered to their underlying DC with the ScriptStringOut function. This function takes a number of parameters. For instance, it can render the text selection for you.

Font metrics -- the ascent, descent, and leading -- can be obtained by calling GetTextMetrics() on the DC.

Once the text has been rendered into a memory DC, the underlying bitmap needs to be obtained and the graphics object handles freed. This is the third time I implement a similar-looking make-bitmap-image combinator, so by now I've developed some utilities that I've abstracted out into the images.memory vocabulary. The bitmap is cached as a texture in the same way on all platforms; I've discussed OpenGL texture caching before.

Converting between x co-ordinates and line offsets, and vice versa, can be done with ScriptStringXToCP() and ScriptStringCpToX(). Watch out for the fact that passing the length of the string to ScriptStringCpToX() is invalid; if you want the X-offset of the trailing edge of the last code point, you have to pass length-1 instead, with the fTrailing parameter TRUE.

Finally, script strings are freed by calling ScriptStringFree(). When binding to the API from another language, watch out for the type of the input parameter; it's a pointer to a SCRIPT_STRING_ANALYSIS type, which is itself a pointer type. So you're passing a pointer to a pointer to a struct! I got the type wrong in my FFI declaration the first time around and was getting segfaults when deallocating stuff. Very annoying.

The code implementing this is split between four vocabularies:
  • windows.offscreen - support code for creating GDI memory DCs. Originally written by Joe Groff for another purpose; I've generalized it and put it in a common location so that it can be used by both the Uniscribe code and offscreen gadget rendering.
  • windows.fonts - looking up GDI font handles from Factor font descriptions
  • windows.uniscribe - the bulk of the code; creating, rendering script strings, converting x co-ordinates to line offsets and vice versa
  • ui.text.uniscribe - a backend for the UI's cross-platform text rendering support. Just a thin shim over the preceding vocabularies.

Here is what it looks like when all is said and done:

1 comment:

empt said...

Hi, I've just download latest 4/16 win32 version and find I can now programming in Chinese. Just want to say thanks.