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.