1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
|
"""Image utilities ported from Compass."""
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import logging
import mimetypes
import os.path
import six
from six.moves import xrange
from . import CompassExtension
from .helpers import add_cache_buster
from scss import config
from scss.errors import SassMissingDependency
from scss.types import Color, List, Number, String, Url
from scss.util import getmtime, make_data_url, make_filename_hash
from scss.extension import Cache
try:
from PIL import Image
except ImportError:
try:
import Image
except:
Image = None
log = logging.getLogger(__name__)
ns = CompassExtension.namespace
def _images_root():
return config.STATIC_ROOT if config.IMAGES_ROOT is None else config.IMAGES_ROOT
def _assets_root():
return config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets')
def _get_cache(prefix):
return Cache((config.CACHE_ROOT or _assets_root(), prefix))
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):
"""
src_color - a list of or a single color to be replaced by each corresponding dst_color colors
spacing - spaces to be added to the image
collapse_x, collapse_y - collapsable (layered) image of the given size (x, y)
"""
if inline or dst_color or spacing:
if not Image:
raise SassMissingDependency('PIL', 'image manipulation')
filepath = String.unquoted(path).value
fileext = os.path.splitext(filepath)[1].lstrip('.').lower()
if mime_type:
mime_type = String.unquoted(mime_type).value
if not mime_type:
mime_type = mimetypes.guess_type(filepath)[0]
if not mime_type:
mime_type = 'image/%s' % fileext
path = None
IMAGES_ROOT = _images_root()
if callable(IMAGES_ROOT):
try:
_file, _storage = list(IMAGES_ROOT(filepath))[0]
except IndexError:
filetime = None
else:
filetime = getmtime(_file, _storage)
if filetime is None:
filetime = 'NA'
elif inline or dst_color or spacing:
path = _storage.open(_file)
else:
_path = os.path.join(IMAGES_ROOT.rstrip(os.sep), filepath.strip('\\/'))
filetime = getmtime(_path)
if filetime is None:
filetime = 'NA'
elif inline or dst_color or spacing:
path = open(_path, 'rb')
BASE_URL = config.IMAGES_URL or config.STATIC_URL
if path:
dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_color) if v]
src_color = Color.from_name('black') if src_color is None else src_color
src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_color)]
len_colors = max(len(dst_colors), len(src_colors))
dst_colors = (dst_colors * len_colors)[:len_colors]
src_colors = (src_colors * len_colors)[:len_colors]
spacing = Number(0) if spacing is None else spacing
spacing = [int(Number(v).value) for v in List.from_maybe(spacing)]
spacing = (spacing * 4)[:4]
file_name, file_ext = os.path.splitext(os.path.normpath(filepath).replace(os.sep, '_'))
key = (filetime, src_color, dst_color, spacing)
asset_file = file_name + '-' + make_filename_hash(key) + file_ext
ASSETS_ROOT = _assets_root()
asset_path = os.path.join(ASSETS_ROOT, asset_file)
if os.path.exists(asset_path):
filepath = asset_file
BASE_URL = config.ASSETS_URL
if inline:
path = open(asset_path, 'rb')
url = make_data_url(mime_type, path.read())
else:
url = '%s%s' % (BASE_URL, filepath)
if cache_buster:
filetime = getmtime(asset_path)
url = add_cache_buster(url, filetime)
else:
simply_process = False
image = None
if fileext in ('cur',):
simply_process = True
else:
try:
image = Image.open(path)
except IOError:
if not collapse_x and not collapse_y and not dst_colors:
simply_process = True
if simply_process:
if inline:
url = make_data_url(mime_type, path.read())
else:
url = '%s%s' % (BASE_URL, filepath)
if cache_buster:
filetime = getmtime(asset_path)
url = add_cache_buster(url, filetime)
else:
width, height = collapse_x or image.size[0], collapse_y or image.size[1]
new_image = Image.new(
mode='RGBA',
size=(width + spacing[1] + spacing[3], height + spacing[0] + spacing[2]),
color=(0, 0, 0, 0)
)
for i, dst_color in enumerate(dst_colors):
src_color = src_colors[i]
pixdata = image.load()
for _y in xrange(image.size[1]):
for _x in xrange(image.size[0]):
pixel = pixdata[_x, _y]
if pixel[:3] == src_color:
pixdata[_x, _y] = tuple([int(c) for c in dst_color] + [pixel[3] if len(pixel) == 4 else 255])
iwidth, iheight = image.size
if iwidth != width or iheight != height:
cy = 0
while cy < iheight:
cx = 0
while cx < iwidth:
cropped_image = image.crop((cx, cy, cx + width, cy + height))
new_image.paste(cropped_image, (int(spacing[3]), int(spacing[0])), cropped_image)
cx += width
cy += height
else:
new_image.paste(image, (int(spacing[3]), int(spacing[0])))
if not inline:
try:
new_image.save(asset_path)
filepath = asset_file
BASE_URL = config.ASSETS_URL
if cache_buster:
filetime = getmtime(asset_path)
except IOError:
log.exception("Error while saving image")
inline = True # Retry inline version
url = os.path.join(config.ASSETS_URL.rstrip(os.sep), asset_file.lstrip(os.sep))
if cache_buster:
url = add_cache_buster(url, filetime)
if inline:
output = six.BytesIO()
new_image.save(output, format='PNG')
contents = output.getvalue()
output.close()
url = make_data_url(mime_type, contents)
else:
url = os.path.join(BASE_URL.rstrip('/'), filepath.lstrip('\\/'))
if cache_buster and filetime != 'NA':
url = add_cache_buster(url, filetime)
if not os.sep == '/':
url = url.replace(os.sep, '/')
if only_path:
return String.unquoted(url)
else:
return Url.unquoted(url)
@ns.declare
def inline_image(image, mime_type=None, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None):
"""
Embeds the contents of a file directly inside your stylesheet, eliminating
the need for another HTTP request. For small files such images or fonts,
this can be a performance benefit at the cost of a larger generated CSS
file.
"""
return _image_url(image, False, False, dst_color, src_color, True, mime_type, spacing, collapse_x, collapse_y)
@ns.declare
def image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None):
"""
Generates a path to an asset found relative to the project's images
directory.
Passing a true value as the second argument will cause the only the path to
be returned instead of a `url()` function
"""
return _image_url(path, only_path, cache_buster, dst_color, src_color, False, None, spacing, collapse_x, collapse_y)
@ns.declare
def image_width(image):
"""
Returns the width of the image found at the path supplied by `image`
relative to your project's images directory.
"""
if not Image:
raise SassMissingDependency('PIL', 'image manipulation')
image_size_cache = _get_cache('image_size_cache')
filepath = String.unquoted(image).value
path = None
try:
width = image_size_cache[filepath][0]
except KeyError:
width = 0
IMAGES_ROOT = _images_root()
if callable(IMAGES_ROOT):
try:
_file, _storage = list(IMAGES_ROOT(filepath))[0]
except IndexError:
pass
else:
path = _storage.open(_file)
else:
_path = os.path.join(IMAGES_ROOT, filepath.strip(os.sep))
if os.path.exists(_path):
path = open(_path, 'rb')
if path:
image = Image.open(path)
size = image.size
width = size[0]
image_size_cache[filepath] = size
return Number(width, 'px')
@ns.declare
def image_height(image):
"""
Returns the height of the image found at the path supplied by `image`
relative to your project's images directory.
"""
image_size_cache = _get_cache('image_size_cache')
if not Image:
raise SassMissingDependency('PIL', 'image manipulation')
filepath = String.unquoted(image).value
path = None
try:
height = image_size_cache[filepath][1]
except KeyError:
height = 0
IMAGES_ROOT = _images_root()
if callable(IMAGES_ROOT):
try:
_file, _storage = list(IMAGES_ROOT(filepath))[0]
except IndexError:
pass
else:
path = _storage.open(_file)
else:
_path = os.path.join(IMAGES_ROOT, filepath.strip(os.sep))
if os.path.exists(_path):
path = open(_path, 'rb')
if path:
image = Image.open(path)
size = image.size
height = size[1]
image_size_cache[filepath] = size
return Number(height, 'px')
|