Rendering text on Windows via Uniscribe
Tuesday, April 7, 2009
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: