Using Cairo to generate SVG in Django

Cairo is a 2D vector graphics api used by Firefox, Gtk and other desktop projects.

I’m going to show here that it can also be used to generate web content, using Django.

I’m going to port two examples from the Michael Urmans Cairo Tutorial for PyGTK Programmers.

To understand the cairo and it’s drawing model I’d recommend his his Cairo Tutorial for Python Programmers.

Note: In this example I’ll be generating SVGs…  as I.E. (as of 2010) does not support them, you might want to generate PNG or PDF – if you need to do this with cairo, look for one of the many cairo tutorials on the web.

The example django project can be downloaded at the end of the article.

Prerequisites

You’ll need django and pycairo installed… and a little bit of Django knowledge.

In Windows you can do use easy_install to get the python dependencies:

easy_install pycairo
easy_install django

In linux you’ll need to use your favourite package manager.

Onto the code…

Hosting Framework

The pygtk example starts by building a simple hosting framework

#! /usr/bin/env python
import pygtk
pygtk.require('2.0')
import gtk, gobject, cairo

# Create a GTK+ widget on which we will draw using Cairo
class Screen(gtk.DrawingArea):

    # Draw in response to an expose-event
    __gsignals__ = { "expose-event": "override" }

    # Handle the expose-event by drawing
    def do_expose_event(self, event):

        # Create the cairo context
        cr = self.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        cr.rectangle(event.area.x, event.area.y,
                event.area.width, event.area.height)
        cr.clip()

        self.draw(cr, *self.window.get_size())

    def draw(self, cr, width, height):
        # Fill the background with gray
        cr.set_source_rgb(0.5, 0.5, 0.5)
        cr.rectangle(0, 0, width, height)
        cr.fill()

# GTK mumbo-jumbo to show the widget in a window and quit when it's closed
def run(Widget):
    window = gtk.Window()
    window.connect("delete-event", gtk.main_quit)
    widget = Widget()
    widget.show()
    window.add(widget)
    window.present()
    gtk.main()

if __name__ == "__main__":
    run(Screen)

This needs to be made ready for the web and the Gtkisms removed:

cairodraw.py

from cairo import Context, SVGSurface

# Create a GTK+ widget on which we will draw using Cairo
class CairoWidget:

    def __init__(self, Surface = None):
        if Surface == None:
            Surface = SVGSurface

        self.Surface = Surface

    def draw(self, cr, width, height):
        # Fill the background with gray
        cr.set_source_rgb(0.5, 0.5, 0.5)
        cr.rectangle(0, 0, width, height)
        cr.fill()

def draw_widget(dest, Widget, Surface = SVGSurface, width = 100, height = 100):
    """
    Convenience function to output CairoWidget to a buffer
    """
    widget = Widget(Surface)
    surface = widget.Surface(dest, width, height)
    widget.draw(Context(surface), width, height)
    surface.finish()

Changes:

  • Screen class is now CairoWidget as Screen made less sense in this context.
  • run() is replaced with draw_widget() and it has some new parameters to help it be rendered with django.
  • The new file is ‘cairodraw.py’, not ‘framework.py’

Initial view…

Heres the initial views.py

# Create your views here.

from django.http import HttpResponse
from cairo import SVGSurface

import cairodraw

from math import pi

class Shapes(cairodraw.CairoWidget):
    def draw(self, cr, width, height):
        cr.set_source_rgb(0.5, 0.5, 0.5)
        cr.rectangle(0, 0, width, height)
        cr.fill()

        # draw a rectangle
        cr.set_source_rgb(1.0, 1.0, 1.0)
        cr.rectangle(10, 10, width - 20, height - 20)
        cr.fill()

        # draw lines
        cr.set_source_rgb(0.0, 0.0, 0.8)
        cr.move_to(width / 3.0, height / 3.0)
        cr.rel_line_to(0, height / 6.0)
        cr.move_to(2 * width / 3.0, height / 3.0)
        cr.rel_line_to(0, height / 6.0)
        cr.stroke()

        # and a circle
        cr.set_source_rgb(1.0, 0.0, 0.0)
        radius = min(width, height)
        cr.arc(width / 2.0, height / 2.0, radius / 2.0 - 20, 0, 2 * pi)
        cr.stroke()
        cr.arc(width / 2.0, height / 2.0, radius / 3.0 - 10, pi / 3, 2 * pi / 3)
        cr.stroke()

class Transform(cairodraw.CairoWidget):
    def draw(self, cr, width, height):
        cr.set_source_rgb(0.5, 0.5, 0.5)
        cr.rectangle(0, 0, width, height)
        cr.fill()

        # draw a rectangle
        cr.set_source_rgb(1.0, 1.0, 1.0)
        cr.rectangle(10, 10, width - 20, height - 20)
        cr.fill()

        # set up a transform so that (0,0) to (1,1)
        # maps to (20, 20) to (width - 40, height - 40)
        cr.translate(20, 20)
        cr.scale((width - 40) / 1.0, (height - 40) / 1.0)

        # draw lines
        cr.set_line_width(0.01)
        cr.set_source_rgb(0.0, 0.0, 0.8)
        cr.move_to(1 / 3.0, 1 / 3.0)
        cr.rel_line_to(0, 1 / 6.0)
        cr.move_to(2 / 3.0, 1 / 3.0)
        cr.rel_line_to(0, 1 / 6.0)
        cr.stroke()

        # and a circle
        cr.set_source_rgb(1.0, 0.0, 0.0)
        radius = 1
        cr.arc(0.5, 0.5, 0.5, 0, 2 * pi)
        cr.stroke()
        cr.arc(0.5, 0.5, 0.33, pi / 3, 2 * pi / 3)
        cr.stroke()

def shapes(request):
    response = HttpResponse(mimetype='image/svg+xml')
    cairodraw.draw_widget(response, Shapes)
    return response

def transform(request):
    response = HttpResponse(mimetype='image/svg+xml')
    cairodraw.draw_widget(response, Transform)
    return response

The main difference is that Shape and Transform are the same, except they extend cairodraw.CairoWidget.

urls.py

This is pretty straightforward.

(r'^shapes/shapes/$', 'shapes.views.shapes'),
(r'^shapes/transform/$', 'shapes.views.transform'),

Taking stock…

This is a good stage to try things out, heres the django project so far: django_cairo_1a.

Enter the svgsite directory and run

python manage.py runserver

If all is well it should output something like this:

Validating models...
0 errors found

Django version 1.1.1, using settings 'svgsite.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

If you visit the two URLs in most browsers except I.E. the output will look like this:

http://127.0.0.1:8000/shapes/transform/

http://127.0.0.1:8000/shapes/shapes/

Inline SVG and templates…

The previous examples output straight SVG, however it would be much better to be able to incorperate SVG into templates and use it with HTML.

Luckily modern browsers support this, and with a couple of changes we can make templates that will output mixed documents like this.

Example template:

<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
 <title>SVG embedded inline in XHTML</title>
 </head>
 <body>
 <h1>Transform</h1>
 <div style="width:50%, height:40px">{{transform|safe}}</div>
 <div><a href="transform">Full screen svg</a></div>
 <h1>Shapes</h1>
 <div style="width:50%">{{shapes|safe}}</div>
 <div><a href="shapes">Full screen svg</a></div>
 </body>
</html>

The variables ‘shapes’ and ‘transform’ will be the svg, the key is the ‘|safe’, which means the XML won’t be processed by django.

Changes to views to support inline

To use templates the SVG data is needed as a string to pass to the template.

Heres an index view demonstrating this:

def index(request):
    buff = StringIO()
    cairodraw.draw_widget(buff, Shapes)
    shapes = buff.getvalue()[38:]

    buff = StringIO()
    cairodraw.draw_widget(buff, Transform)
    transform = buff.getvalue()[38:]

    return render_to_response(
    'shapes_index.html',
    {"transform": transform, "shapes": shapes},
    mimetype='application/xhtml+xml')

The main differences are that -
cairodraw.draw_widget() is called with a temporary buffer, we use templates.

Evil hack alert:

OK, I did something naughty… notice the [38:] ? Unfortunately django doesn’t like having in the middle of the output. I couldn’t find a way to turn this off so we chop it off the beginning of the string.

Apart from the evil hack this works well and you get output like this:

http://127.0.0.1:8000/shapes/

Final Version

Cool, every thing seems to be working !

The final version to try django_cairo_1b.

Next time….

I’ll be looking at using a Cairo based library,  PyCha library with django to output smooth looking charts in SVG.

9 thoughts on “Using Cairo to generate SVG in Django

  1. Wow, you beat me, I was working on a quite similar setup past week, but had to back off due to other tasks.

    Any thoughts on how to hook this up for interactivity with ecmascript?

  2. Good question… I had a look at the generated SVG and it could definitely be nicer (theres no way to name particular bits).

    You might be able to generate a piece of SVG with cairo, then put the XML into some DOM library where you could add a css ID or Path to it.

    Really we need a new bit of the cairo-svg api that will let us output metadata as we go.

    Post here if you do anything though as I think there should be more Cairo and SVG on the web.

  3. response = HttpResponse(mimetype=’image/svg xml’)

    I don’t understand this – you’re using a Django HttpResponse object as a context for rendering in Cairo? How does this work? Thanks

  4. Almost that, but not quite –
    The rendering context is provided by SVGSurface.
    SVGSurface can take a file like object instead of a filename, so we use the response.
    Rendering happens on the SVGSurfaces Context.
    When .finish() is called on SVGSurface, it does it’s rendering (in this case SVG data, to the response).

  5. Hi Stu,

    Nice article/example. Really appreciate it.

    In the “Evil hack alert” there is something which doesn’t render out to the reader’s browser in this line: “django doesn’t like having in the middle”

    Can you patch that up? I’m assuming it is a control character, probably a line end, but for the sake of the reader it would be nice not to have to guess.

    Cheers!

  6. Only just saw this comment … I’ll try and find some time to fix this – the code removes something like an XML DocString (I’ll need to get home and run this example to find the exact string it removes).

    [This comment edited after I saw what the question really meant]

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>