7675.507.19
by Edwin Grubbs
More progress. |
1 |
# Copyright 2010 Canonical Ltd. This software is licensed under the
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
2 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
3 |
#
|
|
7675.507.10
by Edwin Grubbs
Added make_master.py |
4 |
# Derived from make_master.py by Oran Looney.
|
5 |
# http://oranlooney.com/make-css-sprites-python-image-library/
|
|
7675.507.9
by Edwin Grubbs
Added make_master.py |
6 |
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
7 |
"""Library to create sprites."""
|
8 |
||
9 |
__metaclass__ = type |
|
10 |
||
11 |
__all__ = [ |
|
12 |
'SpriteUtil', |
|
13 |
]
|
|
14 |
||
7675.507.9
by Edwin Grubbs
Added make_master.py |
15 |
import os |
11403.1.4
by Henning Eggers
Reformatted imports using format-imports script r32. |
16 |
import re |
7675.507.12
by Edwin Grubbs
Fixed pil_sprite_maker.py |
17 |
import sys |
11403.1.4
by Henning Eggers
Reformatted imports using format-imports script r32. |
18 |
from textwrap import dedent |
19 |
||
7675.507.12
by Edwin Grubbs
Fixed pil_sprite_maker.py |
20 |
import cssutils |
7675.507.9
by Edwin Grubbs
Added make_master.py |
21 |
import Image |
7675.507.17
by Edwin Grubbs
Improved spriteutils.py. |
22 |
import simplejson |
7675.507.9
by Edwin Grubbs
Added make_master.py |
23 |
|
7675.507.33
by Edwin Grubbs
Addressed reviewer comments. |
24 |
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
25 |
class SpriteUtil: |
7675.507.28
by Edwin Grubbs
Added warning to top of icon-sprites.positioning |
26 |
EDIT_WARNING = dedent("""\ |
27 |
/* DO NOT EDIT THIS FILE BY HAND!!! */
|
|
28 |
/* It is autogenerated by spriteutils. */
|
|
29 |
""") |
|
30 |
||
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
31 |
def __init__(self, css_template_file, group_name, |
32 |
url_prefix_substitutions=None, margin=150): |
|
33 |
"""Initialize with the specified css template file.
|
|
34 |
||
35 |
:param css_template_file: (str) Name of the file containing
|
|
36 |
css rules with a background-image style that needs to be
|
|
37 |
combined into the sprite file, a comment allowing sprites to
|
|
38 |
be grouped into different image files, and a
|
|
39 |
background-repeat style if necessary. Currently, "repeat-y"
|
|
40 |
is not supported since the file is combined vertically, so
|
|
41 |
repeat-y would show the entire combined image file.
|
|
42 |
||
43 |
Example css template:
|
|
44 |
edit-icon {
|
|
45 |
background-image: url(../edit.png)
|
|
46 |
/* sprite-ref: group1 */
|
|
47 |
}
|
|
48 |
blue-bar {
|
|
49 |
background-image: url(../blue-bar.png)
|
|
50 |
/* sprite-ref: group1 */
|
|
51 |
background-repeat: repeat-x
|
|
52 |
}
|
|
53 |
||
54 |
:param group_name: (str) Only add sprites to the
|
|
55 |
combined image file whose sprite-ref comment in the
|
|
56 |
css template match this group-name.
|
|
57 |
||
58 |
:param url_prefix_substitutions: (dict) The css template
|
|
59 |
will contain references to image files by their url
|
|
60 |
path, but the filesystem path relative to the css
|
|
61 |
template is needed.
|
|
62 |
||
63 |
:param margin: (int) The number of pixels between each sprite.
|
|
64 |
Be aware that Firefox will ignore extremely large images,
|
|
65 |
for example 64x34000 pixels.
|
|
66 |
||
67 |
If the css_template_file has been modified, a new
|
|
68 |
css file using an existing combined image and positioning
|
|
69 |
file can be generated using:
|
|
70 |
sprite_util = SpriteUtil(...)
|
|
71 |
sprite_util.loadPositioning(...)
|
|
72 |
sprite_util.saveConvertedCSS(...)
|
|
73 |
||
74 |
If a new image file needs to be added to the combined image
|
|
75 |
and the positioning file, they can be regenerated with:
|
|
76 |
sprite_util = SpriteUtil(...)
|
|
77 |
sprite_util.combineImages(...)
|
|
78 |
sprite_util.savePNG(...)
|
|
79 |
sprite_util.savePositioning(...)
|
|
80 |
||
81 |
If the image file is regenerated any time the css file is
|
|
82 |
regenerated, then the step for saving and loading the positioning
|
|
83 |
information could be removed. For example:
|
|
84 |
sprite_util = SpriteUtil(...)
|
|
85 |
sprite_util.combineImages(...)
|
|
86 |
sprite_util.savePNG(...)
|
|
87 |
sprite_util.saveConvertedCSS(...)
|
|
88 |
"""
|
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
89 |
self.combined_image = None |
90 |
self.positions = None |
|
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
91 |
self.group_name = group_name |
92 |
self.margin = margin |
|
93 |
self._loadCSSTemplate( |
|
94 |
css_template_file, group_name, url_prefix_substitutions) |
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
95 |
|
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
96 |
def _loadCSSTemplate(self, css_template_file, group_name, |
7675.507.19
by Edwin Grubbs
More progress. |
97 |
url_prefix_substitutions=None): |
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
98 |
"""See `__init__`."""
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
99 |
smartsprites_exp = re.compile( |
100 |
r'/\*+([^*]*sprite-ref: [^*]*)\*/') |
|
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
101 |
self.css_object = cssutils.parseFile(css_template_file) |
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
102 |
self.sprite_info = [] |
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
103 |
for rule in self.css_object: |
7675.507.17
by Edwin Grubbs
Improved spriteutils.py. |
104 |
if rule.cssText is None: |
105 |
continue
|
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
106 |
match = smartsprites_exp.search(rule.cssText) |
107 |
if match is not None: |
|
108 |
smartsprites_info = match.group(1) |
|
7675.507.19
by Edwin Grubbs
More progress. |
109 |
parameters = self._parseCommentParameters(match.group(1)) |
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
110 |
# Currently, only a single combined image is supported.
|
7675.507.19
by Edwin Grubbs
More progress. |
111 |
if parameters['sprite-ref'] == group_name: |
112 |
filename = self._getSpriteImagePath( |
|
113 |
rule, url_prefix_substitutions) |
|
10322.1.1
by Edwin Grubbs
Fixed missing .branch sprite. Fixed missing .cves class. |
114 |
if filename == '': |
115 |
raise AssertionError( |
|
116 |
"Missing background-image url for %s css style" |
|
117 |
% rule.selectorText) |
|
7675.507.32
by Edwin Grubbs
Fixed lint error. |
118 |
self.sprite_info.append( |
119 |
dict(filename=filename, rule=rule)) |
|
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
120 |
|
121 |
if len(self.sprite_info) == 0: |
|
122 |
raise AssertionError( |
|
123 |
"No sprite-ref comments for group %r found" % group_name) |
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
124 |
|
7675.507.19
by Edwin Grubbs
More progress. |
125 |
def _getSpriteImagePath(self, rule, url_prefix_substitutions=None): |
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
126 |
"""Convert the url path to a filesystem path."""
|
7675.507.19
by Edwin Grubbs
More progress. |
127 |
# Remove url() from string.
|
128 |
filename = rule.style.backgroundImage[4:-1] |
|
129 |
# Convert urls to paths relative to the css
|
|
130 |
# file, e.g. '/@@/foo.png' => '../images/foo.png'.
|
|
131 |
if url_prefix_substitutions is not None: |
|
132 |
for old, new in url_prefix_substitutions.items(): |
|
133 |
if filename.startswith(old): |
|
134 |
filename = new + filename[len(old):] |
|
135 |
return filename |
|
136 |
||
137 |
def _parseCommentParameters(self, parameter_string): |
|
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
138 |
"""Parse parameters out of javascript comments.
|
139 |
||
140 |
Currently only used for the group name specified
|
|
141 |
by "sprite-ref".
|
|
142 |
"""
|
|
7675.507.19
by Edwin Grubbs
More progress. |
143 |
results = {} |
144 |
for parameter in parameter_string.split(';'): |
|
145 |
if parameter.strip() != '': |
|
146 |
name, value = parameter.split(':') |
|
147 |
name = name.strip() |
|
148 |
value = value.strip() |
|
149 |
results[name] = value |
|
150 |
return results |
|
151 |
||
7675.507.17
by Edwin Grubbs
Improved spriteutils.py. |
152 |
def combineImages(self, css_dir): |
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
153 |
"""Copy all the sprites into a single PIL image."""
|
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
154 |
|
155 |
# Open all the sprite images.
|
|
156 |
sprite_images = {} |
|
157 |
max_sprite_width = 0 |
|
158 |
total_sprite_height = 0 |
|
159 |
for sprite in self.sprite_info: |
|
160 |
abs_filename = os.path.join(css_dir, sprite['filename']) |
|
10322.1.1
by Edwin Grubbs
Fixed missing .branch sprite. Fixed missing .cves class. |
161 |
try: |
162 |
sprite_images[sprite['filename']] = Image.open(abs_filename) |
|
163 |
except IOError: |
|
164 |
print >> sys.stderr, "Error opening '%s' for %s css rule" % ( |
|
165 |
abs_filename, sprite['rule'].selectorText) |
|
166 |
raise
|
|
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
167 |
width, height = sprite_images[sprite['filename']].size |
168 |
max_sprite_width = max(width, max_sprite_width) |
|
169 |
total_sprite_height += height |
|
170 |
||
171 |
# The combined image is the height of all the sprites
|
|
172 |
# plus the margin between each of them.
|
|
7675.507.23
by Edwin Grubbs
Completely working. |
173 |
combined_image_height = ( |
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
174 |
total_sprite_height + (self.margin * len(self.sprite_info) - 1)) |
175 |
transparent = (0, 0, 0, 0) |
|
7675.507.23
by Edwin Grubbs
Completely working. |
176 |
combined_image = Image.new( |
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
177 |
mode='RGBA', |
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
178 |
size=(max_sprite_width, combined_image_height), |
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
179 |
color=transparent) |
180 |
||
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
181 |
# Paste each sprite into the combined image.
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
182 |
y = 0 |
183 |
positions = {} |
|
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
184 |
for index, sprite in enumerate(self.sprite_info): |
185 |
sprite_image = sprite_images[sprite['filename']] |
|
7675.507.23
by Edwin Grubbs
Completely working. |
186 |
try: |
7675.507.26
by Edwin Grubbs
Handle repeat-x for sprites. |
187 |
position = [0, y] |
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
188 |
combined_image.paste(sprite_image, tuple(position)) |
7675.507.26
by Edwin Grubbs
Handle repeat-x for sprites. |
189 |
# An icon in a vertically combined image can be repeated
|
190 |
# horizontally, but we have to repeat it in the combined
|
|
191 |
# image so that we don't repeat white space.
|
|
192 |
if sprite['rule'].style.backgroundRepeat == 'repeat-x': |
|
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
193 |
width = sprite_image.size[0] |
194 |
for x_position in range(width, max_sprite_width, width): |
|
7675.507.26
by Edwin Grubbs
Handle repeat-x for sprites. |
195 |
position[0] = x_position |
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
196 |
combined_image.paste(sprite_image, tuple(position)) |
7675.507.23
by Edwin Grubbs
Completely working. |
197 |
except: |
198 |
print >> sys.stderr, ( |
|
199 |
"Error with image file %s" % sprite['filename']) |
|
200 |
raise
|
|
7675.507.19
by Edwin Grubbs
More progress. |
201 |
# This is the position of the combined image on an HTML
|
202 |
# element. Therefore, it subtracts the position of the
|
|
203 |
# sprite in the file to move it to the top of the element.
|
|
204 |
positions[sprite['filename']] = (0, -y) |
|
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
205 |
y += sprite_image.size[1] + self.margin |
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
206 |
|
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
207 |
# If there is an exception earlier, these attributes will remain None.
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
208 |
self.positions = positions |
7675.507.23
by Edwin Grubbs
Completely working. |
209 |
self.combined_image = combined_image |
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
210 |
|
211 |
def savePNG(self, filename): |
|
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
212 |
"""Save the PIL image object to disk."""
|
7675.507.23
by Edwin Grubbs
Completely working. |
213 |
self.combined_image.save(filename, format='png', optimize=True) |
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
214 |
|
215 |
def savePositioning(self, filename): |
|
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
216 |
"""Save the positions of sprites in the combined image.
|
217 |
||
218 |
This allows the final css to be generated after making
|
|
219 |
changes to the css template without recreating the combined
|
|
220 |
image file.
|
|
221 |
"""
|
|
7675.507.28
by Edwin Grubbs
Added warning to top of icon-sprites.positioning |
222 |
fp = open(filename, 'w') |
223 |
fp.write(self.EDIT_WARNING) |
|
224 |
simplejson.dump(self.positions, fp=fp, indent=4) |
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
225 |
|
226 |
def loadPositioning(self, filename): |
|
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
227 |
"""Load file with the positions of sprites in the combined image."""
|
7675.507.28
by Edwin Grubbs
Added warning to top of icon-sprites.positioning |
228 |
json = open(filename).read() |
229 |
# Remove comments from the beginning of the file.
|
|
230 |
start = json.index('{') |
|
231 |
json = json[start:] |
|
232 |
self.positions = simplejson.loads(json) |
|
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
233 |
|
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
234 |
def saveConvertedCSS(self, css_file, combined_image_url_path): |
235 |
"""Generate new css from the template and the positioning info.
|
|
236 |
||
237 |
Example css template:
|
|
238 |
background-image: url(../edit.png); /* sprite-ref: group1 */
|
|
239 |
Example css output:
|
|
240 |
background-image: url(combined_image_url_path)
|
|
241 |
background-position: 0px 2344px
|
|
242 |
"""
|
|
7675.507.30
by Edwin Grubbs
Added tests. Moved to lp.services. |
243 |
for sprite in self.sprite_info: |
7675.507.10
by Edwin Grubbs
Added make_master.py |
244 |
rule = sprite['rule'] |
7675.507.29
by Edwin Grubbs
Added docstrings and moved more code into SpriteUtil.__init__(). |
245 |
rule.style.backgroundImage = 'url(%s)' % combined_image_url_path |
7675.507.17
by Edwin Grubbs
Improved spriteutils.py. |
246 |
position = self.positions[sprite['filename']] |
7675.507.18
by Edwin Grubbs
Updated. |
247 |
rule.style.backgroundPosition = '%dpx %dpx' % tuple(position) |
7675.507.16
by Edwin Grubbs
Refactored into spriteutils.py |
248 |
|
7675.507.20
by Edwin Grubbs
Generating css correctly. |
249 |
with open(css_file, 'w') as fp: |
7675.507.28
by Edwin Grubbs
Added warning to top of icon-sprites.positioning |
250 |
fp.write(self.EDIT_WARNING) |
7675.507.20
by Edwin Grubbs
Generating css correctly. |
251 |
fp.write(self.css_object.cssText) |