Make CSS Sprites with the Python Image Library colorful lead photo

I'm going to show you how to write a Python script to automatically rollup icons into a single sprite image. The CSS to use those sprites is generated at the same time, by the same script. I think you'll find this a lot more maintainable than doing it by hand.

If you're familar with the CSS sprite technique, you can skip the next section, which gives a quick introduction.

Background

Icons can kill a page's performance, because there's some fixed overhead associated with loading an image, regardless of size. For a small icon, say 16x16 pixels, this overhead dwarfs the time spent loading the actual data.

CSS gurus figured out a way to avoid this: put all the icons into a single image, and use CSS to only show a little piece of the image at a time. It's called the sprite technique because the rollup images resemble the sprite sheets that graphic designers used to make for video games back in the day.

It's a good technique: it saves bandwith and makes the user's experience more responsive. But it's annoying to manually edit the rollup file when icons are added, removed, or edited, and then go into the CSS file and get the offsets to match. So, let's script it!

Script

Here's the complete script. I'll come back and describe the important bits later, but first let's see how it works.

It expects to be run in a directory that contains the icon files and a plain text file called icon_map.txt that defines a mapping from CSS classes to images:

menu_home:arrow_left.png
menu_about_me:vcard.png


menu_about_site:sitemap.png
...
menu_resume:page_word.png
menu_index:page_key.png

Only the images listed in this file will be included in the master sprite image. The example lists PNG files, but you can use any format. The images don't have to be the same format, but they do have to all be the same size.

When the script runs, it creates four files:

  • master.png, the sprite sheet in PNG format
  • master.gif, the sprite sheet in GIF format
  • icons_png.css, a css file that uses master.png
  • icons_gif.css, a css file that uses master.gif

Both generated icon sprite sheets look like this:

icon sprite sheet image

And the generated CSS files look like this:

li.menu_about_me {


    background-image:url(/static/icons/master.gif);
    background-position: 6px 4px;
}
li.menu_about_site {
    background-image:url(/static/icons/master.gif);
    background-position: 6px -28px;
}
li.menu_tags {
    background-image:url(/static/icons/master.gif);
    background-position: 6px -476px;
}

As you can see, each list item uses the master image for its background, but with the exact offset to show only one icon.

Why Both PNG and GIF?

I generate seperate images and css files for both PNG and GIF for two reasons:

  • It's not obvious (or well-documented) how to get PIL to create a transparent GIF from scratch, so I wanted to give a concrete example of that.
  • My site actually uses conditional comments to use GIF for IE6, and PNG for everyone else, to work around the well-known IE6 bug.

How Does the Script Work?

Three words: Python Image Library. I use PIL to open each icon, paste them onto the master at increasing offset, and write out the master image in both formats.

I start by parsing icon_map.txt into an array of pairs:

iconMapFile = open('icon_map.txt')
iconMap = sorted( 
                  line.rstrip().split(':') 
                  for line in iconMapFile.readlines()
                  if line.rstrip()
)
iconMapFile.close()

Then I load all the icons:

images = [Image.open(filename) for cssClass, filename in iconMap]

Since the images should all be the same size, I figure out the icon size from the first one and compute the correct size for the master:

image_width, image_height = images[0].size
master_width = image_width
master_height = (image_height * len(images) * 2) - image_height

A new image is created for the master:

master = Image.new(
    mode='RGBA',
    size=(master_width, master_height),
    color=(0,0,0,0))  # fully transparent

And each icon is pasted on at its offset:

for count, image in enumerate(images):
    location = image_height*count*2
    master.paste(image,(0,location))

Then I save the master in both formats:

master.save('master.gif', transparency=0 )
master.save('master.png')

That's it for the sprite sheet; next I generate the css files. There's a basic pattern each CSS rule will follow:

cssTemplate = '''li.%s {
    background-image:url(/static/icons/master.%s);
    background-position: 6px %dpx;
}
'''

And then making the CSS files is easy:

for format in ['png','gif']:
    iconCssFile = open('icons_%s.css' % format ,'w')
    for count, pair in enumerate(iconMap):
        cssClass, filename = pair
        location = image_height*count*2
        iconCssFile.write(cssTemplate % (cssClass, format, 4-location))
    iconCssFile.close()

Hacking the Script

The script isn't exactly flexible, is it? All the file names and such are hard coded. But since it's just a very short Python script, it's easier to just modify the script then it is to figure out how to configure it. There's no real substance to it: all the real work is done by PIL, so this will best serve as example of how to use that excellent library. It has a creative commons license, so go wild.

- Oran Looney January 12th 2008

Thanks for reading. This blog is in "archive" mode and comments and RSS feed are disabled. We appologize for the inconvenience.