Friday, December 19, 2008

A small svg module

Update: by combining suggestions made in comments, one can probably do away with much of what I describe in this blog post. To wit:

>>> from xml.etree import ElementTree as etree
>>> from functools import partial
>>> Circle = partial(etree.Element, 'svg:circle')
>>> c = Circle(cx='100', cy='200', fill='red')
>>> etree.tostring(c)
'<svg:circle cx="100" cy="200" fill="red" />'


The only minor drawback is that attributes have to be strings, whereas the module described in this post could handle integer attributes. (Python 2.5+ required for ElementTree)


Original post below
(Time to take a break from the plugins blog series...)

Scalable Vector Graphics (SVG) are becoming more and more common on the web due to the increased support by decent browsers. SVG specifications include basic shapes such as circle, rectangles, etc., as well as supporting clipping, masking and composition, filter effects and much more. Attempting to write a python-ic module supporting all possible SVG primitives and options via code like

test_circle = Circle(x=10, y=10, r=5, color='red')

can be a daunting task. Furthermore, documenting such a module would result in a lot of duplication with the official specification document. Fortunately, there is a simpler way than simply attempting to write a complete SVG module using Class-based definitions such as the one written above. The idea is to use instead an API similar to that of ElementTree (see also) - albeit much simplified.

Suppose that we would want to be able to create SVG circles, such as
<circle cx="600" cy="200" r="100" fill="red" stroke="blue" width="10"/>
and rectangles, such as
<rect x="1" y="1" height="398" fill="none" stroke="blue" width="1198"/>
using Python code. A simple way to achieve this would be to define the following class:

class XmlElement(object):
'''First prototype from which all the xml elements are derived.

By design, this enables all elements to automatically give a
text representation of themselves - it is not quite complete.'''

def __init__(self, tag, **attributes):
'''A basic definition that will be replaced by the specific
one required by any element.'''
self.tag = tag
if attributes is not None:
self.attributes = attributes
else:
self.attributes = {}

def __repr__(self):
'''This normal python method used to give a string representation
for an object is used to automatically create the appropriate
syntax representing an xml object.'''
attrib = [" <%s" % self.tag] # open tag
for att in self.attributes:
attrib.append(' %s="%s"' % (att, self.attributes[att]))
attrib.append("/>\n")
return ''.join(attrib)
Using this class, we can create a circle instance corresponding to the definition written previously as

circle = XmlElement("circle", cx=600, cy=200, r=100, fill="red",
stroke="blue", width=10)


This is not quite as simple as the very first Circle() class-based example we wrote but it has the advantage of supporting all possible SVG attributes.

While the above XmlElement class definition is adequate for most basic SVG elements, it does not support such features as 1. text, 2. namespace (e.g. svg: prefix) and 3. grouping and sub-elements. All three additional features can be taken care of by the following modified class definition:

class XmlElement(object):
'''Prototype from which all the xml elements are derived.

By design, this enables all elements to automatically give a
text representation of themselves.'''

def __init__(self, tag, **attributes):
'''A basic definition that will be replaced by the specific
one required by any element.'''
self.tag = tag
self.prefix = ""
self.sub_elements = []
if attributes is not None:
self.attributes = attributes
else:
self.attributes = {}

def __repr__(self):
'''This normal python method used to give a string representation
for an object is used to automatically create the appropriate
syntax representing an xml object.'''
attrib = [" <%s%s"%(self.prefix, self.tag)] # open tag
for att in self.attributes:
if att != 'text':
attrib.append(' %s="%s"' % (att, self.attributes[att]))
if 'text' in self.attributes:
attrib.append(">%s\n" % (self.attributes['text'],
self.prefix, self.tag))
elif self.sub_elements:
attrib.append(">\n")
for elem in self.sub_elements:
attrib.append(" %s" % elem)
attrib.append("\n" % (self.prefix, self.tag))
else:
attrib.append("/>\n")
return ''.join(attrib)

def append(self, other):
'''append other to self to create list of lists of elements'''''
self.sub_elements.append(other)


That's almost it! With the exception of comments and Document Type Definition (dtd), we can use the above to create simple xhtml document containing ANY svg graphics without having to worry about xhtml syntax, opening and closing brackets, etc. However, we can possibly do even a little better. Consider the following xhtml document with embedded svg graphics:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<head>
<title>This is the title.</title>
</head>
<body>
<p>This is the body.</p>
<svg:svg width="0" height="0">
<svg:defs>
<svg:circle cy="0" cx="0" r="20" id="red_circle" fill="red"/>
</svg:defs>
</svg:svg>
<svg:svg width="200" height="200">
<svg:use xlink:href="#red_circle" transform="translate(100, 100)"/>
</svg:svg>
<!-- This is a comment. -->
</body>
</html>


With just a few additional definitions, we can create this document using only Python code as follows:
doc = XmlDocument()
doc.head.append(XmlElement("title", text="This is the title."))

# A good practice is to define svg objects, and insert them
# using the definition; this is overkill for this example, but it
# provides a test of the class.
test_def = SvgDefs()
test_def.append(SvgElement("circle", cx=0, cy=0, r=20, fill="red",
id="red_circle"))

doc.body.append(XmlElement("p", text="This is the body."))
doc.body.append(test_def)

# we now create an svg object, that will make use of the definition above.
svg_window = SvgElement("svg", width="200", height="200")
use_circle = SvgElement("use", transform="translate(100, 100)")

# xlink:href can't be used as an attribute name passed to __init__
# this is why we use this two-step process.
use_circle.attributes["xlink:href"] = "#red_circle"

svg_window.append(use_circle)
doc.body.append(svg_window)
doc.body.append(Comment("This is a comment.")) # just for fun.

print doc

The additional definitions are as follow:

class XmlDocument(XmlElement):
def __init__(self):
self._begin = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">\n"""
self._end = "</html>"
self.head = XmlElement("head")
self.body = XmlElement("body")

def append(self):
'''Directly appending is not allowed'''
assert False, "Append to either head or body."

def __repr__(self):
'''Gives an appropriate representation of an xml document.'''
return self._begin + str(self.head) + str(self.body) + self._end


class SvgElement(XmlElement):
'''Prototype from which all the svg elements are derived.

By design, this enables all elements to automatically give an
appropriate text representation of themselves.'''
def __init__(self, tag, **attributes):
XmlElement.__init__(self, tag, **attributes)
self.prefix = "svg:"

class SvgDefs(SvgElement):
'''Short-cut to create svg defs. A user creates an instance of this
object and simply appends other svg Elements'''
def __init__(self):
self.defs = SvgElement("defs")
self.root = SvgElement("svg", width=0, height=0)
self.root.append(self.defs)

def append(self, other):
'''appends other to defs sub-element, instead of root element'''
self.defs.append(other)

def __repr__(self):
'''gives a string representation of an object, appropriate for
insertion in an html document'''
return str(self.root)

class Comment(object):
'''Comment that can be inserted in code xml documents'''
def __init__(self, text):
self.text = text
def __repr__(self):
return "<!-- " + self.text + " -->\n"


That's it for real this time! Fewer than 100 lines of code that you can use if you need to programmatically create (x)html documents containing svg images. There are a few limitations (elements containing text may not be chained...) but it works for me. If you want to try it yourself, you can find the module here.

4 comments:

Andrew Dalke said...

As a variation, you could derive from ElementTree classes. There is the slightly tricky part of getting the namespaces to use the "svg:" that you want, but otherwise it's pretty direct.

Two immediate advantages are escape support for special characters (like the three characters '"> in an attribute) and Unicode.

These days I prefer generating XML with templates; keep the data structure in Python and use some template package to convert that into XML.

Anonymous said...

from functools import partial

Circle = partial(XMLElement, 'circle')

circle = Circle(cx=600, cy=200, r=100, fill="red", stroke="blue", width=10)

André Roberge said...

Andrew and Anonymous: Thank you for your suggestions; you gave me something else to investigate...

André

Kfm said...

Nice,
have a look at http://codeboje.de/pysvg/