PageRenderTime 74ms CodeModel.GetById 41ms RepoModel.GetById 1ms app.codeStats 0ms

/r2/r2/lib/nymph.py

https://github.com/stevewilber/reddit
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
  1. # The contents of this file are subject to the Common Public Attribution
  2. # License Version 1.0. (the "License"); you may not use this file except in
  3. # compliance with the License. You may obtain a copy of the License at
  4. # http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
  5. # License Version 1.1, but Sections 14 and 15 have been added to cover use of
  6. # software over a computer network and provide for limited attribution for the
  7. # Original Developer. In addition, Exhibit A has been modified to be consistent
  8. # with Exhibit B.
  9. #
  10. # Software distributed under the License is distributed on an "AS IS" basis,
  11. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
  12. # the specific language governing rights and limitations under the License.
  13. #
  14. # The Original Code is reddit.
  15. #
  16. # The Original Developer is the Initial Developer. The Initial Developer of
  17. # the Original Code is reddit Inc.
  18. #
  19. # All portions of the code written by reddit are Copyright (c) 2006-2012 reddit
  20. # Inc. All Rights Reserved.
  21. ###############################################################################
  22. import os
  23. import re
  24. import hashlib
  25. import Image
  26. import subprocess
  27. from r2.lib.static import generate_static_name
  28. SPRITE_PADDING = 1
  29. sprite_line = re.compile(r"background-image: *url\((.*)\) *.*/\* *SPRITE *(stretch-x)? *\*/")
  30. def optimize_png(filename, optimizer='/usr/bin/env optipng'):
  31. with open(os.path.devnull, 'w') as devnull:
  32. subprocess.check_call(' '.join((optimizer, filename)), shell=True, stdout=devnull)
  33. def _extract_css_info(match):
  34. image_filename, properties = match.groups('')
  35. image_filename = image_filename.strip('"\'')
  36. should_stretch = (properties == 'stretch-x')
  37. return image_filename, should_stretch
  38. class SpritableImage(object):
  39. def __init__(self, image, should_stretch=False):
  40. self.image = image
  41. self.stretch = should_stretch
  42. self.filenames = []
  43. @property
  44. def width(self):
  45. return self.image.size[0]
  46. @property
  47. def height(self):
  48. return self.image.size[1]
  49. def stretch_to_width(self, width):
  50. self.image = self.image.resize((width, self.height))
  51. class SpriteBin(object):
  52. def __init__(self, bounding_box):
  53. # the bounding box is a tuple of
  54. # top-left-x, top-left-y, bottom-right-x, bottom-right-y
  55. self.bounding_box = bounding_box
  56. self.offset = 0
  57. self.height = bounding_box[3] - bounding_box[1]
  58. def has_space_for(self, image):
  59. return (self.offset + image.width <= self.bounding_box[2] and
  60. self.height >= image.height)
  61. def add_image(self, image):
  62. image.sprite_location = (self.offset, self.bounding_box[1])
  63. self.offset += image.width + SPRITE_PADDING
  64. def _load_spritable_images(css_filename):
  65. css_location = os.path.dirname(os.path.abspath(css_filename))
  66. images = {}
  67. with open(css_filename, 'r') as f:
  68. for line in f:
  69. m = sprite_line.search(line)
  70. if not m:
  71. continue
  72. image_filename, should_stretch = _extract_css_info(m)
  73. image = Image.open(os.path.join(css_location, image_filename))
  74. image_hash = hashlib.md5(image.convert("RGBA").tostring()).hexdigest()
  75. if image_hash not in images:
  76. images[image_hash] = SpritableImage(image, should_stretch)
  77. else:
  78. assert images[image_hash].stretch == should_stretch
  79. images[image_hash].filenames.append(image_filename)
  80. # Sort images by filename to group the layout by names when possible.
  81. return sorted(images.values(), key=lambda i: i.filenames[0])
  82. def _generate_sprite(images, sprite_path):
  83. sprite_width = max(i.width for i in images)
  84. sprite_height = 0
  85. # put all the max-width and stretch-x images together at the top
  86. small_images = []
  87. for image in images:
  88. if image.width == sprite_width or image.stretch:
  89. if image.stretch:
  90. image.stretch_to_width(sprite_width)
  91. image.sprite_location = (0, sprite_height)
  92. sprite_height += image.height + SPRITE_PADDING
  93. else:
  94. small_images.append(image)
  95. # lay out the remaining images -- done with a greedy algorithm
  96. small_images.sort(key=lambda i: i.height, reverse=True)
  97. bins = []
  98. for image in small_images:
  99. # find a bin to fit in
  100. for bin in bins:
  101. if bin.has_space_for(image):
  102. break
  103. else:
  104. # or give up and create a new bin
  105. bin = SpriteBin((0, sprite_height, sprite_width, sprite_height + image.height))
  106. sprite_height += image.height + SPRITE_PADDING
  107. bins.append(bin)
  108. bin.add_image(image)
  109. # generate the image
  110. sprite_dimensions = (sprite_width, sprite_height)
  111. background_color = (255, 69, 0, 0) # transparent orangered
  112. sprite = Image.new('RGBA', sprite_dimensions, background_color)
  113. for image in images:
  114. sprite.paste(image.image, image.sprite_location)
  115. sprite.save(sprite_path, optimize=True)
  116. optimize_png(sprite_path)
  117. # give back the mangled name
  118. sprite_base, sprite_name = os.path.split(sprite_path)
  119. return generate_static_name(sprite_name, base=sprite_base)
  120. def _rewrite_css(css_filename, sprite_path, images):
  121. # map filenames to coordinates
  122. locations = {}
  123. for image in images:
  124. for filename in image.filenames:
  125. locations[filename] = image.sprite_location
  126. def rewrite_sprite_reference(match):
  127. image_filename, should_stretch = _extract_css_info(match)
  128. position = locations[image_filename]
  129. return ''.join((
  130. 'background-image: url(%s);' % sprite_path,
  131. 'background-position: -%dpx -%dpx;' % position,
  132. 'background-repeat: %s;' % ('repeat' if should_stretch else 'no-repeat'),
  133. ))
  134. # read in the css and replace sprite references
  135. with open(css_filename, 'r') as f:
  136. css = f.read()
  137. return sprite_line.sub(rewrite_sprite_reference, css)
  138. def spritify(css_filename, sprite_path):
  139. images = _load_spritable_images(css_filename)
  140. sprite_path = _generate_sprite(images, sprite_path)
  141. return _rewrite_css(css_filename, sprite_path, images)
  142. if __name__ == '__main__':
  143. import sys
  144. print spritify(sys.argv[1], sys.argv[2])