Articles

Django and Cairo: Rendering Pretty Titles

Well, perhaps this isn't the sole reason, but nonetheless it's a more than feasible idea these days to have headings, titles and short lines of text using fonts a user doesn't have installed on their system. And, until the spec for embedding fonts is finialised in around 2065, there are two main options:

So, we need header images. One horribly labour-intensive way of doing this is making them manually in Generic Graphics Editor 8.6. However, since we're sensible people, we'll generate them on the fly. And, since we're sensible people, we'll be using Django*, so we need to write some nice Python code.

I'll be using Cairo to generate graphics, in part because it's a nice library, and is pretty common these days. You'll need the Python Cairo bindings; on debian-like systems, this is the package python-cairo; in other places, your mileage may vary.

The key to making Cairo work with Django is wrapping a Cairo canvas in a django view. For this reason, I have this function lying around:

def render_image(drawer, width, height):
import os, tempfile, cairo
# We render to a temporary file, since Cairo can't stream nicely
filename = tempfile.mkstemp()[1]
# We render to a generic Image, being careful not to use colour hinting
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(width), int(height))
font_options = surface.get_font_options()
font_options.set_antialias(cairo.ANTIALIAS_GRAY)
context = cairo.Context(surface)
# Call our drawing function on that context, now.
drawer(context)
# Write the PNG data to our tempfile
surface.write_to_png(filename)
surface.finish()
# Now stream that file's content back to the client
fo = open(filename)
data = fo.read()
fo.close()
os.unlink(filename)
return HttpResponse(data, mimetype="image/png")

The idea is, you pass it a function which will draw the image onto a context, and the image's width and height, and it takes care of all the boring tedium of wrapping cairo and django together.

Now, that's not very useful by itself, is it? Time to draw some text!

Firstly, as an aside, we need a way of seeing how big a certain text string will be for a given font and size, so we can render an image just big enough for it. This function achieves that:

def text_bounds(text, size, font="Sans", weight=cairo.FONT_WEIGHT_NORMAL, style=cairo.FONT_SLANT_NORMAL):
import cairo
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)
context = cairo.Context(surface)
context.select_font_face(font, style, weight)
context.set_font_size(size)
width, height = context.text_extents(text)[2:4]
return width, height

Yes, yes, it's somewhat cryptic, but it does the job. Now, we can write a text-rendering view!


def render_title(request, text, size=60):
# Get some variables pinned down
size = int(size) * 3
font = "Meta"
width, height = text_bounds(text, size, font)
def draw(cr):
import cairo
# Paint the background white. Replace with 1,1,1,0 for transparent PNGs.
cr.set_source_rgba(1,1,1,1)
cr.paint()
# Some black text
cr.set_source_rgba(0,0,0,1)
cr.select_font_face(font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(size)
# We need to adjust by the text's offsets to center it.
x_bearing, y_bearing, width, height = cr.text_extents(text)[:4]
cr.move_to(-x_bearing,-y_bearing)
# We stroke and fill to make sure thinner parts are visible.
cr.text_path(text)
cr.set_line_width(0.5)
cr.stroke_preserve()
cr.fill()
return render_image(draw, width, height)

Here, we construct the draw function with a simple text drawing command, and run the wrapper.

There's some interesting text positioning going on up there; for more on this, and cairo in general, read through the excellent Cairo Tutorial for Python Programmers.

The last thing is to add an appropriate URL into your URLconf, such as

(r'^title/([^\/]+)/(\d+)/$', "render_title")

And then, when you browse to /title/HelloWorld/20/, you'll hopefully get a nice PNG of your new title! Then, you can just use img tags instead of titles, in this sort of style:

<img src="/title/{{ item.title }}/20" alt="{{ item.title }}" />

This process is quite quick, but not without a small cost of processing power; if you're using it a lot, think about some sort of caching. Apart from that, be happy with your newfound title freedom...

* Or possily Pylons. As long as you don't go and cavort with those Gems On Guiderails people, or heaven forbid the [PH/AS]P guys...