SVG Library in Python

In this post I will develop a Python class to create and save an SVG (Scalable Vector Graphics) file, complete with a few examples of its use.

What is SVG?

This post covers implementing an SVG library in Python, but before actually starting to cut code I'll give a brief overview of SVG, as it perhaps isn't as well know as, say, JPEG or PNG. Obviously, if you already understand SVG skip the rest of this section.

Image formats such as JPEG and PNG are bitmap or raster images: they consist of just rows and columns of pixels and are best suited to photographs or complex computer-generated artworks. SVG is completely different as it is a vector graphics format (SVG stands for Scalable Vector Graphics). Instead of pixels, the image is made up of individual elements such as shapes (rectangles, circles etc.), lines, text and so on. This makes it more suitable for simpler images such as graphs, diagrams or logos. SVG is XML based and therefore contains a hierarchy of human-readable tags. Just to give a flavour the following will draw a blue circle with a 2-pixel black border. The circle's radius is 32 pixels, and the coordinates of the centre are 128, 128.

SVG example

<circle stroke='black' stroke-width='2px' fill='blue' r='32' cy='128' cx='128' />

It's probably worth mentioning that SVG can be used in two ways. The first way is to create a stand-alone file as I am doing here. The second way is to embed it in an HTML page, where the individual elements form part of the DOM and can therefore be manipulated by JavaScript (eg for animations), and any text in the image can be read by search engines. You can of course put an SVG file in an image tag on a page, but it cannot then be scripted or picked up by search engines.

What are we actually going to create?

So let's think about what we are going to create. As I mentioned above, we will develop a class which can be reused in different projects, as well as a client application to test it out and demonstrate its usage.

The class will hold the various elements of the SVG graphic in a list (for reasons I'll explain later) and will have a number of methods for creating a new drawing, adding elements, finalizing the drawing, converting it to a string and saving it to a file.

The class will not cover all that SVG is capable of by any means, but it covers the basics enough to be useful. There is plenty of scope for adding more functionality which I hope to do in the future.

Start Coding!

Firstly, create a new folder somewhere convenient and within it create the following empty files. If you do not want to type or copy/paste the code you can download a zip file with the source code, or clone/download from Github.

Source Code Links

ZIP File
GitHub

  • svg.py - contains the SVG class
  • main.py - a main function plus a few functions to demonstrate the class

In svg.py type or copy/paste this:

svg.py

class SVG(object):

    """
    Provides methods for creating an empty SVG drawing, adding various
    shapes and text, and saving the finished file.
    """

    def __init__(self):

        """
        Create a few attributes with default values,
        and initialize the templates dictionary.
        """

        self.svg_list = []
        self.width = 0
        self.height = 0

        self.templates = self.__generate_templates()

    def __add_to_svg(self, text):

        """
        Utility function to add element to drawing.
        """

        self.svg_list.append(str(text))

    def __generate_templates(self):

        """
        Create a set of templates for each element type for use by
        methods creating each of these types.
        """

        templates = {}

        templates["create"] = "<svg width='{}px' height='{}px' xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink'>\n"
        templates["finalize"] = "</svg>"
        templates["circle"] = "    <circle stroke='{}' stroke-width='{}px' fill='{}' r='{}' cy='{}' cx='{}' />\n"
        templates["line"] = "    <line stroke='{}' stroke-width='{}px' y2='{}' x2='{}' y1='{}' x1='{}' />\n"
        templates["rectangle"] = "    <rect fill='{}' stroke='{}' stroke-width='{}px' width='{}' height='{}' y='{}' x='{}' ry='{}' rx='{}' />\n"
        templates["text"] = "    <text x='{}' y = '{}' font-family='{}' stroke='{}' fill='{}' font-size='{}px'>{}</text>\n"
        templates["ellipse"] = "    <ellipse cx='{}' cy='{}' rx='{}' ry='{}' fill='{}' stroke='{}' stroke-width='{}' />\n"

        return templates

    def create(self, width, height):

        """
        Adds the necessary opening element to document.
        """

        self.width = width
        self.height = height

        self.svg_list.clear()

        self.__add_to_svg(self.templates["create"].format(width, height))

    def finalize(self):

        """
        Closes the SVG element.
        """

        self.__add_to_svg(self.templates["finalize"])

    def circle(self, stroke, strokewidth, fill, r, cx, cy):

        """
        Adds a circle using the method's arguments.
        """

        self.__add_to_svg(self.templates["circle"].format(stroke, strokewidth, fill, r, cy, cx))

    def line(self, stroke, strokewidth, x1, y1, x2, y2):

        """
        Adds a line using the method's arguments.
        """

        self.__add_to_svg(self.templates["line"].format(stroke, strokewidth, y2, x2, y1, x1))

    def rectangle(self, width, height, x, y, fill, stroke, strokewidth, radiusx, radiusy):

        """
        Adds a rectangle using the method's arguments.
        """

        self.__add_to_svg(self.templates["rectangle"].format(fill, stroke, strokewidth, width, height, y, x, radiusy, radiusx))

    def fill(self, Fill):

        """
        Fills the entire drawing with specified Fill.
        """

        self.rectangle(self.width, self.height, 0, 0, Fill, Fill, 0, 0, 0)

    def text(self, x, y, fontfamily, fontsize, fill, stroke, text):

        """
        Adds text using the method's arguments.
        """

        self.__add_to_svg(self.templates["text"].format(x, y, fontfamily, stroke, fill, fontsize, text))

    def ellipse(self, cx, cy, rx, ry, fill, stroke, strokewidth):

        """
        Adds ellipse using the method's arguments.
        """

        self.__add_to_svg(self.templates["ellipse"].format(cx, cy, rx, ry, fill, stroke, strokewidth))

    def __str__(self):

        """
        Returns the entire drawing by joining list elements.
        """

        return("".join(self.svg_list))

    def save(self, path):

        """
        Saves the SVG drawing to specified path.
        Let any exceptions propagate up to calling code.
        """

        f = open(path, "w+")

        f.write(str(self))

        f.close()

The class is rather long but quite straightforward, with the methods which add elements all working in roughly the same way. I'll run through each function and method in turn.

__init__

Here we just create an empty list to hold SVG elements, and height and width attributes with default values of 0. We also create a templates attribute, initialized with a call to __generate_templates() which I'll describe in a moment.

__add_to_svg

A simple one-liner which adds the specified string to the SVG list. You could justifiably argue that this function is unnecessary but for the purist it does separate out the process of adding elements to the list from their creation.

__generate_templates

SVG elements consist mostly of standard text with a few variables for colours, positions, sizes etc. The neatest way of creating an element string is therefore to use a template for each type which we can then use with the string format function to substitute {} for actual values.

In this function I have created a dictionary and added various templates to it before returning the dictionary. We saw this function in use in __init__ to initialize self.templates. You might like to spend a minute or two looking at the templates to understand how SVG elements work.

create

Rather than including the code to create and initialize an SVG drawing in __init__ I have used a separate method for this. The rationale behind this is that it enables a single instance of a class to be used repeatedly to create and save any number of drawings.

The method sets height and width, empties the list (in case we are reusing the object for more than one drawing) and adds an opening element.

Note the format of the last line as it will be used for several more methods:

  • self.__add_to_svg - adds the string argument to the list
  • self.templates["create"] - picks up the relevant template from the dictionary
  • .format(width, height) - substitutes the {} placeholders in the template with arguments

finalize

This method finishes of the drawing by adding the relevant template, the only one with no additional variables.

circle, line, rectangle, ellipse, text

All of these methods work in very much the same way and use the templating technique as seen in the create method. They only differ in their arguments which are specific to each shape.

Most of the arguments are self-explanatory except these:

  • The circle's argument r is its radius
  • stroke is the colour of the shape's border or outline
  • fill is also a colour, this time of the shape's interior

fill

This method fills the entire drawing with the specified colour by leveraging the rectangle method with the height and width attributes.

__str__

Here we define the __str__ special method to return the drawing as a single string. If you call str() on an object Python will call __str__, so any class implementing __str__ should do so by returning some sort of meaningful string representation of the object.

In this case we call join on the list of SVG elements. The reason I have used a list rather than a single string is that strings are immutable therefore repeated concatenation is very inefficient as it actually involves the creation of new strings each time. Storing the individual components of a string in a list and then calling join is a far more efficient pattern and should be used whenever you need to build a string from a number of smaller strings.

save

A pretty simple function which opens a file for writing, writes the string representation of the drawing to it, and then closes it. There is no exception handling as in this case it makes more sense for the calling code to handle any IO errors.

Now we can try out the SVG class by creating a few drawings. Open main.py and enter or paste the first chunk of code.

main.py (part 1)

import svg
import random

def main():

    """
    Try out the SVG class by calling function to
    create and save a few drawings.
    """

    print("-----------------")
    print("| codedrome.com |")
    print("| SVG Library   |")
    print("-----------------")

    draw_all_shapes()

    #i_want_to_believe()

    #mondrian()


def draw_all_shapes():

    """
    Quick demo of creating SVG object, using all methods,
    and saving the finished file.
    """

    s = svg.SVG()

    s.create(256, 192)

    s.fill("#A0A0FF")
    s.circle("#000080", 4, "#0000FF", 32, 64, 96)
    s.line("#000000", 2, 8, 8, 248, 184)
    s.rectangle(64, 64, 112, 32, "#00FF00", "#008000", 4, 4, 4)
    s.text(32, 16, "sans-serif", 16, "#000000", "#000000", "codedrome.com")
    s.ellipse(64, 160, 32, 16, "#FF0000", "#800000", 4)

    s.finalize()

    try:
        s.save("allshapes.svg")
    except IOError as ioe:
        print(ioe)

    print(s)


main()

The main function includes three calls of functions which we'll now implement, starting with draw_all_shapes(). This function demonstrates the basic usage pattern of the SVG class:

  • Create a new object with svg.SVG()
  • Initialize a drawing with create
  • Call any of the methods which draw shapes or text
  • Call finalize()
  • Call save with appropriate exception handling

I have also called print with the SVG object just to show the contents of the drawing; this calls str() behind the scenes so will show exactly what is saved to the file.

In each of the three methods here I have only used an instance of the SVG class once but you can reuse them as much as you like by calling create to reset the dimensions and contents.

We can now run the program with this command.

Running

python3.7 main.py

Go to the folder where you saved your files and open allshapes.svg which should look like this.

allshapes.svg

We could end it there but even software engineers have a bit of artistic talent so let's finish off by drawing a couple of proper pictures. Enter or copy/paste the i_want_to_believe function below, comment out draw_all_shapes in main and uncomment i_want_to_believe.

main.py (part 2)

def i_want_to_believe():

    """
    A more complex drawing for X Files fans.
    """

    s = svg.SVG()

    s.create(512, 768)

    s.fill("#000010");

    for star in range(0, 512):

        x = random.randrange(0, 512)
        y = random.randrange(0, 768)

        s.rectangle(1, 1, x, y, "white", "white", 0, 0, 0)

    s.text(96, 712, "sans-serif", 32, "#FFFFFF", "#FFFFFF", "I WANT TO BELIEVE")

    s.circle("silver", 1, "rgba(0,0,0,0)", 28, 256, 384)

    s.ellipse(256, 374, 8, 14, "#808080", "#808080", 0)
    s.ellipse(252, 372, 3, 2, "#000000", "#000000", 0)
    s.ellipse(260, 372, 3, 2, "#000000", "#000000", 0)
    s.rectangle(1, 1, 251, 371, "white", "white", 0, 0, 0)
    s.rectangle(1, 1, 259, 371, "white", "white", 0, 0, 0)
    s.line("black", 2, 254, 378, 258, 378)

    s.line("silver", 2, 234, 416, 226, 432)
    s.line("silver", 2, 278, 416, 286, 432)
    s.ellipse(256, 400, 64, 16, "silver", "silver", 4)

    s.finalize()

    try:
        s.save("iwanttobelieve.svg")
    except IOError as ioe:
        print(ioe)

Note that colours can be in hexadecimal format, named colours, rgb or rgba; the "a" in rgba stands for alpha, ie opacity. In one of the functions I have set this to 0 meaning completely transparent. You'll see why in a moment.

Run the program again to create the following image.

iwanttobelieve.svg

The stars are randomly generated so the chances of yours being identical are

Number of possible star patterns!

1.0 / pow(768, 393216)

so not much chance at all really! If there happen to be any stars behind the spaceship's dome you will see them - that's why I used an alpha of 0 for its fill.

Finally let's do some real art. Paste the following into main.py, in the main function comment out i_want_to_believe, uncomment mondrian and run.

main.py (part 3)

def mondrian():

    """
    Serious art here.
    """

    s = svg.SVG()

    s.create(512, 512)

    s.fill("white")

    s.rectangle(512, 512, 0, 0, "white", "black", 1, 0, 0)

    s.rectangle(256, 256, 64, 64, "red", "red", 0, 0, 0)
    s.rectangle(128, 128, 64, 320, "black", "black", 0, 0, 0)
    s.rectangle(64, 128, 0, 384, "orange", "orange", 0, 0, 0)
    s.rectangle(128, 192, 320, 0, "orange", "orange", 0, 0, 0)
    s.rectangle(128, 64, 320, 384, "navy", "navy", 0, 0, 0)
    s.rectangle(64, 128, 448, 384, "red", "red", 0, 0, 0)

    s.line("black", 8, 0, 64, 448, 64)
    s.line("black", 8, 64, 64, 64, 512)
    s.line("black", 8, 0, 192, 64, 192)
    s.line("black", 8, 0, 384, 512, 384)
    s.line("black", 8, 128, 0, 128, 64)
    s.line("black", 8, 320, 0, 320, 448)
    s.line("black", 8, 64, 320, 448, 320)
    s.line("black", 8, 320, 192, 448, 192)
    s.line("black", 8, 64, 448, 448, 448)
    s.line("black", 8, 448, 0, 448, 512)
    s.line("black", 8, 192, 320, 192, 512)
    s.line("black", 8, 384, 192, 384, 320)

    s.finalize()

    try:
        s.save("mondrian.svg")
    except IOError as ioe:
        print(ioe)

You should end up with this, the image from the top of the page. I told you we were doing real art!

mondrian.svg

We now have a complete and useful SVG class but of course there is huge scope for both improving the existing functionality and adding more. I'll save those for the future.

Please follow CodeDrome on Twitter for news of future posts and other useful stuff.

3 thoughts on “SVG Library in Python

  1. Hi Alex, I’ll post your email here so everyone can see it:

    “Good day Chris.
    I read your article on the svg class and I would like to ask you a question please.
    In the templates for the circle, eclipse , create etc I was wondering if you may have the template for a bezier path.
    The Quadratic Bezier will do.
    I was looking all over.
    I know what the code in svg write is to draw a bezier path but would like to add a bezier to your class.
    thanks
    Kind regards
    Alex”

    Bezier curves have been on my very long “to do” list for a long time but I have never got round to it. This is a link to a page on the Mozilla site which covers the topic (about a third of the way down).

    https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths

    I’ll get round to it one day but in the meantime if you want to write your own version please feel free to fork the Github repository,

    Chris.

Leave a Reply

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