/r2/r2/lib/nymph.py
Python | 185 lines | 157 code | 7 blank | 21 comment | 1 complexity | 2c5dfb82e53b4b885ea751ef8812ba6d MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, Apache-2.0
- # 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-2012 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 = 1
- sprite_line = re.compile(r"background-image: *url\((.*)\) *.*/\* *SPRITE *(stretch-x)? *\*/")
- def optimize_png(filename, optimizer='/usr/bin/env optipng'):
- with open(os.path.devnull, 'w') as devnull:
- subprocess.check_call(' '.join((optimizer, filename)), shell=True, 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])