summaryrefslogtreecommitdiff
path: root/scss/extension/compass/images.py
diff options
context:
space:
mode:
Diffstat (limited to 'scss/extension/compass/images.py')
-rw-r--r--scss/extension/compass/images.py275
1 files changed, 275 insertions, 0 deletions
diff --git a/scss/extension/compass/images.py b/scss/extension/compass/images.py
new file mode 100644
index 0000000..a3721fb
--- /dev/null
+++ b/scss/extension/compass/images.py
@@ -0,0 +1,275 @@
+"""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 _image_size_cache
+from .helpers import add_cache_buster
+from scss import config
+from scss.namespace import Namespace
+from scss.types import Color, List, Number, String
+from scss.util import escape, getmtime, make_data_url, make_filename_hash
+
+try:
+ from PIL import Image
+except ImportError:
+ try:
+ import Image
+ except:
+ Image = None
+
+log = logging.getLogger(__name__)
+ns = images_namespace = Namespace()
+__all__ = ['gradients_namespace']
+
+
+def _images_root():
+ return config.STATIC_ROOT if config.IMAGES_ROOT is None else config.IMAGES_ROOT
+
+
+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 Exception("Images manipulation require PIL")
+ 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 = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets')
+ 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 not only_path:
+ url = 'url(%s)' % escape(url)
+ return String.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 Exception("Images manipulation require PIL")
+ 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.
+ """
+ if not Image:
+ raise Exception("Images manipulation require PIL")
+ 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')