CSS Image Sprite Maker in Python

css sprites

If you have a number of small images on your website, for example those used for icons or buttons, it is more efficient to combine them into a single image which is usually smaller in size and requires only one HTTP request to download. You can then use CSS to specify an offset to the exact position of the individual image in the combined image.

Creating the combined image and corresponding CSS can be done by hand but this is time-consuming, boring and error-prone so I have written a simple Python module to do the job for you.

Project Overview

This project consists of a file called spritemaker.py for the module, and another file called spritemakerconsole.py containing a main function to try out the module. The program really needs a GUI which I'll write for a future project.

The download ZIP file and GitHub repository also contain a few social media icons which we'll combine into a single image although of course you can substitute your own. This is the combined image.

css sprites

And this is a sneak preview of one of the CSS classes generated by the program.

.youtube
{
    background: url('sprites.png') no-repeat;
    width: 36px;
    height: 36px;
    display: inline-block;
    background-position: -180px 0px;
}

In the last line note that the x-coordinate is negative. This is because we need to specify how much to move the image relative to its default 0,0 position, not the position of the image within the file. The YouTube icon's x coordinate is 180px so we need to move the image -180px (ie. to the left) to bring the YouTube icon to 0.

You can download the source code and sample images as a zip file or clone/download the Github repository.

Source Code Links

ZIP File
GitHub

This is the source code for spritemaker.py.

spritemaker.py

import os
from pathlib import Path

import PIL
from PIL import Image


def create_sprites(imagepaths, spritefilepath, cssfilepath):

    """
    Creates a sprite image by combining the images in the imagepaths tuple into one image.
    This is saved to spritefilepath.
    Also creates a file of CSS classes saved to cssfilepath.
    The class names are the original image filenames without the filename extension.
    IOErrors are raised.
    """

    size = _calculate_size(imagepaths)

    _create_sprite_image(imagepaths, size, "sprites.png")

    _create_styles(imagepaths, "spritestyles.css", "sprites.png")


def _calculate_size(imagepaths):

    """
    Creates a width/height tuple specifying the size of the image
    needed for the combined images.
    """

    totalwidth = 0
    maxheight = 0

    try:

        for imagepath in imagepaths:

            image = Image.open(imagepath)

            totalwidth += image.width
            maxheight = max(image.height, maxheight)

    except IOError as e:

        raise

    return (totalwidth, maxheight)


def _create_sprite_image(imagepaths, size, spritefilepath):

    """
    Creates a new image and pastes the original images into it,
    then saves it to spritefilepath.
    """

    sprites = PIL.Image.new("RGBA", size, (255,0,0,0))

    x = 0

    try:

        for imagepath in imagepaths:

            image = Image.open(imagepath)

            sprites.paste(image, (x, 0))

            x += image.width

        sprites.save(spritefilepath, compress_level = 9)

    except IOError as e:

        raise


def _create_styles(imagepaths, cssfilepath, spritefilepath):

    """
    Creates a set of CSS classes for the sprite images
    and saves it to spritefilepath.
    """

    styles = []

    x = 0

    try:

        for imagepath in imagepaths:

            image = Image.open(imagepath)

            classname = Path(imagepath).stem

            style = ["."]
            style.append(f"{classname}\n")
            style.append("{\n")

            style.append(f"    background: url('{spritefilepath}') no-repeat;\n")
            style.append(f"    width: {image.width}px;\n")
            style.append(f"    height: {image.height}px;\n")
            style.append("    display: inline-block;\n")
            style.append(f"    background-position: -{x}px 0px;\n")

            style.append("}\n\n")

            x += image.width

            style = "".join(style)

            styles.append(style)

        styles = "".join(styles)

        f = open(cssfilepath, "w+")
        f.write(styles)
        f.close()

    except IOError as e:

        raise

Imports and Pillow

We need a couple of imports for file handling, and also two more for Pillow, the Python imaging library which is used for creating the images. The Pillow usage in this project is very simple and self-explanatory but if you want to learn more I also have a full article called An Introduction to Image Manipulation in Python with Pillow.

create_sprites

This is the sole "public" function in the module which takes the three arguments necessary for the whole process of creating the sprite graphic and CSS, and then calls the three "private" functions to actually do the hard work. The arguments are:

  • imagepaths - a tuple of the individual files
  • spritefilepath - the path to save the combined image to
  • cssfilepath - the path to save the CSS file to

_calculate_size

This function calculates the width and height of the combined image. The width is the sum of the widths of the individual images, and the height is that of the highest individual file. (I made the arbitrary decision to arrange the files horizontally.)

Here we see the first use of Pillow to open images and retrieve their widths and heights.

_create_sprite_image

Firstly we create a new Pillow image of the required size and then iterate the input images, pasting each into the new image at the appropriate x-coordinate. Finally the new image is saved to the specified path; note compress_level = 9 which I'll discuss later on.

_create_styles

Firstly we create a list to hold the CSS classes, and initialize the offset x coordinate to 0. Then we iterate the input images again, creating the individual lines of the class and adding them to another list which is joined to form a single string.

The class list is then also joined into a string which is then saved to the specified path.

Now let's look at spritemakerconsole.py.

spritemakerconsole.py

import spritemaker


def main():

    """
    A simple console application to test the spritemaker module
    """

    imagepaths = ("icons/facebook.png", "icons/github.png", "icons/linkedin.png", "icons/pinterest.png", "icons/twitter.png", "icons/youtube.png")

    try:

        spritemaker.create_sprites(imagepaths, "sprites.png", "spritestyles.css")

    except IOError as e:

        print(e)


main()

This is a very simple program to try out the module, so all it needs to do is create a tuple of paths and call spritemaker.create_sprites.

That's the coding finished so we can run it with this command:

Running the Program

python3.8 spritemakerconsole.py

You will now find the sprite image and CSS file in the locations specified.

Usage Within HTML

To use the sprites all you need do is set the relevant classes on the elements you want to show the images. This is a simple HTML page I put together as an example.

<!DOCTYPE html>

<html>

<head>
    <meta charset="utf-8" />
    <title>CSS Image Sprite Maker</title>
    <link href="spritestyles.css" rel="stylesheet" type="text/css" />
</head>

<body>

    <div>
        <span class="facebook" title="Facebook"></span>
        <span class="twitter" title="Twitter"></span>
        <span class="linkedin" title="LinkedIn"></span>
        <span class="youtube" title="YouTube"></span>
        <span class="pinterest" title="Pinterest"></span>
        <span class="github" title="GitHub"></span>
    </div>

</body>

</html>

The HTML file is included in the ZIP and GitHub repository, and if you open it you'll see this.

css sprites on page

A Few Words About File Sizes

The PNG or Portable Network Graphics format is lossless but as you saw in the code it is possible to specify a compression level when saving files. This affects the file size and speed of encoding/decoding but with no effect on image quality. The compression level is an integer between 0 (no compression, high file size, fast encoding/decoding) and 9 (maximum compression, low file size, slow encoding/decoding). The original files used in this project were all saved with a compression level of 9 as was the output sprite file so that the sizes could be meaningfully compared.

The total file size of the six input files is 5,252 bytes, and the file size of the output file is 4,365 bytes or about 83% of the individual sizes. Every little helps!

What About HTTP/2

While researching this project I found an article on the Mozilla site called Implementing image sprites in CSS. It includes the note "When using HTTP/2, it may in fact be more bandwidth-friendly to use multiple small requests", but fails to explain or expand on this. Looks like I'll need to research this further and possibly write an article on the topic...!

"But I Prefer JavaScript..."

If you prefer JavaScript I have a NodeJS version of this program in the pipeline. Watch this space.