Articles

Django and Cairo: Rendering Pretty Titles

One of the overwhelming horrors of designing for the web (or so it would appear from a lot of the mockups I've seen) is that designers (and people who are just bored of Arial and Times New roman) want to make their titles on web pages using non-standard fonts. "But that's shockingly non-accessible and uses more bandwidth", I hear you cry; well, there's a reason the alt tag is around, and why broadband is much more common.

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...

Comments

  1. Jon Leighton

    alt *attribute*!

    That will be all ;)

  2. Andrew Godwin

    Yes, yes, very clever, Mr. Pedantic. Who needs proofreading, when you can have people point out your mistakes for you... *coughs loudly*.

  3. Alan71

    Yes, images are cute but welcome to the non-semantic web. I've noted two things. First, SEO : Pretty titles won't have the same weights for crawlers than h1, h2, .. than an alt attribute. Second thing, accessibility : every application that has to read the content of a webpage will be in trouble and speech synthesis, for instance, will be badly renderer.

    Those same problems occur with flash banners/titles/etc... Is it really worth the pain?

    PS: nice piece of code by the way

  4. LKRaider

    I think it's great!

    For accessibility, there's always the CSS media types ;D

  5. Anonymous

    Sorry to go off-topic, but any news on LastGraph? "983 graphs needing rendering." ;_;

  6. Jehiah

    any chance of an example link? i want to see how the rendering looks.

  7. Antti Kaihola

    Alternative implementations of similar ideas:

    * improved text image view[1] by Jacob Kaplan-Moss
    * simple text image view[2] by derelm, based on work by Andrew Gwozdziewycz and Jacob Kaplan-Moss (above)
    * django-rendertext[3] by olau at iola in Denmark, based on snippet above
    * django-image-replacement[4] by Ludwig Pettersson

    [1] http://jacobian.org/writing/improved-text-image-view/
    [2] http://www.djangosnippets.org/snippets/117/
    [3] http://code.google.com/p/django-rendertext/
    [4] http://code.google.com/p/django-image-replacement

  8. Antti Kaihola

    The django-cairo-text re-usable app[1] is based on Andrew's code snippets above.

    [1]: http://github.com/akaihola/django-cairo-text/

  9. Comments are no longer possible on this post.

15th December 2007

Web Design, Python, Django