Serving Dynamic Images with Python CGI and Graphics Libraries

Having random images is mostly a novelty, but dynamic images are more useful in more places (CAPTCHAs, graphically displaying a log file, Sparklines, automatically resize pictures for a photo gallery, etc.)

Using the ideas from Serving Random Images, and help from Python's third party graphics libraries, serving up and creating dynamic images is a pretty straightforward process.

Step Zero: Setting Up

Before we jump in there are a few things to set up. First, we will need the third party graphics libraries. There are a few options for Python, but this tutorial will focus on using the Python Imaging Library (PIL). Other options include Agg, ImageMagick, Gnuplot, etc.; the process will be mostly the same.

Download PIL for your version of Python from the PIL site. If you are unfamiliar with PIL a manual is available on the site.

As with the previous article, we will be using Python's built in CGI server for testing. Type python -c "import CGIHTTPServer;CGIHTTPServer.test()" from the command line to start the test server in your current directory.

Step One: Create a simple dynamic image

To start we will try to get acquainted with some basic PIL functionality such as opening an image, creating a drawing object, drawing to the image, and writing back to a file. If you are already acquainted with using PIL you can skip this first step.

First, we need to open up an image file. You can either use an already existing file and open it using the Image.open function, or use Image.new to create a new image. Once you have an Image object created you can use ImageDraw.draw to open up a draw object, which provides simple 2d graphics such as lines, ellipses, text, etc (PIL ImageDraw Documentation). We will use some random numbers and draw.line to produce a different looking image each time.

import Image,ImageDraw
from random import randint as rint

def randgradient():
    img = Image.new("RGB", (300,300), "#FFFFFF")
    draw = ImageDraw.Draw(img)

    r,g,b = rint(0,255), rint(0,255), rint(0,255)
    dr = (rint(0,255) - r)/300.
    dg = (rint(0,255) - g)/300.
    db = (rint(0,255) - b)/300.
    for i in range(300):
        r,g,b = r+dr, g+dg, b+db
        draw.line((i,0,i,300), fill=(int(r),int(g),int(b)))

    img.save("out.png", "PNG")

if __name__ == "__main__":
    randgradient()
Save this code as dynimg1.py in any folder and run it. The output image out.png should be different every time.

Step Two: Add CGI

Now that we can make some kind of dynamic image we can put it inside of a CGI script to display a new image every time the page is refreshed. Because we would be putting this script on a web server it is bad form to write the image to a file every time (for security, disk space, and speed to name a few). Instead, we will manipulate all of the data in memory using the cStringIO module, which provides file-like objects that reside completely in memory.

import Image,ImageDraw
import cStringIO
from random import randint as rint

def randgradient():
    img = Image.new("RGB", (300,300), "#FFFFFF")
    draw = ImageDraw.Draw(img)

    r,g,b = rint(0,255), rint(0,255), rint(0,255)
    dr = (rint(0,255) - r)/300.
    dg = (rint(0,255) - g)/300.
    db = (rint(0,255) - b)/300.
    for i in range(300):
        r,g,b = r+dr, g+dg, b+db
        draw.line((i,0,i,300), fill=(int(r),int(g),int(b)))

    f = cStringIO.StringIO()
    img.save(f, "PNG")

    print "Content-type: image/png\n"
    f.seek(0)
    print f.read()

if __name__ == "__main__":
    randgradient()
Save this as dynimg2.py and load http://loopback:8000/cgi-bin/dynimg2.py, you should see a dynamically created gradient everytime you refresh.

So now that we can see it is possible to use PIL to dynamically create an image let's try something a little more advanced.

Step Three: Do Something More Useful

Now that the basics are down let's try something more useful. Let's say we have some third party program that produces a set of logs that record bandwidth at some regular interval. We could stick a Python script in the directory, start up a web server, and view dynamically generated graphs remotely over the internet.

Doing this is not much more complicated than the gradients created in the last section. Values from the log will be read from the file, then plotted using ImageDraw.line across the axis. The one new thing we will add is the ability for the script to take arguments. Arguments are read using the cgi module's FieldStorage object. When FieldStorage is initialized all of the information sent to the script (user info from a form, text inside the query string, etc.) is loaded for the script to use. The FieldStorage object works much like a dictionary, taking keys and returning values.

Our script will be used by loading the script with one argument, the log's filename (which should be in the root of the webserver). Once we have the filename we can open the file, read the values, plot them, then send the graph to the browser.

Here are three sample log files to use with this script: log1.txt, log2.txt, log3.txt

import Image,ImageDraw
import cStringIO
import cgi

X,Y = 500, 275 #image width and height

def graph(filename):
    img = Image.new("RGB", (X,Y), "#FFFFFF")
    draw = ImageDraw.Draw(img)

    #draw some axes and markers
    for i in range(X/10):
        draw.line((i*10+30, Y-15, i*10+30, 20), fill="#DDD")
        if i % 5 == 0:
            draw.text((i*10+15, Y-15), `i*10`, fill="#000")
    for j in range(1,Y/10-2):
        draw.text((0,Y-15-j*10), `j*10`, fill="#000")
    draw.line((20,Y-19,X,Y-19), fill="#000")
    draw.line((19,20,19,Y-18), fill="#000")

    #read in file and graph it
    log = file(r"c:\python\random_img\%s" % filename)
    for (i, value) in enumerate(log):
        value = int(value.strip())
        draw.line((i+20,Y-20,i+20,Y-20-value), fill="#55d")

    #write to file object
    f = cStringIO.StringIO()
    img.save(f, "PNG")
    f.seek(0)

    #output to browser
    print "Content-type: image/png\n"
    print f.read()

if __name__ == "__main__":
    form = cgi.FieldStorage()
    if "filename" in form:
        graph(form["filename"].value)
    else:
        print "Content-type: text/html\n"
        print """<html><body>No input file given</body></html>"""
Save the file as dynimg3.py and load the following URLs: A dynamic image will be created for each file; if you change the data in the files it is reflected in the graph.

Next steps

From here, one of the next steps you can take is using your script with an accompanying form (the form's action attribute must be set to the script name). You could also provide additional arguments to the script (size, colors, etc.).

Conclusion

Python's features make it a great web scripting language. You can write useful scripts very easily, and the language's diverse range of web frameworks make writing web apps even easier. Also, Python's great third party graphics libraries give you the ability to generate useful visual output with very little work.


-- Steven Kryskalla <skryskalla@gmail.com>
Back to the lost-theory Python pages.
Back to the lost-theory main page.