QDCSS : Quick and Dirty CSS Sprites

qdcss
Ah, spriting.

When I think about sprites, my mind goes directly to the NES, and SNES era.

snests4nintendones

The graphics were meshed together in a way that was easier to handle than multiple images. And well, now they have this nostalgic appeal. I can’t watch this without a smile.

Luigi Sprite from Super Mario Bros.

Luigi Sprite from Super Mario Bros.

As old as 10 years ago (already? :-O) A list apart published an amazing article on how to use this same idea to reduce the amount of browser petitions for images. That article is wonderful, but apart (hehe) from that, it urges people to think creatively!

Long story short, this is going to be a post on how to create a CSS sprite image and stylesheet with 100 lines of python.

CSS sprite generator

You may say: “This is boring, just send me to the code.“. It is done 😀 if not, just keep going.

There’s a plethora of sprite generators. This is not new terrain for anyone.

It’s not even new terrain for me. I’ve recently been working again with web applications. And wanted to transform old CSS plain images to CSS sprites. For that I opened my old TECHTEST directory, blown away the dust, and find for qdcss (quick and dirty css sprites). My memory was not bad at all, and in no time, this old python script appeared.

QDCSS

What follows is a simple code I wrote after reading the aforementioned article, back in mid 2000’s. It is a very simple script I used while at my first job, I rewrapped it a little (just a little, no big changes) and it will be dissected in this post.

HEADER

"""
    Generate sprites image and CSS code to go with it

    It does not take input. assumes the following.

    There's a directory "img" where this script resides
    with the different images to be sprited.
"""

So, what I understand is that I just have to run it.
python qdcss.py
And the magic will just happen.

LIBRARIES

from PIL import Image
from os import listdir
from os.path import isfile, join
import string

It pulls in various basic libraries. The only fancy one being PIL, the Python Imaging Library to manipulate images.

HELPER FUNCTIONS

I’m happy to see that “past me” liked to document what the functions did. The following are the helper functions used by the script.

def getAllFiles(path)
def isImage(fpath)
def validname(fname)
def widest(paths)
def highest(paths)
def baseCss(filename)
def imgCss(name, image, box)

getAllFiles and isImage work together. The first one uses list comprehensions to generate a list with the names of images in a path.
The second one, uses the exception thrown by PIL when we try to create an image with a file that’s not an image.

def getAllFiles(path):
    """Obtain all the image files under a path"""
    return [f for f in listdir(path) if isImage(join(path,f))]

def isImage(fpath):
    """Checks if a file path corresponds to a image file"""
    try:
        im = Image.open(fpath)
        return True
    except:
        return False

widest & highest get for a list of paths (assumed to be image paths, ugh, big assumption, I would go with a list of images). From those images it will return the biggest width, and biggest height.
Both this sizes are used to generate the final sprite image size.

This is done in one line, with the aid of list comprehensions and the max function. From the provided paths it creates a list of images with Image.open(path). What it really does is create a list of integers, getting the size from each image Image.open(path).size[1] or Image.open(path).size[0].
From this generated list, we get the max value. A single line with a lot of information, but quite clear to read and understand, that’s why people love python.

def widest(paths):
    """Return the width of the widest image"""
    return max([Image.open(join(PATH,fname)).size[1] for fname in paths])

def highest(paths):
    """Return the height of the highest image"""
    return max([Image.open(join(PATH,fname)).size[1] for fname in paths])

Then, we have baseCss and imgCss. Both those functions are related to generating the CSS for the sprite. The first one will create a general class pointing to the sprite file. The other one generates CSS for an input image.

This works with the basis that when we want to use an image, we’ll use two classes. One representing which sprite to use, and the other one representing the coordinates within that sprite. Other generators use a single class approach.

def baseCss(filename):
    """Generate the base CSS with the image"""
    class_name = validname(filename)
    file_path  = join(PATH, filename)
    
    return """.%s_SPRITE {
    background-image: url("%s");
    background-repeat: no-repeat;
    display: block;
}
""" % (class_name, file_path)
    
def imgCss(name, image, box):
    """
        Returns the css for that image
        the background image is a general property
        for all our images.

        name: name of the sprite
        image: PIL Image object
        box: (Top, left) position of the image in the sprite
    """
    class_name = validname(name)
    width = "%spx" % image.size[0]
    height = "%spx" % image.size[1]
    position = "%spx %spx" % (-box[0], -box[1])
    
    return """.sprite-%s {
    width: %s;
    height: %s;
    background-position:%s;
}""" % (class_name, width, height, position)

Finally, the boring function validname simply removes all non accepted characters. It would be nice to improve, since qdcss outputs ugly names like sprite-imagewhategerpng. We could at least strip the file extension.

def validname(fname):
    """Return a valid name"""
    valid_chars = "-_%s%s" % (string.ascii_letters, string.digits)
    return "".join(c for c in fname if c in valid_chars)

That’s it, no more functions are used in this generator.

MAIN CODE

The main code is not wrapped within a main function (ohh, it is a plain script).

First things first, it defines a global PATH variable pointing to the directory “css”.

From that path it gets all the files (and prints the list, so you know more or less what is happening). Using that that list it will obtain the biggest height value and the biggest width value.

files = getAllFiles(PATH)
print files

W = widest(files)
H = widest(files)

The generated sprite is a long horizontal image. There’s no fancy positioning algorithm here (remember, it’s quick and dirty). In a spur of originality, the output file is named sprite.png.

The base CSS is also set here. If you check the original function, you’ll see that it jsut uses the global PATH variable.

#CREATE THE NEW IMAGE
sprite = Image.new("RGBA", (W * len(files) ,H))
sname = "sprite.png"
css = baseCss(sname)

Once the image is generated. We just have to: loop over all the images, load them and copy the contents into the new image. The script uses the paste method.

At each iteration, we have to generate the box (x,y) where the image will be pasted. Since this is a long horizontal image treated as a grid, the box is always i * max_width.

The css string keeps growing with the generation of each looped image css.

for i,file in enumerate(files):
    im = Image.open(join(PATH,file))
    box = (i*W,0)
    sprite.paste(im,box)
    css += imgCss(file,im,box)

After that loop, everything is set, everything is done. Only one thing left to do, store the results. Again, the CSS file is brilliantly named sprites.css.

with open("sprite.css","wb+") as f:
    f.write(css)

Full Code

For copy paste purposes.

"""
    Generate sprites image and CSS code to go with it

    It does not take input. assumes the following.

    There's a directory "img" where this script resides
    with the different images to be sprited.
"""

from PIL import Image
from os import listdir
from os.path import isfile, join
import string

PATH = "img"
CSS = ""

def getAllFiles(path):
    """Obtain all the image files under a path"""
    return [f for f in listdir(path) if isImage(join(path,f))]

def isImage(fpath):
    """Checks if a file path corresponds to a image file"""
    try:
        im = Image.open(fpath)
        return True
    except:
        return False
    
def validname(fname):
    """Return a valid name"""
    valid_chars = "-_%s%s" % (string.ascii_letters, string.digits)
    return "".join(c for c in fname if c in valid_chars)

def widest(paths):
    """Return the width of the widest image"""
    return max([Image.open(join(PATH,fname)).size[1] for fname in paths])

def highest(paths):
    """Return the height of the highest image"""
    return max([Image.open(join(PATH,fname)).size[1] for fname in paths])

def baseCss(filename):
    """Generate the base CSS with the image"""
    class_name = validname(filename)
    file_path  = join(PATH, filename)
    
    return """.%s_SPRITE {
    background-image: url("%s");
    background-repeat: no-repeat;
    display: block;
}
""" % (class_name, file_path)
    
def imgCss(name, image, box):
    """
        Returns the css for that image
        the background image is a general property
        for all our images.

        name: name of the sprite
        image: PIL Image object
        box: (Top, left) position of the image in the sprite
    """
    class_name = validname(name)
    width = "%spx" % image.size[0]
    height = "%spx" % image.size[1]
    position = "%spx %spx" % (-box[0], -box[1])
    
    return """.sprite-%s {
    width: %s;
    height: %s;
    background-position:%s;
}""" % (class_name, width, height, position)

files = getAllFiles(PATH)
print files

W = widest(files)
H = widest(files)

#CREATE THE NEW IMAGE
sprite = Image.new("RGBA", (W * len(files) ,H))
sname = "sprite.png"
css = baseCss(sname)

for i,file in enumerate(files):
    im = Image.open(join(PATH,file))
    box = (i*W,0)
    sprite.paste(im,box)
    print i,im.size
    print imgCss(file,im,box)
    css += imgCss(file,im,box)

sprite.save(sprite_path, "png")

with open("sprite.css","wb+") as f:
    f.write(css)

Conclusion

It is done! We have a CSS pointing to a single image file, and different classes to show the different images.

It could be fancier, it could use a fancy layout algorithm, or another fancy algorithm. fancy fancy fancy

I would also add some command line flags. Like for example -o to direct the output, or -i for the input.

But at the end, you know what? for that I have glue, and I barely used it. This is my go-to base code to generate the sprites. It is easy to understand, easy to fix, direct to use.

Kudos, to past me :-D, and an applause to A list apart that keeps providing very nice articles.

Advertisements

Leave a comment

Filed under code, tools

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s