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.