Chaco's Free Type engine

by Eric Jones eric@enthought.com

This document covers the simple FreeType2 font rendering interface included in Chaco.

Downloads:

source package: freetype-0.1.0.tar.gz
windows binaries (Python 2.2.1): freetype-0.1.0.win32-py2.2.exe

Document Contents

Introduction
Installation
Standard Usage
Increasing Font Rendering Resolution
Rendering aliased text
Selecting different fonts
More on selecting fonts and encodings
Accessing the current font object
Rotating_and_transforming_text
Rendering unicode strings
Existing Issues

Introduction

Chaco includes a simple Python interface to FreeType 2 that draws text into an image (Numeric array). From there, the text image can be transferred to the screen on a number of GUI backends (wxPython, OpenGL, etc.). Freetype is an open source font rendering engine that handles multiple font formats (truetype, opentype, type1, and others) and renders both aliased (i.e. standard monotone text) as well as anti-aliased text. Freetype it praised as a high quality text, and is the de- facto standard rendering engine on Linux systems (its used in KDE and elsewhere). It also can draw arbitrarily transformed text which is why we want to use it instead of standard system calls (which often only handle rotated text) for Chaco.

Robert Kern has written a more complete Python interface to Freetype 1 called PyFT. Robert and I discussed updating PyFT to Freetype 2 and fixing some issues with memory management, etc. Together, we decided that a less ambitious interface that served the needs of Chaco was a better solution. The result is a small package (about 250 lines for the interface and 800 lines of py/C in the weave wrapper) for querying the available fonts on a system and rendering arbitrarily transformed text (both ascii and unicode) into a Numeric array. The rendering mode can be set to aliased or anti-aliased.

The freetype wrapper is considered alpha. I have integrated it into the wxPython backend of Chaco, and it does work. But the test are fairly limited. Still, since the interface is small, the major issues (if any) should be found quickly.

Availability and Installation

The freetype package is part of Chaco. Chaco is currently only accessible through CVS. However, I've also bundled freetype up so that people can try it out separately. When Chaco is released, the freetype package will install as its own package (not nested within Chaco) so code using this version will continue to work with the final distribution (barring interface changes).

Downloads:

source package: freetype-0.1.0.tar.gz
windows binaries (Python 2.2.1): freetype-0.1.0.win32-py2.2.exe

The source package contains a setup.py file, and only depends on Numeric for standard usage. The dependencies on weave have been removed in the distribution to simplify builds. If a recent CVS version of scipy.weave is present, the _ft.cpp will be rebuilt. Also, if you want to use the view() method for Glyphs objects (see the samples below), you'll also need wxPython and SciPy. If you want to build from the CVS, you'll need the CVS version of scipy.weave. It has support for unicode objects that older versions don't have. To build from source, do the following (on Unix):

    > tar -xzvf freetype-0.1.0.tar.gz
    > cd freetype-0.1.0
    > python setup.py build
    > python setup.py install    
    
On windows, you can build from source using either the mingw32 compiler or MSVC++. Pretty much the same instructions apply, except you may want to use winzip for the unpacking process if you don't have tar installed on your machine. The easiest approach on windows is to use the binaries supplied above.

Standard Usage

Interaction with the freetype engine generally occurs through the FreeType class.
    # only needed if you'd like to view the rendered text in a plot window
    >>> import gui_thread
    
    >>> from scipy import plt
    
    >>> import freetype
    >>> ft = freetype.FreeType()
    >>> rendered_text = ft.render("please")
    >>> rendered_text.view()
    

The rendered_text object is a Glyphs instance that holds the image array for the rendered text as well as some other information about the size of the image, etc. The image is stored as an unsigned character array (type UnsignedInt8) of gray scale values.

The results here don't look so good because some of the characters run together, and the resolution is somewhat low. Increasing the rendering resolution can improve the results (although I thought this should be better than it is.)

Increasing Font Rendering Resolution

The FreeType initializer has a dpi keyword argument that set the "dots per inch" of the display device you want to render for. This affects the size and resolution of fonts for a given point size (10, 12, etc.). 72 dpi is the standard and default. I've noticed though, that this gives pretty lousy font rendering for font sizes below 14 points. I use 133.3 on my laptop because it has a high resolution screen, and this looks fairly good with the text rendered at the correct size. However, on low resolution screens the letters will appear too large.
    >>> ft = freetype.FreeType(dpi=133.3)
    >>> rendered_text = ft.render("please")
    >>> rendered_text.view()
    

Note: I'd really like a to know how to query the system for its dpi value. If you know how to do this on a particular system, please email me.

Rendering aliased text.

By default, FreeType renders antialiased text. The antialias() method will disable this if desired.
    >>> ft = freetype.FreeType(dpi=133.3)
    >>> rendered_text = ft.antialias(0)
    >>> rendered_text = ft.render("please")
    >>> rendered_text.view()
    

Selecting different fonts

FreeType can handle a lot of different font formats, but we currently only support scalable fonts (this limitation will probably continue) and have only tested with TrueType on Windows. freetype reads the available fonts from a user specified directory (I'd like this to be automatically found) and keeps track of the available fonts. This is managed by a FontLookup class, but most of the information is also exposed through the FreeType class.

    >>> ft = freetype.FreeType(dpi=133.3)
    >>> ft.available_fonts()
    ['arial', 'arial black', 'arial narrow', 'arial unicode ms', 'batang', 
    'book ant iqua', 'bookman old style', 'century', 'century gothic', 
    'comic sans ms', 'courier new', 'estrangelo edessa', 
    'franklin gothic medium', 'garamond', 'gautami', 'georgia', 
    'haettenschweiler', 'impact', 'latha', 'lucida console', 
    'lucida sans unicode', 'mangal', 'marlett', 'microsoft sans serif', 
    'monotype corsiva', 'ms mincho', 'ms outlook', 'mv boli', 
    'opensymbol', 'palatino linotype', 'pmingliu', 'raavi', 'shruti', 
    'simsun', 'sylfaen', 'symbol', 'tahoma', 'times new roman', '
    trebuchet ms', 'tunga', 'verdana', 'webdings', 'wingdings', 
    'wingdings 2', 'wingdings 3']    
    >>> ft.select_font("times new roman",size=24)
    >>> rendered_text = ft.render("please")
    >>> rendered_text.view()
    

Note: Font management is one of the areas I'd like to see some improvement. We need some simple name matching to handle "times" instead of "times new roman", etc. Also, I have no idea where the font files are stored on Unix systems or even if there is a common location for these guys. It would be very good to auto detect the location of font files instead of having a hard coded path. On windows systems, it'll probably be pretty easy because there are only about 3 places to look (I think). On Unix, it could be a lot more difficult.

Another thought is to also include 4 or 5 standard fonts with the freetype package so that there are always several available fonts. I'm not entirely happy without how the Microsoft fonts are rendering, so there might be some aesthetic benefits also if there are some "freetype optimized" fonts out there that are open sourced.

More on selecting fonts and encodings

The select_font() method has the following full definition:
def select_font(self,name, size=12, style='regular', encoding=None)
The size keyword specifies the size of the font in points. Note that the actual size of the font also relies on the font itself and the dpi setting used to initialize the FreeType object. style is one of 4 strings: 'regular', 'bold', 'italic', or 'bold italic'. Currently, bold and italic settings are only available for fonts that have a specialized font to handle the styles. Adding a style transformation matrix to turn fonts that only have a regular style font into styled fonts shouldn't be hard, but hasn't been attempted yet. Note: Does anyone have a set of transformation matrices that does a good job of creating each of these styles?

The encoding string specifies the character map that maps characters to the glyph that displays them. By default, unicode ('unic') is used for all the fonts that support that style. If unicode is supported, then symbol ('symb') is attempted. Another common style is Apple Roman('armn'). You can ask a font object what encodings it supports:

    >>> ft = freetype.FreeType(dpi=133.3)
    >>> ft.select_font("times new roman",size=24)
    >>> the_font.supported_encodings()
    ['unic', 'armn', 'unic']
    
Note: I'm not sure why 'unic' is listed twice.

If you try to set the encoding to an unsupported value, it'll raise a ValueError. Here is the list of potentially supported encodings:

symbol "symb"
unicode "unic"
latin1 "lat1"
latin2 "lat2"
SJIS "sjis"
gb2312 "gb"
big5 "big5"
wansung "wans"
johab "joha"
adobe standard "ADOB"
adobe expert "ADBE"
adobe custom "ADBC"
apple roman "armn"

I haven't used encodings much yet, so I don't have any practical examples to share. So far, accepting the default encoding has worked well.

Note: I haven't experimented with how the various encodings interact with the fact that all strings passed into the freetype renderer are unicode. I don't think it is an issue, but it might have some affect.

Accessing the current font object

The current font object is accessible through the font() method. It allows you to set and retrieve the font of a FreeType object. It will only allow font objects created by the same FreeType object to be assigned to the object. If font is called without an argument, it returns a reference to the current font object. The Font class has the following methods:
    class Font:
        def name(self):
        def style(self):
        def supported_encodings(self):
        def glyph_count(self):
        def size(self,sz=None):
        def encoding(self,encoding=None):
        def char_index_from_int(self,value):
    
For more info, you'll have to look at the source.

Rotating and transforming text

The transform() method sets a 4 element transformation matrix used during rendering. The [a,b,c,d] elements are positioned as follows:

    |a b|
    |c d|
    

transform() also returns the current transform for the FreeType object. You can call it without any arguments if you just want to query for the transform value.

Here is an example where the text is rotated 45 degrees using the affine matrix tools in Chaco.

    >>> from scipy import *
    >>> import affine
    >>> m = affine.affine_identity()
    >>> m = affine.rotate(m,pi/4)
    >>> a,b,c,d,tx,ty = affine.affine_params(m)
    
    >>> ft = freetype.FreeType(dpi=133.3)
    >>> ft.transform([a,b,c,d])
    array([ 0.70710678,  0.70710678, -0.70710678,  0.70710678])
    >>> ft.select_font("times new roman",size=24)
    >>> rendered_text = ft.render("please")
    >>> rendered_text.view()
    

Rendering unicode strings

All strings are actually converted to unicode before being sent to the freetype rendering engine. For fonts that support unicode characters, you can render until your hearts content. Here is one that scientist are interested in:
    >>> ft = freetype.FreeType(dpi=133.3)
    >>> ft.select_font("arial unicode ms",size=24)
    >>> rendered_text = ft.render(u'Angstrom \xc5')
    >>> rendered_text.view()
    

And here is a kanji string:

    >>> ft = freetype.FreeType(dpi=133.3)
    >>> ft.select_font("arial unicode ms",size=24)
    >>> rendered_text = ft.render(u'\u4ee0\u4ee1\u4ee2\u4ee3\u4ee4')
    >>> rendered_text.view()
    

Note: Someone with a more intelligent kanji string, please send it to me. I just picked 4 sequential characters at random from the unicode table.

Existing Issues

These are generally requests for help on issues that I've run into. If you have knowledge about these, please email the scipy-dev list and we can have a discussion there about it. Thanks.

Font rendering quality

I haven't been as happy with the output of FreeType as I expected to be. At 72 dpi and 12 point fonts, the letters in the fonts run together. At higher resolutions, the aliased rendering seems jagged compared to letting the system render the text. Most people have sung high praises for FreeType's output, so I'm betting that I don't have something set up correctly, or I should use different fonts. My only tests have come with the standard Microsoft TrueType fonts. I am using kerning, but have noticed that most character pairs don't have kerning information for the fonts I've tried. The "AV" character pair is a notable exception. Antialiased fonts generally look fine at higher resolutions. Chaco doesn't support alpha blending yet, though, so there isn't any way to display them on a background at the moment.

System font management

On windows, finding the system fonts isn't going to be that hard. On Unix, it may also be easy, but I don't know. Also, currently I've only tested TrueType fonts. Type1, etc. may (should) work also. I do know that non-scalable fonts will not work. The font system management needs a little work so that it is easier to choose some default fonts like "times" and others. It should have a simple mapping between several standard names and the closest font available that has that style. I've also thought that carrying a few fonts around within FreeType might be a good idea. If anyone has knowledge of a set of fonts that work well with FreeType and have a BSD compatible license, please let me know.

Speed

The create_glyph_list method doesn't do any caching at all. In simple tests, it looked like creating this list took the same amount of time as rendering the glyphs. FreeType has a caching API that might cut this time down to imperceptible. I doubt adding support for it is that hard, but it isn't a high priority of mine. I'd be happy if someone that knew the caching API took up the cause though.

Rendering glyphs also takes a noticeable amount of time. Building freetype with optimization flags sped it up by 33%, but it is still quite a bit more expensive than having the system draw the fonts. I don't know if this will be noticeable in the end.

The conversion from an array into a wxPython bitmap turned out to be a real mess and is probably really slow. A weave method -- and probably a bug fix to wxPython's bitmap masks rendering-- will improve this markedly.

The bug I'm talking about in the wxPython masks appears because Chaco flips the direction of positive y in the device context. As a result, bitmaps are rendered upside down. However, the mask is not! It is rendered right side up. I don't know if this is a wxPython issue or is a problem in the underlying windows API. Anyway, the current fix is to set the mask separately to the correct orientation which is time consuming. This will also come up in image rendering.

Antialiased text

We have to have alpha blending capabilities in Chaco before this becomes a reality. Alpha blending isn't that hard to write, but it may be slow. The Windows API has an AlphaBlend method which we can weavify for use in wxPython. If GTK has a similar API method, then wxPython can be made to work with alpha blending. OpenGL I'm sure can do it pretty easily. TkInter... I don't know. Anyway, alpha blending isn't the highest priority, so for now I'm sticking with aliased rendering.