Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

186 lines (144 sloc) 6.376 kb
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
###############################################################################
import os
import re
import hashlib
import Image
import subprocess
from r2.lib.static import generate_static_name
SPRITE_PADDING = 6
sprite_line = re.compile(r"background-image: *url\((.*)\) *.*/\* *SPRITE *(stretch-x)? *\*/")
def optimize_png(filename):
with open(os.path.devnull, 'w') as devnull:
subprocess.check_call(("/usr/bin/optipng", filename), stdout=devnull)
def _extract_css_info(match):
image_filename, properties = match.groups('')
image_filename = image_filename.strip('"\'')
should_stretch = (properties == 'stretch-x')
return image_filename, should_stretch
class SpritableImage(object):
def __init__(self, image, should_stretch=False):
self.image = image
self.stretch = should_stretch
self.filenames = []
@property
def width(self):
return self.image.size[0]
@property
def height(self):
return self.image.size[1]
def stretch_to_width(self, width):
self.image = self.image.resize((width, self.height))
class SpriteBin(object):
def __init__(self, bounding_box):
# the bounding box is a tuple of
# top-left-x, top-left-y, bottom-right-x, bottom-right-y
self.bounding_box = bounding_box
self.offset = 0
self.height = bounding_box[3] - bounding_box[1]
def has_space_for(self, image):
return (self.offset + image.width <= self.bounding_box[2] and
self.height >= image.height)
def add_image(self, image):
image.sprite_location = (self.offset, self.bounding_box[1])
self.offset += image.width + SPRITE_PADDING
def _load_spritable_images(css_filename):
css_location = os.path.dirname(os.path.abspath(css_filename))
images = {}
with open(css_filename, 'r') as f:
for line in f:
m = sprite_line.search(line)
if not m:
continue
image_filename, should_stretch = _extract_css_info(m)
image = Image.open(os.path.join(css_location, image_filename))
image_hash = hashlib.md5(image.convert("RGBA").tostring()).hexdigest()
if image_hash not in images:
images[image_hash] = SpritableImage(image, should_stretch)
else:
assert images[image_hash].stretch == should_stretch
images[image_hash].filenames.append(image_filename)
# Sort images by filename to group the layout by names when possible.
return sorted(images.values(), key=lambda i: i.filenames[0])
def _generate_sprite(images, sprite_path):
sprite_width = max(i.width for i in images)
sprite_height = 0
# put all the max-width and stretch-x images together at the top
small_images = []
for image in images:
if image.width == sprite_width or image.stretch:
if image.stretch:
image.stretch_to_width(sprite_width)
image.sprite_location = (0, sprite_height)
sprite_height += image.height + SPRITE_PADDING
else:
small_images.append(image)
# lay out the remaining images -- done with a greedy algorithm
small_images.sort(key=lambda i: i.height, reverse=True)
bins = []
for image in small_images:
# find a bin to fit in
for bin in bins:
if bin.has_space_for(image):
break
else:
# or give up and create a new bin
bin = SpriteBin((0, sprite_height, sprite_width, sprite_height + image.height))
sprite_height += image.height + SPRITE_PADDING
bins.append(bin)
bin.add_image(image)
# generate the image
sprite_dimensions = (sprite_width, sprite_height)
background_color = (255, 69, 0, 0) # transparent orangered
sprite = Image.new('RGBA', sprite_dimensions, background_color)
for image in images:
sprite.paste(image.image, image.sprite_location)
sprite.save(sprite_path, optimize=True)
optimize_png(sprite_path)
# give back the mangled name
sprite_base, sprite_name = os.path.split(sprite_path)
return generate_static_name(sprite_name, base=sprite_base)
def _rewrite_css(css_filename, sprite_path, images):
# map filenames to coordinates
locations = {}
for image in images:
for filename in image.filenames:
locations[filename] = image.sprite_location
def rewrite_sprite_reference(match):
image_filename, should_stretch = _extract_css_info(match)
position = locations[image_filename]
return ''.join((
'background-image: url(%s);' % sprite_path,
'background-position: -%dpx -%dpx;' % position,
'background-repeat: %s;' % ('repeat' if should_stretch else 'no-repeat'),
))
# read in the css and replace sprite references
with open(css_filename, 'r') as f:
css = f.read()
return sprite_line.sub(rewrite_sprite_reference, css)
def spritify(css_filename, sprite_path):
images = _load_spritable_images(css_filename)
sprite_path = _generate_sprite(images, sprite_path)
return _rewrite_css(css_filename, sprite_path, images)
if __name__ == '__main__':
import sys
print spritify(sys.argv[1], sys.argv[2])
Jump to Line
Something went wrong with that request. Please try again.