Skip to content

Instantly share code, notes, and snippets.

@mbarkhau
Created May 19, 2011 07:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbarkhau/980354 to your computer and use it in GitHub Desktop.
Save mbarkhau/980354 to your computer and use it in GitHub Desktop.
Makes a sprite and css file for png images in a directory.
#!/usr/bin/env python
import os, sys
import subprocess
import Image
from collections import namedtuple
min_css = lambda (s): s.replace(" ", "").replace("\n", "") + "\n"
BASE_CSS = min_css("""
.sprite
{
display:inline-block;
overflow:hidden;
text-align: left !important;
text-indent: -99999px;
background: url(%(url)s) no-repeat;
}
""")
ICON_CSS = min_css("""
.s_%(name)s
{
width:%(w)dpx;
height:%(h)dpx;
background-position:%(x)dpx %(y)dpx;
}
""")
class Pos(object):
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return "Pos(x=" + str(self.x) + ", y=" + str(self.y) + ")"
class Rect(object):
def __init__(self, x=0, y=0, w=0, h=0):
self.x = x
self.y = y
self.w = w
self.h = h
@property
def area(self):
return self.w * self.h
@property
def right(self):
return self.x + self.w
@property
def bottom(self):
return self.y + self.h
def has_overlap(self, other):
return not (
other.x > self.right or self.x > other.right or
other.y > self.bottom or self.y > other.bottom
)
class ImgRect(Rect):
def __init__(self, img, name):
self.img = img
self.name = name
self.x = self.y = 0
self.w, self.h = img.size
def area_key(i):
return i.area
def width_key(i):
return i.w
def height_key(i):
return i.h
def read_image(path):
img_name = path.split("/")[-1].split('.')[0]
img = Image.open(path)
return ImgRect(img, img_name)
def generate(path, media_path='/img/', sprite_file='sprites.png', css_file='sprites.css'):
""" Creates a sprite and css file in the specified directory
path : The filesystem path to your media files
media_path : The url from which the media files are served
(used in generated css file)
sprite_file : The name of the sprite image (placed in path)
css_file : The name of the css file (placed in path)
"""
if not os.path.exists(path):
print "No such directory: %s" % path
return
path = os.path.abspath(path)
sprite_url = media_path + sprite_file
sprite_file = os.path.join(path, sprite_file)
css_file = os.path.join(path, css_file)
files = os.listdir(path)
def is_image_file(f):
return (
not sprite_file.endswith(f) and (
f.endswith('png') or
f.endswith('jpg') or
f.endswith('jpeg') or
f.endswith('gif')
)
)
image_paths = [os.path.join(path, f) for f in files if is_image_file(f)]
if not image_paths:
print "No images found in %s" % path
return
print "compiling %d icons" % len(image_paths)
images = [read_image(p) for p in image_paths]
def sprite_size(images):
"""Returns the dimensions that can contain all of the images"""
max_x = 0
max_y = 0
for img in images:
max_x = max(max_x, img.right)
max_y = max(max_y, img.bottom)
return [max_x, max_y]
def expansion(img, pos, frame):
"""returns: extra area required, if the img uses pos"""
p_right = pos.x + img.w
p_bottom = pos.y + img.h
exp = 0
if p_right > frame.w:
exp += (p_right - frame.w) * frame.h
if p_bottom > frame.h:
exp += (p_bottom - frame.h) * frame.w
return exp
def select_pos(img, positions, frame):
"""select pos which expands frame the least"""
min_pos = None
min_exp = 0
for pos in positions:
exp = expansion(img, pos, frame)
# is_inward = pos.x < min_pos.x or pos.y < min_pos.y
# or exp == 0 and is_inward
if not min_pos or min_exp > exp:
min_pos = pos
min_exp = exp
return min_pos
def position_img(pos, img, frame):
"""update relevant data when positioning img in frame"""
img.x = pos.x
img.y = pos.y
frame.w = max(frame.w, img.right)
frame.h = max(frame.h, img.bottom)
def new_positions(pos, img, positions, positioned):
positioned.append(img)
positions.remove(pos)
r = Rect()
r.w = img.w
r.h = img.h
new_positions = []
for p in positions:
r.x = p.x
r.y = p.y
if not img.has_overlap(r):
new_positions.append(p)
def has_overlap(pos):
r.w = 1000
r.h = 1000
r.x = pos.x
r.y = pos.y
for p_img in positioned:
if p_img.has_overlap(r):
return True
return False
below = Pos(img.right+1, img.y)
right = Pos(img.x, img.bottom+1)
if not has_overlap(below):
new_positions.append(below)
if not has_overlap(right):
new_positions.append(right)
return new_positions
def pack(images):
"""Set x,y coordinates for images, such that they fit in a sprit
returns rect with dimensions for sprite
"""
frame = Rect(0, 0, 0, 0)
positions = [ Pos(0, 0) ]
positioned = []
for img in images:
pos = select_pos(img, positions, frame)
position_img(pos, img, frame)
positions = new_positions(pos, img, positions, positioned)
return frame
def gen_sprite(images, frame):
sprite_img = Image.new(mode='RGBA', size=(frame.w, frame.h))
for img in images:
sprite_img.paste(img.img, (img.x, img.y))
sprite_img.save(sprite_file)
cmd = "optipng -q %s" % sprite_file
ret_code = subprocess.call(cmd, shell=True)
if ret_code == 127:
print "You should install 'optipng'."
elif ret_code == 0:
print "created %s" % sprite_file
def gen_css(images, frame):
icon_css = []
for img in images:
arg = {
'name': img.name,
'x': img.x,
'y': img.y,
'w': img.w,
'h': img.h
}
icon_css.append(ICON_CSS % arg)
with open(css_file, 'w') as css:
arg = { 'url': sprite_url, 'w': frame.w, 'h': frame.h }
css.write(BASE_CSS % arg)
css.write("".join(icon_css))
print "created %s" % css_file
images.sort(key=area_key, reverse=True)
frame = pack(images)
gen_sprite(images, frame)
gen_css(images, frame)
USAGE = """Usage:
1. Put the images you want to sprite in a directory
2. Run simple_sprite.py in that directory/pass it the path
3. profit...
> simple_sprite.py /path/to/images
compiling 22 icons
created /path/to/images/sprites.png
created /path/to/images/sprites.css
You may need to modify background-image:url(/img/sprites.png)
in sprites.css depending on your webserver configuration.
In your page you can use the images like so:
<span class="sprite s_{{example_icon}}></span> # ommit .png
Depends on PIL (python image library)
You should also install optipng
Works only with .png, .jpg, and .gif files
"""
def main(args):
if len(args) == 1:
path = '.'
else:
path = args[1]
if not path or path in ('-h', '--help'):
print USAGE
else:
generate(path)
if __name__ == '__main__':
try:
main(sys.argv)
except Exception, e:
print e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment