Ah, spriting.
When I think about sprites, my mind goes directly to the NES, and SNES era.
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.
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.
- glue (python)
- spritegen (web based)
- csssprites.com (web based)
- SmartSprite (php)
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.