1
"""Image utilities ported from Compass."""
3
from __future__ import absolute_import
4
from __future__ import print_function
14
from six.moves import xrange
16
from scss import config
17
from scss.functions.compass import _image_size_cache
18
from scss.functions.compass.helpers import add_cache_buster
19
from scss.functions.library import FunctionLibrary
20
from scss.types import Color, List, Number, String
21
from scss.util import escape
31
log = logging.getLogger(__name__)
33
COMPASS_IMAGES_LIBRARY = FunctionLibrary()
34
register = COMPASS_IMAGES_LIBRARY.register
37
# ------------------------------------------------------------------------------
40
return config.STATIC_ROOT if config.IMAGES_ROOT is None else config.IMAGES_ROOT
43
def _image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, inline=False, mime_type=None, spacing=None, collapse_x=None, collapse_y=None):
45
src_color - a list of or a single color to be replaced by each corresponding dst_color colors
46
spacing - spaces to be added to the image
47
collapse_x, collapse_y - collapsable (layered) image of the given size (x, y)
49
if inline or dst_color or spacing:
51
raise Exception("Images manipulation require PIL")
52
filepath = String.unquoted(path).value
53
fileext = os.path.splitext(filepath)[1].lstrip('.').lower()
55
mime_type = String.unquoted(mime_type).value
57
mime_type = mimetypes.guess_type(filepath)[0]
59
mime_type = 'image/%s' % fileext
61
IMAGES_ROOT = _images_root()
62
if callable(IMAGES_ROOT):
64
_file, _storage = list(IMAGES_ROOT(filepath))[0]
65
d_obj = _storage.modified_time(_file)
66
filetime = int(time.mktime(d_obj.timetuple()))
67
if inline or dst_color or spacing:
68
path = _storage.open(_file)
72
_path = os.path.join(IMAGES_ROOT.rstrip('/'), filepath.strip('/'))
73
if os.path.exists(_path):
74
filetime = int(os.path.getmtime(_path))
75
if inline or dst_color or spacing:
76
path = open(_path, 'rb')
80
BASE_URL = config.IMAGES_URL or config.STATIC_URL
82
dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_color) if v]
84
src_color = Color.from_name('black') if src_color is None else src_color
85
src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_color)]
87
len_colors = max(len(dst_colors), len(src_colors))
88
dst_colors = (dst_colors * len_colors)[:len_colors]
89
src_colors = (src_colors * len_colors)[:len_colors]
91
spacing = Number(0) if spacing is None else spacing
92
spacing = [int(Number(v).value) for v in List.from_maybe(spacing)]
93
spacing = (spacing * 4)[:4]
95
file_name, file_ext = os.path.splitext(os.path.normpath(filepath).replace('\\', '_').replace('/', '_'))
96
key = (filetime, src_color, dst_color, spacing)
97
key = file_name + '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
98
asset_file = key + file_ext
99
ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets')
100
asset_path = os.path.join(ASSETS_ROOT, asset_file)
102
if os.path.exists(asset_path):
103
filepath = asset_file
104
BASE_URL = config.ASSETS_URL
106
path = open(asset_path, 'rb')
107
url = 'data:' + mime_type + ';base64,' + base64.b64encode(path.read())
109
url = '%s%s' % (BASE_URL, filepath)
111
filetime = int(os.path.getmtime(asset_path))
112
url = add_cache_buster(url, filetime)
114
simply_process = False
117
if fileext in ('cur',):
118
simply_process = True
121
image = Image.open(path)
123
if not collapse_x and not collapse_y and not dst_colors:
124
simply_process = True
128
url = 'data:' + mime_type + ';base64,' + base64.b64encode(path.read())
130
url = '%s%s' % (BASE_URL, filepath)
132
filetime = int(os.path.getmtime(asset_path))
133
url = add_cache_buster(url, filetime)
135
width, height = collapse_x or image.size[0], collapse_y or image.size[1]
136
new_image = Image.new(
138
size=(width + spacing[1] + spacing[3], height + spacing[0] + spacing[2]),
141
for i, dst_color in enumerate(dst_colors):
142
src_color = src_colors[i]
143
pixdata = image.load()
144
for _y in xrange(image.size[1]):
145
for _x in xrange(image.size[0]):
146
pixel = pixdata[_x, _y]
147
if pixel[:3] == src_color:
148
pixdata[_x, _y] = tuple([int(c) for c in dst_color] + [pixel[3] if len(pixel) == 4 else 255])
149
iwidth, iheight = image.size
150
if iwidth != width or iheight != height:
155
cropped_image = image.crop((cx, cy, cx + width, cy + height))
156
new_image.paste(cropped_image, (int(spacing[3]), int(spacing[0])), cropped_image)
160
new_image.paste(image, (int(spacing[3]), int(spacing[0])))
164
new_image.save(asset_path)
165
filepath = asset_file
166
BASE_URL = config.ASSETS_URL
168
filetime = int(os.path.getmtime(asset_path))
170
log.exception("Error while saving image")
171
inline = True # Retry inline version
172
url = os.path.join(config.ASSETS_URL.rstrip('/'), asset_file.lstrip('/'))
174
url = add_cache_buster(url, filetime)
176
output = six.BytesIO()
177
new_image.save(output, format='PNG')
178
contents = output.getvalue()
180
url = 'data:' + mime_type + ';base64,' + base64.b64encode(contents)
182
url = os.path.join(BASE_URL.rstrip('/'), filepath.lstrip('/'))
183
if cache_buster and filetime != 'NA':
184
url = add_cache_buster(url, filetime)
187
url = 'url(%s)' % escape(url)
188
return String.unquoted(url)
191
@register('inline-image', 1)
192
@register('inline-image', 2)
193
@register('inline-image', 3)
194
@register('inline-image', 4)
195
@register('inline-image', 5)
196
def inline_image(image, mime_type=None, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None):
198
Embeds the contents of a file directly inside your stylesheet, eliminating
199
the need for another HTTP request. For small files such images or fonts,
200
this can be a performance benefit at the cost of a larger generated CSS
203
return _image_url(image, False, False, dst_color, src_color, True, mime_type, spacing, collapse_x, collapse_y)
206
@register('image-url', 1)
207
@register('image-url', 2)
208
@register('image-url', 3)
209
@register('image-url', 4)
210
@register('image-url', 5)
211
@register('image-url', 6)
212
def image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None):
214
Generates a path to an asset found relative to the project's images
216
Passing a true value as the second argument will cause the only the path to
217
be returned instead of a `url()` function
219
return _image_url(path, only_path, cache_buster, dst_color, src_color, False, None, spacing, collapse_x, collapse_y)
222
@register('image-width', 1)
223
def image_width(image):
225
Returns the width of the image found at the path supplied by `image`
226
relative to your project's images directory.
229
raise Exception("Images manipulation require PIL")
230
filepath = String.unquoted(image).value
233
width = _image_size_cache[filepath][0]
236
IMAGES_ROOT = _images_root()
237
if callable(IMAGES_ROOT):
239
_file, _storage = list(IMAGES_ROOT(filepath))[0]
240
path = _storage.open(_file)
244
_path = os.path.join(IMAGES_ROOT, filepath.strip('/'))
245
if os.path.exists(_path):
246
path = open(_path, 'rb')
248
image = Image.open(path)
251
_image_size_cache[filepath] = size
252
return Number(width, 'px')
255
@register('image-height', 1)
256
def image_height(image):
258
Returns the height of the image found at the path supplied by `image`
259
relative to your project's images directory.
262
raise Exception("Images manipulation require PIL")
263
filepath = String.unquoted(image).value
266
height = _image_size_cache[filepath][1]
269
IMAGES_ROOT = _images_root()
270
if callable(IMAGES_ROOT):
272
_file, _storage = list(IMAGES_ROOT(filepath))[0]
273
path = _storage.open(_file)
277
_path = os.path.join(IMAGES_ROOT, filepath.strip('/'))
278
if os.path.exists(_path):
279
path = open(_path, 'rb')
281
image = Image.open(path)
284
_image_size_cache[filepath] = size
285
return Number(height, 'px')