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.
{: .alignnone .size-full .wp-image-223 width="302" height="230"}
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:
{: .brush:shell}
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
``` {: .brush:python} 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/
{: .alignnone .size-full .wp-image-222 width="302" height="230"}
http://127.0.0.1:8000/shapes/shapes/
{: .alignnone .size-full .wp-image-223 width="302" height="230"}
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:
``` {: .brush:python} 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/
{: .alignnone .size-full .wp-image-230 width="294" height="652"}
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.