Livereload css
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Global cache of image sizes, shared between sprites and images libraries.
|
||||
_image_size_cache = {}
|
||||
@@ -0,0 +1,400 @@
|
||||
"""Utilities for working with gradients. Inspired by Compass, but not quite
|
||||
the same.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
from scss.functions.library import FunctionLibrary
|
||||
from scss.functions.compass.helpers import opposite_position, position
|
||||
from scss.types import Color, List, Number, String
|
||||
from scss.util import escape, split_params, to_float, to_str
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
COMPASS_GRADIENTS_LIBRARY = FunctionLibrary()
|
||||
register = COMPASS_GRADIENTS_LIBRARY.register
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
def __color_stops(percentages, *args):
|
||||
if len(args) == 1:
|
||||
if isinstance(args[0], (list, tuple, List)):
|
||||
list(args[0])
|
||||
elif isinstance(args[0], (String, six.string_types)):
|
||||
color_stops = []
|
||||
colors = split_params(getattr(args[0], 'value', args[0]))
|
||||
for color in colors:
|
||||
color = color.strip()
|
||||
if color.startswith('color-stop('):
|
||||
s, c = split_params(color[11:].rstrip(')'))
|
||||
s = s.strip()
|
||||
c = c.strip()
|
||||
else:
|
||||
c, s = color.split()
|
||||
color_stops.append((to_float(s), c))
|
||||
return color_stops
|
||||
|
||||
colors = []
|
||||
stops = []
|
||||
prev_color = False
|
||||
for c in args:
|
||||
for c in List.from_maybe(c):
|
||||
if isinstance(c, Color):
|
||||
if prev_color:
|
||||
stops.append(None)
|
||||
colors.append(c)
|
||||
prev_color = True
|
||||
elif isinstance(c, Number):
|
||||
stops.append(c)
|
||||
prev_color = False
|
||||
|
||||
if prev_color:
|
||||
stops.append(None)
|
||||
stops = stops[:len(colors)]
|
||||
if stops[0] is None:
|
||||
stops[0] = Number(0, '%')
|
||||
if stops[-1] is None:
|
||||
stops[-1] = Number(100, '%')
|
||||
|
||||
maxable_stops = [s for s in stops if s and not s.is_simple_unit('%')]
|
||||
if maxable_stops:
|
||||
max_stops = max(maxable_stops)
|
||||
else:
|
||||
max_stops = None
|
||||
|
||||
stops = [_s / max_stops if _s and not _s.is_simple_unit('%') else _s for _s in stops]
|
||||
|
||||
init = 0
|
||||
start = None
|
||||
for i, s in enumerate(stops + [1.0]):
|
||||
if s is None:
|
||||
if start is None:
|
||||
start = i
|
||||
end = i
|
||||
else:
|
||||
final = s
|
||||
if start is not None:
|
||||
stride = (final - init) / Number(end - start + 1 + (1 if i < len(stops) else 0))
|
||||
for j in range(start, end + 1):
|
||||
stops[j] = init + stride * Number(j - start + 1)
|
||||
init = final
|
||||
start = None
|
||||
|
||||
if not max_stops or percentages:
|
||||
pass
|
||||
else:
|
||||
stops = [s if s.is_simple_unit('%') else s * max_stops for s in stops]
|
||||
|
||||
return list(zip(stops, colors))
|
||||
|
||||
|
||||
def _render_standard_color_stops(color_stops):
|
||||
pairs = []
|
||||
for i, (stop, color) in enumerate(color_stops):
|
||||
if ((i == 0 and stop == Number(0, '%')) or
|
||||
(i == len(color_stops) - 1 and stop == Number(100, '%'))):
|
||||
pairs.append(color)
|
||||
else:
|
||||
pairs.append(List([color, stop], use_comma=False))
|
||||
|
||||
return List(pairs, use_comma=True)
|
||||
|
||||
|
||||
@register('grad-color-stops')
|
||||
def grad_color_stops(*args):
|
||||
args = List.from_maybe_starargs(args)
|
||||
color_stops = __color_stops(True, *args)
|
||||
ret = ', '.join(['color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops])
|
||||
return String.unquoted(ret)
|
||||
|
||||
|
||||
def __grad_end_position(radial, color_stops):
|
||||
return __grad_position(-1, 100, radial, color_stops)
|
||||
|
||||
|
||||
@register('grad-point')
|
||||
def grad_point(*p):
|
||||
pos = set()
|
||||
hrz = vrt = Number(0.5, '%')
|
||||
for _p in p:
|
||||
pos.update(String.unquoted(_p).value.split())
|
||||
if 'left' in pos:
|
||||
hrz = Number(0, '%')
|
||||
elif 'right' in pos:
|
||||
hrz = Number(1, '%')
|
||||
if 'top' in pos:
|
||||
vrt = Number(0, '%')
|
||||
elif 'bottom' in pos:
|
||||
vrt = Number(1, '%')
|
||||
return List([v for v in (hrz, vrt) if v is not None])
|
||||
|
||||
|
||||
def __grad_position(index, default, radial, color_stops):
|
||||
try:
|
||||
stops = Number(color_stops[index][0])
|
||||
if radial and not stops.is_simple_unit('px') and (index == 0 or index == -1 or index == len(color_stops) - 1):
|
||||
log.warn("Webkit only supports pixels for the start and end stops for radial gradients. Got %s", stops)
|
||||
except IndexError:
|
||||
stops = Number(default)
|
||||
return stops
|
||||
|
||||
|
||||
@register('grad-end-position')
|
||||
def grad_end_position(*color_stops):
|
||||
color_stops = __color_stops(False, *color_stops)
|
||||
return Number(__grad_end_position(False, color_stops))
|
||||
|
||||
|
||||
@register('color-stops')
|
||||
def color_stops(*args):
|
||||
args = List.from_maybe_starargs(args)
|
||||
color_stops = __color_stops(False, *args)
|
||||
ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops])
|
||||
return String.unquoted(ret)
|
||||
|
||||
|
||||
@register('color-stops-in-percentages')
|
||||
def color_stops_in_percentages(*args):
|
||||
args = List.from_maybe_starargs(args)
|
||||
color_stops = __color_stops(True, *args)
|
||||
ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops])
|
||||
return String.unquoted(ret)
|
||||
|
||||
|
||||
def _get_gradient_position_and_angle(args):
|
||||
for arg in args:
|
||||
ret = None
|
||||
skip = False
|
||||
for a in arg:
|
||||
if isinstance(a, Color):
|
||||
skip = True
|
||||
break
|
||||
elif isinstance(a, Number):
|
||||
ret = arg
|
||||
if skip:
|
||||
continue
|
||||
if ret is not None:
|
||||
return ret
|
||||
for seek in (
|
||||
'center',
|
||||
'top', 'bottom',
|
||||
'left', 'right',
|
||||
):
|
||||
if String(seek) in arg:
|
||||
return arg
|
||||
return None
|
||||
|
||||
|
||||
def _get_gradient_shape_and_size(args):
|
||||
for arg in args:
|
||||
for seek in (
|
||||
'circle', 'ellipse',
|
||||
'closest-side', 'closest-corner',
|
||||
'farthest-side', 'farthest-corner',
|
||||
'contain', 'cover',
|
||||
):
|
||||
if String(seek) in arg:
|
||||
return arg
|
||||
return None
|
||||
|
||||
|
||||
def _get_gradient_color_stops(args):
|
||||
color_stops = []
|
||||
for arg in args:
|
||||
for a in List.from_maybe(arg):
|
||||
if isinstance(a, Color):
|
||||
color_stops.append(arg)
|
||||
break
|
||||
return color_stops or None
|
||||
|
||||
|
||||
@register('radial-gradient')
|
||||
def radial_gradient(*args):
|
||||
args = List.from_maybe_starargs(args)
|
||||
|
||||
position_and_angle = _get_gradient_position_and_angle(args)
|
||||
shape_and_size = _get_gradient_shape_and_size(args)
|
||||
color_stops = _get_gradient_color_stops(args)
|
||||
if color_stops is None:
|
||||
raise Exception('No color stops provided to radial-gradient function')
|
||||
color_stops = __color_stops(False, *color_stops)
|
||||
|
||||
args = [
|
||||
position(position_and_angle) if position_and_angle is not None else None,
|
||||
shape_and_size if shape_and_size is not None else None,
|
||||
]
|
||||
args.extend(_render_standard_color_stops(color_stops))
|
||||
|
||||
to__s = 'radial-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
|
||||
ret = String.unquoted(to__s)
|
||||
|
||||
def to__css2():
|
||||
return String.unquoted('')
|
||||
ret.to__css2 = to__css2
|
||||
|
||||
def to__moz():
|
||||
return String.unquoted('-moz-' + to__s)
|
||||
ret.to__moz = to__moz
|
||||
|
||||
def to__pie():
|
||||
log.warn("PIE does not support radial-gradient.")
|
||||
return String.unquoted('-pie-radial-gradient(unsupported)')
|
||||
ret.to__pie = to__pie
|
||||
|
||||
def to__webkit():
|
||||
return String.unquoted('-webkit-' + to__s)
|
||||
ret.to__webkit = to__webkit
|
||||
|
||||
def to__owg():
|
||||
args = [
|
||||
'radial',
|
||||
grad_point(position_and_angle) if position_and_angle is not None else 'center',
|
||||
'0',
|
||||
grad_point(position_and_angle) if position_and_angle is not None else 'center',
|
||||
__grad_end_position(True, color_stops),
|
||||
]
|
||||
args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops)
|
||||
ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
|
||||
return String.unquoted(ret)
|
||||
ret.to__owg = to__owg
|
||||
|
||||
def to__svg():
|
||||
return radial_svg_gradient(color_stops, position_and_angle or 'center')
|
||||
ret.to__svg = to__svg
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@register('linear-gradient')
|
||||
def linear_gradient(*args):
|
||||
args = List.from_maybe_starargs(args)
|
||||
|
||||
position_and_angle = _get_gradient_position_and_angle(args)
|
||||
color_stops = _get_gradient_color_stops(args)
|
||||
if color_stops is None:
|
||||
raise Exception('No color stops provided to linear-gradient function')
|
||||
color_stops = __color_stops(False, *color_stops)
|
||||
|
||||
args = [
|
||||
position(position_and_angle) if position_and_angle is not None else None,
|
||||
]
|
||||
args.extend(_render_standard_color_stops(color_stops))
|
||||
|
||||
to__s = 'linear-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
|
||||
ret = String.unquoted(to__s)
|
||||
|
||||
def to__css2():
|
||||
return String.unquoted('')
|
||||
ret.to__css2 = to__css2
|
||||
|
||||
def to__moz():
|
||||
return String.unquoted('-moz-' + to__s)
|
||||
ret.to__moz = to__moz
|
||||
|
||||
def to__pie():
|
||||
return String.unquoted('-pie-' + to__s)
|
||||
ret.to__pie = to__pie
|
||||
|
||||
def to__ms():
|
||||
return String.unquoted('-ms-' + to__s)
|
||||
ret.to__ms = to__ms
|
||||
|
||||
def to__o():
|
||||
return String.unquoted('-o-' + to__s)
|
||||
ret.to__o = to__o
|
||||
|
||||
def to__webkit():
|
||||
return String.unquoted('-webkit-' + to__s)
|
||||
ret.to__webkit = to__webkit
|
||||
|
||||
def to__owg():
|
||||
args = [
|
||||
'linear',
|
||||
position(position_and_angle or None),
|
||||
opposite_position(position_and_angle or None),
|
||||
]
|
||||
args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops)
|
||||
ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args if a is not None) + ')'
|
||||
return String.unquoted(ret)
|
||||
ret.to__owg = to__owg
|
||||
|
||||
def to__svg():
|
||||
return linear_svg_gradient(color_stops, position_and_angle or 'top')
|
||||
ret.to__svg = to__svg
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@register('radial-svg-gradient')
|
||||
def radial_svg_gradient(*args):
|
||||
args = List.from_maybe_starargs(args)
|
||||
color_stops = args
|
||||
center = None
|
||||
if isinstance(args[-1], (String, Number, six.string_types)):
|
||||
center = args[-1]
|
||||
color_stops = args[:-1]
|
||||
color_stops = __color_stops(False, *color_stops)
|
||||
cx, cy = zip(*grad_point(center).items())[1]
|
||||
r = __grad_end_position(True, color_stops)
|
||||
svg = __radial_svg(color_stops, cx, cy, r)
|
||||
url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg)
|
||||
inline = 'url("%s")' % escape(url)
|
||||
return String.unquoted(inline)
|
||||
|
||||
|
||||
@register('linear-svg-gradient')
|
||||
def linear_svg_gradient(*args):
|
||||
args = List.from_maybe_starargs(args)
|
||||
color_stops = args
|
||||
start = None
|
||||
if isinstance(args[-1], (String, Number, six.string_types)):
|
||||
start = args[-1]
|
||||
color_stops = args[:-1]
|
||||
color_stops = __color_stops(False, *color_stops)
|
||||
x1, y1 = zip(*grad_point(start).items())[1]
|
||||
x2, y2 = zip(*grad_point(opposite_position(start)).items())[1]
|
||||
svg = _linear_svg(color_stops, x1, y1, x2, y2)
|
||||
url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg)
|
||||
inline = 'url("%s")' % escape(url)
|
||||
return String.unquoted(inline)
|
||||
|
||||
|
||||
def __color_stops_svg(color_stops):
|
||||
ret = ''.join('<stop offset="%s" stop-color="%s"/>' % (to_str(s), c) for s, c in color_stops)
|
||||
return ret
|
||||
|
||||
|
||||
def __svg_template(gradient):
|
||||
ret = '<?xml version="1.0" encoding="utf-8"?>\
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">\
|
||||
<defs>%s</defs>\
|
||||
<rect x="0" y="0" width="100%%" height="100%%" fill="url(#grad)" />\
|
||||
</svg>' % gradient
|
||||
return ret
|
||||
|
||||
|
||||
def _linear_svg(color_stops, x1, y1, x2, y2):
|
||||
gradient = '<linearGradient id="grad" x1="%s" y1="%s" x2="%s" y2="%s">%s</linearGradient>' % (
|
||||
to_str(Number(x1)),
|
||||
to_str(Number(y1)),
|
||||
to_str(Number(x2)),
|
||||
to_str(Number(y2)),
|
||||
__color_stops_svg(color_stops)
|
||||
)
|
||||
return __svg_template(gradient)
|
||||
|
||||
|
||||
def __radial_svg(color_stops, cx, cy, r):
|
||||
gradient = '<radialGradient id="grad" gradientUnits="userSpaceOnUse" cx="%s" cy="%s" r="%s">%s</radialGradient>' % (
|
||||
to_str(Number(cx)),
|
||||
to_str(Number(cy)),
|
||||
to_str(Number(r)),
|
||||
__color_stops_svg(color_stops)
|
||||
)
|
||||
return __svg_template(gradient)
|
||||
@@ -0,0 +1,654 @@
|
||||
"""Miscellaneous helper functions ported from Compass.
|
||||
|
||||
See: http://compass-style.org/reference/compass/helpers/
|
||||
|
||||
This collection is not necessarily complete or up-to-date.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import math
|
||||
import os.path
|
||||
import time
|
||||
|
||||
import six
|
||||
|
||||
from scss import config
|
||||
from scss.functions.library import FunctionLibrary
|
||||
from scss.types import Boolean, List, Null, Number, String
|
||||
from scss.util import escape, to_str
|
||||
import re
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
COMPASS_HELPERS_LIBRARY = FunctionLibrary()
|
||||
register = COMPASS_HELPERS_LIBRARY.register
|
||||
|
||||
FONT_TYPES = {
|
||||
'woff': 'woff',
|
||||
'otf': 'opentype',
|
||||
'opentype': 'opentype',
|
||||
'ttf': 'truetype',
|
||||
'truetype': 'truetype',
|
||||
'svg': 'svg',
|
||||
'eot': 'embedded-opentype'
|
||||
}
|
||||
|
||||
|
||||
def add_cache_buster(url, mtime):
|
||||
fragment = url.split('#')
|
||||
query = fragment[0].split('?')
|
||||
if len(query) > 1 and query[1] != '':
|
||||
cb = '&_=%s' % (mtime)
|
||||
url = '?'.join(query) + cb
|
||||
else:
|
||||
cb = '?_=%s' % (mtime)
|
||||
url = query[0] + cb
|
||||
if len(fragment) > 1:
|
||||
url += '#' + fragment[1]
|
||||
return url
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Data manipulation
|
||||
|
||||
@register('blank')
|
||||
def blank(*objs):
|
||||
"""Returns true when the object is false, an empty string, or an empty list"""
|
||||
for o in objs:
|
||||
if isinstance(o, Boolean):
|
||||
is_blank = not o
|
||||
elif isinstance(o, String):
|
||||
is_blank = not len(o.value.strip())
|
||||
elif isinstance(o, List):
|
||||
is_blank = all(blank(el) for el in o)
|
||||
else:
|
||||
is_blank = False
|
||||
|
||||
if not is_blank:
|
||||
return Boolean(False)
|
||||
|
||||
return Boolean(True)
|
||||
|
||||
|
||||
@register('compact')
|
||||
def compact(*args):
|
||||
"""Returns a new list after removing any non-true values"""
|
||||
use_comma = True
|
||||
if len(args) == 1 and isinstance(args[0], List):
|
||||
use_comma = args[0].use_comma
|
||||
args = args[0]
|
||||
|
||||
return List(
|
||||
[arg for arg in args if arg],
|
||||
use_comma=use_comma,
|
||||
)
|
||||
|
||||
|
||||
@register('reject')
|
||||
def reject(lst, *values):
|
||||
"""Removes the given values from the list"""
|
||||
lst = List.from_maybe(lst)
|
||||
values = frozenset(List.from_maybe_starargs(values))
|
||||
|
||||
ret = []
|
||||
for item in lst:
|
||||
if item not in values:
|
||||
ret.append(item)
|
||||
return List(ret, use_comma=lst.use_comma)
|
||||
|
||||
|
||||
@register('first-value-of')
|
||||
def first_value_of(*args):
|
||||
if len(args) == 1 and isinstance(args[0], String):
|
||||
first = args[0].value.split()[0]
|
||||
return type(args[0])(first)
|
||||
|
||||
args = List.from_maybe_starargs(args)
|
||||
if len(args):
|
||||
return args[0]
|
||||
else:
|
||||
return Null()
|
||||
|
||||
|
||||
@register('-compass-list')
|
||||
def dash_compass_list(*args):
|
||||
return List.from_maybe_starargs(args)
|
||||
|
||||
|
||||
@register('-compass-space-list')
|
||||
def dash_compass_space_list(*lst):
|
||||
"""
|
||||
If the argument is a list, it will return a new list that is space delimited
|
||||
Otherwise it returns a new, single element, space-delimited list.
|
||||
"""
|
||||
ret = dash_compass_list(*lst)
|
||||
ret.value.pop('_', None)
|
||||
return ret
|
||||
|
||||
|
||||
@register('-compass-slice', 3)
|
||||
def dash_compass_slice(lst, start_index, end_index=None):
|
||||
start_index = Number(start_index).value
|
||||
end_index = Number(end_index).value if end_index is not None else None
|
||||
ret = {}
|
||||
lst = List(lst)
|
||||
if end_index:
|
||||
# This function has an inclusive end, but Python slicing is exclusive
|
||||
end_index += 1
|
||||
ret = lst.value[start_index:end_index]
|
||||
return List(ret, use_comma=lst.use_comma)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Property prefixing
|
||||
|
||||
@register('prefixed')
|
||||
def prefixed(prefix, *args):
|
||||
to_fnct_str = 'to_' + to_str(prefix).replace('-', '_')
|
||||
for arg in List.from_maybe_starargs(args):
|
||||
if hasattr(arg, to_fnct_str):
|
||||
return Boolean(True)
|
||||
return Boolean(False)
|
||||
|
||||
|
||||
@register('prefix')
|
||||
def prefix(prefix, *args):
|
||||
to_fnct_str = 'to_' + to_str(prefix).replace('-', '_')
|
||||
args = list(args)
|
||||
for i, arg in enumerate(args):
|
||||
if isinstance(arg, List):
|
||||
_value = []
|
||||
for iarg in arg:
|
||||
to_fnct = getattr(iarg, to_fnct_str, None)
|
||||
if to_fnct:
|
||||
_value.append(to_fnct())
|
||||
else:
|
||||
_value.append(iarg)
|
||||
args[i] = List(_value)
|
||||
else:
|
||||
to_fnct = getattr(arg, to_fnct_str, None)
|
||||
if to_fnct:
|
||||
args[i] = to_fnct()
|
||||
|
||||
return List.maybe_new(args, use_comma=True)
|
||||
|
||||
|
||||
@register('-moz')
|
||||
def dash_moz(*args):
|
||||
return prefix('_moz', *args)
|
||||
|
||||
|
||||
@register('-svg')
|
||||
def dash_svg(*args):
|
||||
return prefix('_svg', *args)
|
||||
|
||||
|
||||
@register('-css2')
|
||||
def dash_css2(*args):
|
||||
return prefix('_css2', *args)
|
||||
|
||||
|
||||
@register('-pie')
|
||||
def dash_pie(*args):
|
||||
return prefix('_pie', *args)
|
||||
|
||||
|
||||
@register('-webkit')
|
||||
def dash_webkit(*args):
|
||||
return prefix('_webkit', *args)
|
||||
|
||||
|
||||
@register('-owg')
|
||||
def dash_owg(*args):
|
||||
return prefix('_owg', *args)
|
||||
|
||||
|
||||
@register('-khtml')
|
||||
def dash_khtml(*args):
|
||||
return prefix('_khtml', *args)
|
||||
|
||||
|
||||
@register('-ms')
|
||||
def dash_ms(*args):
|
||||
return prefix('_ms', *args)
|
||||
|
||||
|
||||
@register('-o')
|
||||
def dash_o(*args):
|
||||
return prefix('_o', *args)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Selector generation
|
||||
|
||||
@register('append-selector', 2)
|
||||
def append_selector(selector, to_append):
|
||||
if isinstance(selector, List):
|
||||
lst = selector.value
|
||||
else:
|
||||
lst = String.unquoted(selector).value.split(',')
|
||||
to_append = String.unquoted(to_append).value.strip()
|
||||
ret = sorted(set(s.strip() + to_append for s in lst if s.strip()))
|
||||
ret = dict(enumerate(ret))
|
||||
ret['_'] = ','
|
||||
return ret
|
||||
|
||||
|
||||
_elements_of_type_block = 'address, article, aside, blockquote, center, dd, details, dir, div, dl, dt, fieldset, figcaption, figure, footer, form, frameset, h1, h2, h3, h4, h5, h6, header, hgroup, hr, isindex, menu, nav, noframes, noscript, ol, p, pre, section, summary, ul'
|
||||
_elements_of_type_inline = 'a, abbr, acronym, audio, b, basefont, bdo, big, br, canvas, cite, code, command, datalist, dfn, em, embed, font, i, img, input, kbd, keygen, label, mark, meter, output, progress, q, rp, rt, ruby, s, samp, select, small, span, strike, strong, sub, sup, textarea, time, tt, u, var, video, wbr'
|
||||
_elements_of_type_table = 'table'
|
||||
_elements_of_type_list_item = 'li'
|
||||
_elements_of_type_table_row_group = 'tbody'
|
||||
_elements_of_type_table_header_group = 'thead'
|
||||
_elements_of_type_table_footer_group = 'tfoot'
|
||||
_elements_of_type_table_row = 'tr'
|
||||
_elements_of_type_table_cel = 'td, th'
|
||||
_elements_of_type_html5_block = 'article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary'
|
||||
_elements_of_type_html5_inline = 'audio, canvas, command, datalist, embed, keygen, mark, meter, output, progress, rp, rt, ruby, time, video, wbr'
|
||||
_elements_of_type_html5 = 'article, aside, audio, canvas, command, datalist, details, embed, figcaption, figure, footer, header, hgroup, keygen, mark, menu, meter, nav, output, progress, rp, rt, ruby, section, summary, time, video, wbr'
|
||||
_elements_of_type = {
|
||||
'block': sorted(_elements_of_type_block.replace(' ', '').split(',')),
|
||||
'inline': sorted(_elements_of_type_inline.replace(' ', '').split(',')),
|
||||
'table': sorted(_elements_of_type_table.replace(' ', '').split(',')),
|
||||
'list-item': sorted(_elements_of_type_list_item.replace(' ', '').split(',')),
|
||||
'table-row-group': sorted(_elements_of_type_table_row_group.replace(' ', '').split(',')),
|
||||
'table-header-group': sorted(_elements_of_type_table_header_group.replace(' ', '').split(',')),
|
||||
'table-footer-group': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')),
|
||||
'table-row': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')),
|
||||
'table-cell': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')),
|
||||
'html5-block': sorted(_elements_of_type_html5_block.replace(' ', '').split(',')),
|
||||
'html5-inline': sorted(_elements_of_type_html5_inline.replace(' ', '').split(',')),
|
||||
'html5': sorted(_elements_of_type_html5.replace(' ', '').split(',')),
|
||||
}
|
||||
|
||||
|
||||
@register('elements-of-type', 1)
|
||||
def elements_of_type(display):
|
||||
d = String.unquoted(display)
|
||||
ret = _elements_of_type.get(d.value, None)
|
||||
if ret is None:
|
||||
raise Exception("Elements of type '%s' not found!" % d.value)
|
||||
return List(ret, use_comma=True)
|
||||
|
||||
|
||||
@register('enumerate', 3)
|
||||
@register('enumerate', 4)
|
||||
def enumerate_(prefix, frm, through, separator='-'):
|
||||
separator = String.unquoted(separator).value
|
||||
try:
|
||||
frm = int(getattr(frm, 'value', frm))
|
||||
except ValueError:
|
||||
frm = 1
|
||||
try:
|
||||
through = int(getattr(through, 'value', through))
|
||||
except ValueError:
|
||||
through = frm
|
||||
if frm > through:
|
||||
# DEVIATION: allow reversed enumerations (and ranges as range() uses enumerate, like '@for .. from .. through')
|
||||
frm, through = through, frm
|
||||
rev = reversed
|
||||
else:
|
||||
rev = lambda x: x
|
||||
|
||||
ret = []
|
||||
for i in rev(range(frm, through + 1)):
|
||||
if prefix and prefix.value:
|
||||
ret.append(String.unquoted(prefix.value + separator + str(i)))
|
||||
else:
|
||||
ret.append(Number(i))
|
||||
|
||||
return List(ret, use_comma=True)
|
||||
|
||||
|
||||
@register('headers', 0)
|
||||
@register('headers', 1)
|
||||
@register('headers', 2)
|
||||
@register('headings', 0)
|
||||
@register('headings', 1)
|
||||
@register('headings', 2)
|
||||
def headers(frm=None, to=None):
|
||||
if frm and to is None:
|
||||
if isinstance(frm, String) and frm.value.lower() == 'all':
|
||||
frm = 1
|
||||
to = 6
|
||||
else:
|
||||
try:
|
||||
to = int(getattr(frm, 'value', frm))
|
||||
except ValueError:
|
||||
to = 6
|
||||
frm = 1
|
||||
else:
|
||||
try:
|
||||
frm = 1 if frm is None else int(getattr(frm, 'value', frm))
|
||||
except ValueError:
|
||||
frm = 1
|
||||
try:
|
||||
to = 6 if to is None else int(getattr(to, 'value', to))
|
||||
except ValueError:
|
||||
to = 6
|
||||
ret = [String.unquoted('h' + str(i)) for i in range(frm, to + 1)]
|
||||
return List(ret, use_comma=True)
|
||||
|
||||
|
||||
@register('nest')
|
||||
def nest(*arguments):
|
||||
if isinstance(arguments[0], List):
|
||||
lst = arguments[0]
|
||||
elif isinstance(arguments[0], String):
|
||||
lst = arguments[0].value.split(',')
|
||||
else:
|
||||
raise TypeError("Expected list or string, got %r" % (arguments[0],))
|
||||
|
||||
ret = []
|
||||
for s in lst:
|
||||
if isinstance(s, String):
|
||||
s = s.value
|
||||
elif isinstance(s, six.string_types):
|
||||
s = s
|
||||
else:
|
||||
raise TypeError("Expected string, got %r" % (s,))
|
||||
|
||||
s = s.strip()
|
||||
if not s:
|
||||
continue
|
||||
|
||||
ret.append(s)
|
||||
|
||||
for arg in arguments[1:]:
|
||||
if isinstance(arg, List):
|
||||
lst = arg
|
||||
elif isinstance(arg, String):
|
||||
lst = arg.value.split(',')
|
||||
else:
|
||||
raise TypeError("Expected list or string, got %r" % (arg,))
|
||||
|
||||
new_ret = []
|
||||
for s in lst:
|
||||
if isinstance(s, String):
|
||||
s = s.value
|
||||
elif isinstance(s, six.string_types):
|
||||
s = s
|
||||
else:
|
||||
raise TypeError("Expected string, got %r" % (s,))
|
||||
|
||||
s = s.strip()
|
||||
if not s:
|
||||
continue
|
||||
|
||||
for r in ret:
|
||||
if '&' in s:
|
||||
new_ret.append(s.replace('&', r))
|
||||
else:
|
||||
if not r or r[-1] in ('.', ':', '#'):
|
||||
new_ret.append(r + s)
|
||||
else:
|
||||
new_ret.append(r + ' ' + s)
|
||||
ret = new_ret
|
||||
|
||||
ret = [String.unquoted(s) for s in sorted(set(ret))]
|
||||
return List(ret, use_comma=True)
|
||||
|
||||
|
||||
# This isn't actually from Compass, but it's just a shortcut for enumerate().
|
||||
# DEVIATION: allow reversed ranges (range() uses enumerate() which allows reversed values, like '@for .. from .. through')
|
||||
@register('range', 1)
|
||||
@register('range', 2)
|
||||
def range_(frm, through=None):
|
||||
if through is None:
|
||||
through = frm
|
||||
frm = 1
|
||||
return enumerate_(None, frm, through)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Working with CSS constants
|
||||
|
||||
OPPOSITE_POSITIONS = {
|
||||
'top': String.unquoted('bottom'),
|
||||
'bottom': String.unquoted('top'),
|
||||
'left': String.unquoted('right'),
|
||||
'right': String.unquoted('left'),
|
||||
'center': String.unquoted('center'),
|
||||
}
|
||||
DEFAULT_POSITION = [String.unquoted('center'), String.unquoted('top')]
|
||||
|
||||
|
||||
def _position(opposite, positions):
|
||||
if positions is None:
|
||||
positions = DEFAULT_POSITION
|
||||
else:
|
||||
positions = List.from_maybe(positions)
|
||||
|
||||
ret = []
|
||||
for pos in positions:
|
||||
if isinstance(pos, (String, six.string_types)):
|
||||
pos_value = getattr(pos, 'value', pos)
|
||||
if pos_value in OPPOSITE_POSITIONS:
|
||||
if opposite:
|
||||
ret.append(OPPOSITE_POSITIONS[pos_value])
|
||||
else:
|
||||
ret.append(pos)
|
||||
continue
|
||||
elif pos_value == 'to':
|
||||
# Gradient syntax keyword; leave alone
|
||||
ret.append(pos)
|
||||
continue
|
||||
|
||||
elif isinstance(pos, Number):
|
||||
if pos.is_simple_unit('%'):
|
||||
if opposite:
|
||||
ret.append(Number(100 - pos.value, '%'))
|
||||
else:
|
||||
ret.append(pos)
|
||||
continue
|
||||
elif pos.is_simple_unit('deg'):
|
||||
# TODO support other angle types?
|
||||
if opposite:
|
||||
ret.append(Number((pos.value + 180) % 360, 'deg'))
|
||||
else:
|
||||
ret.append(pos)
|
||||
continue
|
||||
|
||||
if opposite:
|
||||
log.warn("Can't find opposite for position %r" % (pos,))
|
||||
ret.append(pos)
|
||||
|
||||
return List(ret, use_comma=False).maybe()
|
||||
|
||||
|
||||
@register('position')
|
||||
def position(p):
|
||||
return _position(False, p)
|
||||
|
||||
|
||||
@register('opposite-position')
|
||||
def opposite_position(p):
|
||||
return _position(True, p)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Math
|
||||
|
||||
@register('pi', 0)
|
||||
def pi():
|
||||
return Number(math.pi)
|
||||
|
||||
|
||||
@register('e', 0)
|
||||
def e():
|
||||
return Number(math.e)
|
||||
|
||||
|
||||
@register('log', 1)
|
||||
@register('log', 2)
|
||||
def log_(number, base=None):
|
||||
if not isinstance(number, Number):
|
||||
raise TypeError("Expected number, got %r" % (number,))
|
||||
elif not number.is_unitless:
|
||||
raise ValueError("Expected unitless number, got %r" % (number,))
|
||||
|
||||
if base is None:
|
||||
pass
|
||||
elif not isinstance(base, Number):
|
||||
raise TypeError("Expected number, got %r" % (base,))
|
||||
elif not base.is_unitless:
|
||||
raise ValueError("Expected unitless number, got %r" % (base,))
|
||||
|
||||
if base is None:
|
||||
ret = math.log(number.value)
|
||||
else:
|
||||
ret = math.log(number.value, base.value)
|
||||
|
||||
return Number(ret)
|
||||
|
||||
|
||||
@register('pow', 2)
|
||||
def pow(number, exponent):
|
||||
return number ** exponent
|
||||
|
||||
|
||||
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.sqrt), 'sqrt', 1)
|
||||
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.sin), 'sin', 1)
|
||||
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.cos), 'cos', 1)
|
||||
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.tan), 'tan', 1)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Fonts
|
||||
|
||||
def _fonts_root():
|
||||
return config.STATIC_ROOT if config.FONTS_ROOT is None else config.FONTS_ROOT
|
||||
|
||||
|
||||
def _font_url(path, only_path=False, cache_buster=True, inline=False):
|
||||
filepath = String.unquoted(path).value
|
||||
file = None
|
||||
FONTS_ROOT = _fonts_root()
|
||||
if callable(FONTS_ROOT):
|
||||
try:
|
||||
_file, _storage = list(FONTS_ROOT(filepath))[0]
|
||||
d_obj = _storage.modified_time(_file)
|
||||
filetime = int(time.mktime(d_obj.timetuple()))
|
||||
if inline:
|
||||
file = _storage.open(_file)
|
||||
except:
|
||||
filetime = 'NA'
|
||||
else:
|
||||
_path = os.path.join(FONTS_ROOT, filepath.strip('/'))
|
||||
if os.path.exists(_path):
|
||||
filetime = int(os.path.getmtime(_path))
|
||||
if inline:
|
||||
file = open(_path, 'rb')
|
||||
else:
|
||||
filetime = 'NA'
|
||||
|
||||
BASE_URL = config.FONTS_URL or config.STATIC_URL
|
||||
if file and inline:
|
||||
font_type = None
|
||||
if re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value):
|
||||
font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value).groups()[1]).value
|
||||
|
||||
if not FONT_TYPES.get(font_type):
|
||||
raise Exception('Could not determine font type for "%s"' % path.value)
|
||||
|
||||
mime = FONT_TYPES.get(font_type)
|
||||
if font_type == 'woff':
|
||||
mime = 'application/font-woff'
|
||||
elif font_type == 'eot':
|
||||
mime = 'application/vnd.ms-fontobject'
|
||||
url = 'data:' + (mime if '/' in mime else 'font/%s' % mime) + ';base64,' + base64.b64encode(file.read())
|
||||
file.close()
|
||||
else:
|
||||
url = '%s/%s' % (BASE_URL.rstrip('/'), filepath.lstrip('/'))
|
||||
if cache_buster and filetime != 'NA':
|
||||
url = add_cache_buster(url, filetime)
|
||||
|
||||
if not only_path:
|
||||
url = 'url(%s)' % escape(url)
|
||||
return String.unquoted(url)
|
||||
|
||||
|
||||
def _font_files(args, inline):
|
||||
if args == ():
|
||||
return String.unquoted("")
|
||||
|
||||
fonts = []
|
||||
args_len = len(args)
|
||||
skip_next = False
|
||||
for index in range(len(args)):
|
||||
arg = args[index]
|
||||
if not skip_next:
|
||||
font_type = args[index + 1] if args_len > (index + 1) else None
|
||||
if font_type and font_type.value in FONT_TYPES:
|
||||
skip_next = True
|
||||
else:
|
||||
if re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value):
|
||||
font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value).groups()[1])
|
||||
|
||||
if font_type.value in FONT_TYPES:
|
||||
fonts.append(String.unquoted('%s format("%s")' % (_font_url(arg, inline=inline), String.unquoted(FONT_TYPES[font_type.value]).value)))
|
||||
else:
|
||||
raise Exception('Could not determine font type for "%s"' % arg.value)
|
||||
else:
|
||||
skip_next = False
|
||||
|
||||
return List(fonts, separator=',')
|
||||
|
||||
|
||||
@register('font-url', 1)
|
||||
@register('font-url', 2)
|
||||
def font_url(path, only_path=False, cache_buster=True):
|
||||
"""
|
||||
Generates a path to an asset found relative to the project's font directory.
|
||||
Passing a true value as the second argument will cause the only the path to
|
||||
be returned instead of a `url()` function
|
||||
"""
|
||||
return _font_url(path, only_path, cache_buster, False)
|
||||
|
||||
|
||||
@register('font-files')
|
||||
def font_files(*args):
|
||||
return _font_files(args, inline=False)
|
||||
|
||||
|
||||
@register('inline-font-files')
|
||||
def inline_font_files(*args):
|
||||
return _font_files(args, inline=True)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# External stylesheets
|
||||
|
||||
@register('stylesheet-url', 1)
|
||||
@register('stylesheet-url', 2)
|
||||
def stylesheet_url(path, only_path=False, cache_buster=True):
|
||||
"""
|
||||
Generates a path to an asset found relative to the project's css directory.
|
||||
Passing a true value as the second argument will cause the only the path to
|
||||
be returned instead of a `url()` function
|
||||
"""
|
||||
filepath = String.unquoted(path).value
|
||||
if callable(config.STATIC_ROOT):
|
||||
try:
|
||||
_file, _storage = list(config.STATIC_ROOT(filepath))[0]
|
||||
d_obj = _storage.modified_time(_file)
|
||||
filetime = int(time.mktime(d_obj.timetuple()))
|
||||
except:
|
||||
filetime = 'NA'
|
||||
else:
|
||||
_path = os.path.join(config.STATIC_ROOT, filepath.strip('/'))
|
||||
if os.path.exists(_path):
|
||||
filetime = int(os.path.getmtime(_path))
|
||||
else:
|
||||
filetime = 'NA'
|
||||
BASE_URL = config.STATIC_URL
|
||||
|
||||
url = '%s%s' % (BASE_URL, filepath)
|
||||
if cache_buster:
|
||||
url = add_cache_buster(url, filetime)
|
||||
if not only_path:
|
||||
url = 'url("%s")' % (url)
|
||||
return String.unquoted(url)
|
||||
@@ -0,0 +1,285 @@
|
||||
"""Image utilities ported from Compass."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import mimetypes
|
||||
import os.path
|
||||
import time
|
||||
|
||||
import six
|
||||
from six.moves import xrange
|
||||
|
||||
from scss import config
|
||||
from scss.functions.compass import _image_size_cache
|
||||
from scss.functions.compass.helpers import add_cache_buster
|
||||
from scss.functions.library import FunctionLibrary
|
||||
from scss.types import Color, List, Number, String
|
||||
from scss.util import escape
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
try:
|
||||
import Image
|
||||
except:
|
||||
Image = None
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
COMPASS_IMAGES_LIBRARY = FunctionLibrary()
|
||||
register = COMPASS_IMAGES_LIBRARY.register
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
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]
|
||||
d_obj = _storage.modified_time(_file)
|
||||
filetime = int(time.mktime(d_obj.timetuple()))
|
||||
if inline or dst_color or spacing:
|
||||
path = _storage.open(_file)
|
||||
except:
|
||||
filetime = 'NA'
|
||||
else:
|
||||
_path = os.path.join(IMAGES_ROOT.rstrip('/'), filepath.strip('/'))
|
||||
if os.path.exists(_path):
|
||||
filetime = int(os.path.getmtime(_path))
|
||||
if inline or dst_color or spacing:
|
||||
path = open(_path, 'rb')
|
||||
else:
|
||||
filetime = 'NA'
|
||||
|
||||
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('\\', '_').replace('/', '_'))
|
||||
key = (filetime, src_color, dst_color, spacing)
|
||||
key = file_name + '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
|
||||
asset_file = 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 = 'data:' + mime_type + ';base64,' + base64.b64encode(path.read())
|
||||
else:
|
||||
url = '%s%s' % (BASE_URL, filepath)
|
||||
if cache_buster:
|
||||
filetime = int(os.path.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 = 'data:' + mime_type + ';base64,' + base64.b64encode(path.read())
|
||||
else:
|
||||
url = '%s%s' % (BASE_URL, filepath)
|
||||
if cache_buster:
|
||||
filetime = int(os.path.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 = int(os.path.getmtime(asset_path))
|
||||
except IOError:
|
||||
log.exception("Error while saving image")
|
||||
inline = True # Retry inline version
|
||||
url = os.path.join(config.ASSETS_URL.rstrip('/'), asset_file.lstrip('/'))
|
||||
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 = 'data:' + mime_type + ';base64,' + base64.b64encode(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 only_path:
|
||||
url = 'url(%s)' % escape(url)
|
||||
return String.unquoted(url)
|
||||
|
||||
|
||||
@register('inline-image', 1)
|
||||
@register('inline-image', 2)
|
||||
@register('inline-image', 3)
|
||||
@register('inline-image', 4)
|
||||
@register('inline-image', 5)
|
||||
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)
|
||||
|
||||
|
||||
@register('image-url', 1)
|
||||
@register('image-url', 2)
|
||||
@register('image-url', 3)
|
||||
@register('image-url', 4)
|
||||
@register('image-url', 5)
|
||||
@register('image-url', 6)
|
||||
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)
|
||||
|
||||
|
||||
@register('image-width', 1)
|
||||
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]
|
||||
path = _storage.open(_file)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
_path = os.path.join(IMAGES_ROOT, filepath.strip('/'))
|
||||
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')
|
||||
|
||||
|
||||
@register('image-height', 1)
|
||||
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]
|
||||
path = _storage.open(_file)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
_path = os.path.join(IMAGES_ROOT, filepath.strip('/'))
|
||||
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')
|
||||
@@ -0,0 +1,346 @@
|
||||
"""Functions used for generating packed CSS sprite maps.
|
||||
|
||||
|
||||
These are ported from the Binary Tree Bin Packing Algorithm:
|
||||
http://codeincomplete.com/posts/2011/5/7/bin_packing/
|
||||
"""
|
||||
|
||||
# Copyright (c) 2011, 2012, 2013 Jake Gordon and contributors
|
||||
# Copyright (c) 2013 German M. Bravo
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
|
||||
class LayoutNode(object):
|
||||
def __init__(self, x, y, w, h, down=None, right=None, used=False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.down = down
|
||||
self.right = right
|
||||
self.used = used
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
|
||||
@property
|
||||
def area(self):
|
||||
return self.width * self.height
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (%s, %s) [%sx%s]>' % (self.__class__.__name__, self.x, self.y, self.w, self.h)
|
||||
|
||||
|
||||
class SpritesLayout(object):
|
||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None):
|
||||
self.num_blocks = len(blocks)
|
||||
|
||||
if margin is None:
|
||||
margin = [[0] * 4] * self.num_blocks
|
||||
elif not isinstance(margin, (tuple, list)):
|
||||
margin = [[margin] * 4] * self.num_blocks
|
||||
elif not isinstance(margin[0], (tuple, list)):
|
||||
margin = [margin] * self.num_blocks
|
||||
|
||||
if padding is None:
|
||||
padding = [[0] * 4] * self.num_blocks
|
||||
elif not isinstance(padding, (tuple, list)):
|
||||
padding = [[padding] * 4] * self.num_blocks
|
||||
elif not isinstance(padding[0], (tuple, list)):
|
||||
padding = [padding] * self.num_blocks
|
||||
|
||||
if pmargin is None:
|
||||
pmargin = [[0.0] * 4] * self.num_blocks
|
||||
elif not isinstance(pmargin, (tuple, list)):
|
||||
pmargin = [[pmargin] * 4] * self.num_blocks
|
||||
elif not isinstance(pmargin[0], (tuple, list)):
|
||||
pmargin = [pmargin] * self.num_blocks
|
||||
|
||||
if ppadding is None:
|
||||
ppadding = [[0.0] * 4] * self.num_blocks
|
||||
elif not isinstance(ppadding, (tuple, list)):
|
||||
ppadding = [[ppadding] * 4] * self.num_blocks
|
||||
elif not isinstance(ppadding[0], (tuple, list)):
|
||||
ppadding = [ppadding] * self.num_blocks
|
||||
|
||||
self.blocks = tuple((
|
||||
b[0] + padding[i][3] + padding[i][1] + margin[i][3] + margin[i][1] + int(round(b[0] * (ppadding[i][3] + ppadding[i][1] + pmargin[i][3] + pmargin[i][1]))),
|
||||
b[1] + padding[i][0] + padding[i][2] + margin[i][0] + margin[i][2] + int(round(b[1] * (ppadding[i][0] + ppadding[i][2] + pmargin[i][0] + pmargin[i][2]))),
|
||||
b[0],
|
||||
b[1],
|
||||
i
|
||||
) for i, b in enumerate(blocks))
|
||||
|
||||
self.margin = margin
|
||||
self.padding = padding
|
||||
self.pmargin = pmargin
|
||||
self.ppadding = ppadding
|
||||
|
||||
|
||||
class PackedSpritesLayout(SpritesLayout):
|
||||
@staticmethod
|
||||
def MAXSIDE(a, b):
|
||||
"""maxside: Sort pack by maximum sides"""
|
||||
return cmp(max(b[0], b[1]), max(a[0], a[1])) or cmp(min(b[0], b[1]), min(a[0], a[1])) or cmp(b[1], a[1]) or cmp(b[0], a[0])
|
||||
|
||||
@staticmethod
|
||||
def WIDTH(a, b):
|
||||
"""width: Sort pack by width"""
|
||||
return cmp(b[0], a[0]) or cmp(b[1], a[1])
|
||||
|
||||
@staticmethod
|
||||
def HEIGHT(a, b):
|
||||
"""height: Sort pack by height"""
|
||||
return cmp(b[1], a[1]) or cmp(b[0], a[0])
|
||||
|
||||
@staticmethod
|
||||
def AREA(a, b):
|
||||
"""area: Sort pack by area"""
|
||||
return cmp(b[0] * b[1], a[0] * a[1]) or cmp(b[1], a[1]) or cmp(b[0], a[0])
|
||||
|
||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, methods=None):
|
||||
super(PackedSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin)
|
||||
|
||||
ratio = 0
|
||||
|
||||
if methods is None:
|
||||
methods = (self.MAXSIDE, self.WIDTH, self.HEIGHT, self.AREA)
|
||||
|
||||
for method in methods:
|
||||
sorted_blocks = sorted(
|
||||
self.blocks,
|
||||
cmp=method,
|
||||
)
|
||||
root = LayoutNode(
|
||||
x=0,
|
||||
y=0,
|
||||
w=sorted_blocks[0][0] if sorted_blocks else 0,
|
||||
h=sorted_blocks[0][1] if sorted_blocks else 0
|
||||
)
|
||||
|
||||
area = 0
|
||||
nodes = [None] * self.num_blocks
|
||||
|
||||
for block in sorted_blocks:
|
||||
w, h, width, height, i = block
|
||||
node = self._findNode(root, w, h)
|
||||
if node:
|
||||
node = self._splitNode(node, w, h)
|
||||
else:
|
||||
root = self._growNode(root, w, h)
|
||||
node = self._findNode(root, w, h)
|
||||
if node:
|
||||
node = self._splitNode(node, w, h)
|
||||
else:
|
||||
node = None
|
||||
nodes[i] = node
|
||||
node.width = width
|
||||
node.height = height
|
||||
area += node.area
|
||||
|
||||
this_ratio = area / float(root.w * root.h)
|
||||
# print method.__doc__, "%g%%" % (this_ratio * 100)
|
||||
if ratio < this_ratio:
|
||||
self.root = root
|
||||
self.nodes = nodes
|
||||
self.method = method
|
||||
ratio = this_ratio
|
||||
if ratio > 0.96:
|
||||
break
|
||||
# print self.method.__doc__, "%g%%" % (ratio * 100)
|
||||
|
||||
def __iter__(self):
|
||||
for i, node in enumerate(self.nodes):
|
||||
margin, padding = self.margin[i], self.padding[i]
|
||||
pmargin, ppadding = self.pmargin[i], self.ppadding[i]
|
||||
cssw = node.width + padding[3] + padding[1] + int(round(node.width * (ppadding[3] + ppadding[1]))) # image width plus padding
|
||||
cssh = node.height + padding[0] + padding[2] + int(round(node.height * (ppadding[0] + ppadding[2]))) # image height plus padding
|
||||
cssx = node.x + margin[3] + int(round(node.width * pmargin[3]))
|
||||
cssy = node.y + margin[0] + int(round(node.height * pmargin[0]))
|
||||
x = cssx + padding[3] + int(round(node.width * ppadding[3]))
|
||||
y = cssy + padding[0] + int(round(node.height * ppadding[0]))
|
||||
yield x, y, node.width, node.height, cssx, cssy, cssw, cssh
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.root.w
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.root.h
|
||||
|
||||
def _findNode(self, root, w, h):
|
||||
if root.used:
|
||||
return self._findNode(root.right, w, h) or self._findNode(root.down, w, h)
|
||||
elif w <= root.w and h <= root.h:
|
||||
return root
|
||||
else:
|
||||
return None
|
||||
|
||||
def _splitNode(self, node, w, h):
|
||||
node.used = True
|
||||
node.down = LayoutNode(
|
||||
x=node.x,
|
||||
y=node.y + h,
|
||||
w=node.w,
|
||||
h=node.h - h
|
||||
)
|
||||
node.right = LayoutNode(
|
||||
x=node.x + w,
|
||||
y=node.y,
|
||||
w=node.w - w,
|
||||
h=h
|
||||
)
|
||||
return node
|
||||
|
||||
def _growNode(self, root, w, h):
|
||||
canGrowDown = w <= root.w
|
||||
canGrowRight = h <= root.h
|
||||
|
||||
shouldGrowRight = canGrowRight and (root.h >= root.w + w) # attempt to keep square-ish by growing right when height is much greater than width
|
||||
shouldGrowDown = canGrowDown and (root.w >= root.h + h) # attempt to keep square-ish by growing down when width is much greater than height
|
||||
|
||||
if shouldGrowRight:
|
||||
return self._growRight(root, w, h)
|
||||
elif shouldGrowDown:
|
||||
return self._growDown(root, w, h)
|
||||
elif canGrowRight:
|
||||
return self._growRight(root, w, h)
|
||||
elif canGrowDown:
|
||||
return self._growDown(root, w, h)
|
||||
else:
|
||||
# need to ensure sensible root starting size to avoid this happening
|
||||
assert False, "Blocks must be properly sorted!"
|
||||
|
||||
def _growRight(self, root, w, h):
|
||||
root = LayoutNode(
|
||||
used=True,
|
||||
x=0,
|
||||
y=0,
|
||||
w=root.w + w,
|
||||
h=root.h,
|
||||
down=root,
|
||||
right=LayoutNode(
|
||||
x=root.w,
|
||||
y=0,
|
||||
w=w,
|
||||
h=root.h
|
||||
)
|
||||
)
|
||||
return root
|
||||
|
||||
def _growDown(self, root, w, h):
|
||||
root = LayoutNode(
|
||||
used=True,
|
||||
x=0,
|
||||
y=0,
|
||||
w=root.w,
|
||||
h=root.h + h,
|
||||
down=LayoutNode(
|
||||
x=0,
|
||||
y=root.h,
|
||||
w=root.w,
|
||||
h=h
|
||||
),
|
||||
right=root
|
||||
)
|
||||
return root
|
||||
|
||||
|
||||
class HorizontalSpritesLayout(SpritesLayout):
|
||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None):
|
||||
super(HorizontalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin)
|
||||
|
||||
self.width = sum(zip(*self.blocks)[0])
|
||||
self.height = max(zip(*self.blocks)[1])
|
||||
|
||||
if position is None:
|
||||
position = [0.0] * self.num_blocks
|
||||
elif not isinstance(position, (tuple, list)):
|
||||
position = [position] * self.num_blocks
|
||||
self.position = position
|
||||
|
||||
def __iter__(self):
|
||||
cx = 0
|
||||
for i, block in enumerate(self.blocks):
|
||||
w, h, width, height, i = block
|
||||
margin, padding = self.margin[i], self.padding[i]
|
||||
pmargin, ppadding = self.pmargin[i], self.ppadding[i]
|
||||
position = self.position[i]
|
||||
cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding
|
||||
cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding
|
||||
cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x
|
||||
cssy = int(round((self.height - cssh) * position)) # centered vertically
|
||||
x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding
|
||||
y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding
|
||||
yield x, y, width, height, cssx, cssy, cssw, cssh
|
||||
cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1])))
|
||||
|
||||
|
||||
class VerticalSpritesLayout(SpritesLayout):
|
||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None):
|
||||
super(VerticalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin)
|
||||
|
||||
self.width = max(zip(*self.blocks)[0])
|
||||
self.height = sum(zip(*self.blocks)[1])
|
||||
|
||||
if position is None:
|
||||
position = [0.0] * self.num_blocks
|
||||
elif not isinstance(position, (tuple, list)):
|
||||
position = [position] * self.num_blocks
|
||||
self.position = position
|
||||
|
||||
def __iter__(self):
|
||||
cy = 0
|
||||
for i, block in enumerate(self.blocks):
|
||||
w, h, width, height, i = block
|
||||
margin, padding = self.margin[i], self.padding[i]
|
||||
pmargin, ppadding = self.pmargin[i], self.ppadding[i]
|
||||
position = self.position[i]
|
||||
cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding
|
||||
cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding
|
||||
cssx = int(round((self.width - cssw) * position)) # centered horizontally
|
||||
cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y
|
||||
x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding
|
||||
y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding
|
||||
yield x, y, width, height, cssx, cssy, cssw, cssh
|
||||
cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2])))
|
||||
|
||||
|
||||
class DiagonalSpritesLayout(SpritesLayout):
|
||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None):
|
||||
super(DiagonalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin)
|
||||
self.width = sum(zip(*self.blocks)[0])
|
||||
self.height = sum(zip(*self.blocks)[1])
|
||||
|
||||
def __iter__(self):
|
||||
cx, cy = 0, 0
|
||||
for i, block in enumerate(self.blocks):
|
||||
w, h, width, height, i = block
|
||||
margin, padding = self.margin[i], self.padding[i]
|
||||
pmargin, ppadding = self.pmargin[i], self.ppadding[i]
|
||||
cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding
|
||||
cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding
|
||||
cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x
|
||||
cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y
|
||||
x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding
|
||||
y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding
|
||||
yield x, y, width, height, cssx, cssy, cssw, cssh
|
||||
cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1])))
|
||||
cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2])))
|
||||
@@ -0,0 +1,524 @@
|
||||
"""Functions used for generating CSS sprites.
|
||||
|
||||
These are ported from the Compass sprite library:
|
||||
http://compass-style.org/reference/compass/utilities/sprites/
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
|
||||
import base64
|
||||
import glob
|
||||
import hashlib
|
||||
import logging
|
||||
import os.path
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
try:
|
||||
import Image
|
||||
except:
|
||||
Image = None
|
||||
|
||||
from six.moves import xrange
|
||||
|
||||
from scss import config
|
||||
from scss.functions.compass import _image_size_cache
|
||||
from scss.functions.compass.layouts import PackedSpritesLayout, HorizontalSpritesLayout, VerticalSpritesLayout, DiagonalSpritesLayout
|
||||
from scss.functions.library import FunctionLibrary
|
||||
from scss.types import Color, List, Number, String, Boolean
|
||||
from scss.util import escape
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_SPRITE_MAPS = 4096
|
||||
KEEP_SPRITE_MAPS = int(MAX_SPRITE_MAPS * 0.8)
|
||||
|
||||
COMPASS_SPRITES_LIBRARY = FunctionLibrary()
|
||||
register = COMPASS_SPRITES_LIBRARY.register
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Compass-like functionality for sprites and images
|
||||
|
||||
sprite_maps = {}
|
||||
|
||||
|
||||
def alpha_composite(im1, im2, offset=None, box=None, opacity=1):
|
||||
im1size = im1.size
|
||||
im2size = im2.size
|
||||
if offset is None:
|
||||
offset = (0, 0)
|
||||
if box is None:
|
||||
box = (0, 0) + im2size
|
||||
o1x, o1y = offset
|
||||
o2x, o2y, o2w, o2h = box
|
||||
width = o2w - o2x
|
||||
height = o2h - o2y
|
||||
im1_data = im1.load()
|
||||
im2_data = im2.load()
|
||||
for y in xrange(height):
|
||||
for x in xrange(width):
|
||||
pos1 = o1x + x, o1y + y
|
||||
if pos1[0] >= im1size[0] or pos1[1] >= im1size[1]:
|
||||
continue
|
||||
pos2 = o2x + x, o2y + y
|
||||
if pos2[0] >= im2size[0] or pos2[1] >= im2size[1]:
|
||||
continue
|
||||
dr, dg, db, da = im1_data[pos1]
|
||||
sr, sg, sb, sa = im2_data[pos2]
|
||||
da /= 255.0
|
||||
sa /= 255.0
|
||||
sa *= opacity
|
||||
ida = da * (1 - sa)
|
||||
oa = (sa + ida)
|
||||
if oa:
|
||||
pixel = (
|
||||
int(round((sr * sa + dr * ida)) / oa),
|
||||
int(round((sg * sa + dg * ida)) / oa),
|
||||
int(round((sb * sa + db * ida)) / oa),
|
||||
int(round(255 * oa))
|
||||
)
|
||||
else:
|
||||
pixel = (0, 0, 0, 0)
|
||||
im1_data[pos1] = pixel
|
||||
return im1
|
||||
|
||||
|
||||
@register('sprite-map')
|
||||
def sprite_map(g, **kwargs):
|
||||
"""
|
||||
Generates a sprite map from the files matching the glob pattern.
|
||||
Uses the keyword-style arguments passed in to control the placement.
|
||||
|
||||
$direction - Sprite map layout. Can be `vertical` (default), `horizontal`, `diagonal` or `smart`.
|
||||
|
||||
$position - For `horizontal` and `vertical` directions, the position of the sprite. (defaults to `0`)
|
||||
$<sprite>-position - Position of a given sprite.
|
||||
|
||||
$padding, $spacing - Adds paddings to sprites (top, right, bottom, left). (defaults to `0, 0, 0, 0`)
|
||||
$<sprite>-padding, $<sprite>-spacing - Padding for a given sprite.
|
||||
|
||||
$dst-color - Together with `$src-color`, forms a map of source colors to be converted to destiny colors (same index of `$src-color` changed to `$dst-color`).
|
||||
$<sprite>-dst-color - Destiny colors for a given sprite. (defaults to `$dst-color`)
|
||||
|
||||
$src-color - Selects source colors to be converted to the corresponding destiny colors. (defaults to `black`)
|
||||
$<sprite>-dst-color - Source colors for a given sprite. (defaults to `$src-color`)
|
||||
|
||||
$collapse - Collapses every image in the sprite map to a fixed size (`x` and `y`).
|
||||
$collapse-x - Collapses a size for `x`.
|
||||
$collapse-y - Collapses a size for `y`.
|
||||
"""
|
||||
if not Image:
|
||||
raise Exception("Images manipulation require PIL")
|
||||
|
||||
now_time = time.time()
|
||||
|
||||
g = String(g, quotes=None).value
|
||||
|
||||
if g in sprite_maps:
|
||||
sprite_maps[glob]['*'] = now_time
|
||||
elif '..' not in g: # Protect against going to prohibited places...
|
||||
if callable(config.STATIC_ROOT):
|
||||
glob_path = g
|
||||
rfiles = files = sorted(config.STATIC_ROOT(g))
|
||||
else:
|
||||
glob_path = os.path.join(config.STATIC_ROOT, g)
|
||||
files = glob.glob(glob_path)
|
||||
files = sorted((f, None) for f in files)
|
||||
rfiles = [(rf[len(config.STATIC_ROOT):], s) for rf, s in files]
|
||||
|
||||
if not files:
|
||||
log.error("Nothing found at '%s'", glob_path)
|
||||
return String.unquoted('')
|
||||
|
||||
map_name = os.path.normpath(os.path.dirname(g)).replace('\\', '_').replace('/', '_')
|
||||
key = list(zip(*files)[0]) + [repr(kwargs), config.ASSETS_URL]
|
||||
key = map_name + '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
|
||||
asset_file = key + '.png'
|
||||
ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets')
|
||||
asset_path = os.path.join(ASSETS_ROOT, asset_file)
|
||||
cache_path = os.path.join(config.CACHE_ROOT or ASSETS_ROOT, asset_file + '.cache')
|
||||
|
||||
inline = Boolean(kwargs.get('inline', False))
|
||||
|
||||
sprite_map = None
|
||||
asset = None
|
||||
file_asset = None
|
||||
inline_asset = None
|
||||
if os.path.exists(asset_path) or inline:
|
||||
try:
|
||||
save_time, file_asset, inline_asset, sprite_map, sizes = pickle.load(open(cache_path))
|
||||
if file_asset:
|
||||
sprite_maps[file_asset.render()] = sprite_map
|
||||
if inline_asset:
|
||||
sprite_maps[inline_asset.render()] = sprite_map
|
||||
if inline:
|
||||
asset = inline_asset
|
||||
else:
|
||||
asset = file_asset
|
||||
except:
|
||||
pass
|
||||
|
||||
if sprite_map:
|
||||
for file_, storage in files:
|
||||
if storage is not None:
|
||||
d_obj = storage.modified_time(file_)
|
||||
_time = time.mktime(d_obj.timetuple())
|
||||
else:
|
||||
_time = os.path.getmtime(file_)
|
||||
if save_time < _time:
|
||||
if _time > now_time:
|
||||
log.warning("File '%s' has a date in the future (cache ignored)" % file_)
|
||||
sprite_map = None # Invalidate cached sprite map
|
||||
break
|
||||
|
||||
if sprite_map is None or asset is None:
|
||||
cache_buster = Boolean(kwargs.get('cache_buster', True))
|
||||
direction = String.unquoted(kwargs.get('direction', config.SPRTE_MAP_DIRECTION)).value
|
||||
repeat = String.unquoted(kwargs.get('repeat', 'no-repeat')).value
|
||||
collapse = kwargs.get('collapse', Number(0))
|
||||
if isinstance(collapse, List):
|
||||
collapse_x = int(Number(collapse[0]).value)
|
||||
collapse_y = int(Number(collapse[-1]).value)
|
||||
else:
|
||||
collapse_x = collapse_y = int(Number(collapse).value)
|
||||
if 'collapse_x' in kwargs:
|
||||
collapse_x = int(Number(kwargs['collapse_x']).value)
|
||||
if 'collapse_y' in kwargs:
|
||||
collapse_y = int(Number(kwargs['collapse_y']).value)
|
||||
|
||||
position = Number(kwargs.get('position', 0))
|
||||
if not position.is_simple_unit('%') and position.value > 1:
|
||||
position = position.value / 100.0
|
||||
else:
|
||||
position = position.value
|
||||
if position < 0:
|
||||
position = 0.0
|
||||
elif position > 1:
|
||||
position = 1.0
|
||||
|
||||
padding = kwargs.get('padding', kwargs.get('spacing', Number(0)))
|
||||
padding = [int(Number(v).value) for v in List.from_maybe(padding)]
|
||||
padding = (padding * 4)[:4]
|
||||
|
||||
dst_colors = kwargs.get('dst_color')
|
||||
dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_colors) if v]
|
||||
src_colors = kwargs.get('src_color', Color.from_name('black'))
|
||||
src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_colors)]
|
||||
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]
|
||||
|
||||
def images(f=lambda x: x):
|
||||
for file_, storage in f(files):
|
||||
if storage is not None:
|
||||
_file = storage.open(file_)
|
||||
else:
|
||||
_file = file_
|
||||
_image = Image.open(_file)
|
||||
yield _image
|
||||
|
||||
names = tuple(os.path.splitext(os.path.basename(file_))[0] for file_, storage in files)
|
||||
|
||||
has_dst_colors = False
|
||||
all_dst_colors = []
|
||||
all_src_colors = []
|
||||
all_positions = []
|
||||
all_paddings = []
|
||||
|
||||
for name in names:
|
||||
name = name.replace('-', '_')
|
||||
|
||||
_position = kwargs.get(name + '_position')
|
||||
if _position is None:
|
||||
_position = position
|
||||
else:
|
||||
_position = Number(_position)
|
||||
if not _position.is_simple_unit('%') and _position.value > 1:
|
||||
_position = _position.value / 100.0
|
||||
else:
|
||||
_position = _position.value
|
||||
if _position < 0:
|
||||
_position = 0.0
|
||||
elif _position > 1:
|
||||
_position = 1.0
|
||||
all_positions.append(_position)
|
||||
|
||||
_padding = kwargs.get(name + '_padding', kwargs.get(name + '_spacing'))
|
||||
if _padding is None:
|
||||
_padding = padding
|
||||
else:
|
||||
_padding = [int(Number(v).value) for v in List.from_maybe(_padding)]
|
||||
_padding = (_padding * 4)[:4]
|
||||
all_paddings.append(_padding)
|
||||
|
||||
_dst_colors = kwargs.get(name + '_dst_color')
|
||||
if _dst_colors is None:
|
||||
_dst_colors = dst_colors
|
||||
if dst_colors:
|
||||
has_dst_colors = True
|
||||
else:
|
||||
has_dst_colors = True
|
||||
_dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(_dst_colors) if v]
|
||||
_src_colors = kwargs.get(name + '_src_color', Color.from_name('black'))
|
||||
if _src_colors is None:
|
||||
_src_colors = src_colors
|
||||
else:
|
||||
_src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(_src_colors)]
|
||||
_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]
|
||||
all_dst_colors.append(_dst_colors)
|
||||
all_src_colors.append(_src_colors)
|
||||
|
||||
sizes = tuple((collapse_x or i.size[0], collapse_y or i.size[1]) for i in images())
|
||||
|
||||
if direction == 'horizontal':
|
||||
layout = HorizontalSpritesLayout(sizes, all_paddings, position=all_positions)
|
||||
elif direction == 'vertical':
|
||||
layout = VerticalSpritesLayout(sizes, all_paddings, position=all_positions)
|
||||
elif direction == 'diagonal':
|
||||
layout = DiagonalSpritesLayout(sizes, all_paddings)
|
||||
elif direction == 'smart':
|
||||
layout = PackedSpritesLayout(sizes, all_paddings)
|
||||
else:
|
||||
raise Exception("Invalid direction %r" % (direction,))
|
||||
layout_positions = list(layout)
|
||||
|
||||
new_image = Image.new(
|
||||
mode='RGBA',
|
||||
size=(layout.width, layout.height),
|
||||
color=(0, 0, 0, 0)
|
||||
)
|
||||
|
||||
useless_dst_color = has_dst_colors
|
||||
|
||||
offsets_x = []
|
||||
offsets_y = []
|
||||
for i, image in enumerate(images()):
|
||||
x, y, width, height, cssx, cssy, cssw, cssh = layout_positions[i]
|
||||
iwidth, iheight = image.size
|
||||
|
||||
if has_dst_colors:
|
||||
pixdata = image.load()
|
||||
for _y in xrange(iheight):
|
||||
for _x in xrange(iwidth):
|
||||
pixel = pixdata[_x, _y]
|
||||
a = pixel[3] if len(pixel) == 4 else 255
|
||||
if a:
|
||||
rgb = pixel[:3]
|
||||
for j, dst_color in enumerate(all_dst_colors[i]):
|
||||
if rgb == all_src_colors[i][j]:
|
||||
new_color = tuple([int(c) for c in dst_color] + [a])
|
||||
if pixel != new_color:
|
||||
pixdata[_x, _y] = new_color
|
||||
useless_dst_color = False
|
||||
break
|
||||
|
||||
if iwidth != width or iheight != height:
|
||||
cy = 0
|
||||
while cy < iheight:
|
||||
cx = 0
|
||||
while cx < iwidth:
|
||||
new_image = alpha_composite(new_image, image, (x, y), (cx, cy, cx + width, cy + height))
|
||||
cx += width
|
||||
cy += height
|
||||
else:
|
||||
new_image.paste(image, (x, y))
|
||||
offsets_x.append(cssx)
|
||||
offsets_y.append(cssy)
|
||||
|
||||
if useless_dst_color:
|
||||
log.warning("Useless use of $dst-color in sprite map for files at '%s' (never used for)" % glob_path)
|
||||
|
||||
filetime = int(now_time)
|
||||
|
||||
if not inline:
|
||||
try:
|
||||
new_image.save(asset_path)
|
||||
url = '%s%s' % (config.ASSETS_URL, asset_file)
|
||||
if cache_buster:
|
||||
url += '?_=%s' % filetime
|
||||
except IOError:
|
||||
log.exception("Error while saving image")
|
||||
inline = True
|
||||
if inline:
|
||||
output = six.BytesIO()
|
||||
new_image.save(output, format='PNG')
|
||||
contents = output.getvalue()
|
||||
output.close()
|
||||
mime_type = 'image/png'
|
||||
url = 'data:' + mime_type + ';base64,' + base64.b64encode(contents)
|
||||
|
||||
url = 'url(%s)' % escape(url)
|
||||
if inline:
|
||||
asset = inline_asset = List([String.unquoted(url), String.unquoted(repeat)])
|
||||
else:
|
||||
asset = file_asset = List([String.unquoted(url), String.unquoted(repeat)])
|
||||
|
||||
# Add the new object:
|
||||
sprite_map = dict(zip(names, zip(sizes, rfiles, offsets_x, offsets_y)))
|
||||
sprite_map['*'] = now_time
|
||||
sprite_map['*f*'] = asset_file
|
||||
sprite_map['*k*'] = key
|
||||
sprite_map['*n*'] = map_name
|
||||
sprite_map['*t*'] = filetime
|
||||
|
||||
cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT)
|
||||
pickle.dump((now_time, file_asset, inline_asset, sprite_map, zip(files, sizes)), cache_tmp)
|
||||
cache_tmp.close()
|
||||
os.rename(cache_tmp.name, cache_path)
|
||||
|
||||
# Use the sorted list to remove older elements (keep only 500 objects):
|
||||
if len(sprite_maps) > MAX_SPRITE_MAPS:
|
||||
for a in sorted(sprite_maps, key=lambda a: sprite_maps[a]['*'], reverse=True)[KEEP_SPRITE_MAPS:]:
|
||||
del sprite_maps[a]
|
||||
log.warning("Exceeded maximum number of sprite maps (%s)" % MAX_SPRITE_MAPS)
|
||||
sprite_maps[asset.render()] = sprite_map
|
||||
for file_, size in sizes:
|
||||
_image_size_cache[file_] = size
|
||||
# TODO this sometimes returns an empty list, or is never assigned to
|
||||
return asset
|
||||
|
||||
|
||||
@register('sprite-map-name', 1)
|
||||
def sprite_map_name(map):
|
||||
"""
|
||||
Returns the name of a sprite map The name is derived from the folder than
|
||||
contains the sprites.
|
||||
"""
|
||||
map = map.render()
|
||||
sprite_map = sprite_maps.get(map)
|
||||
if not sprite_map:
|
||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
||||
if sprite_map:
|
||||
return String.unquoted(sprite_map['*n*'])
|
||||
return String.unquoted('')
|
||||
|
||||
|
||||
@register('sprite-file', 2)
|
||||
def sprite_file(map, sprite):
|
||||
"""
|
||||
Returns the relative path (from the images directory) to the original file
|
||||
used when construction the sprite. This is suitable for passing to the
|
||||
image_width and image_height helpers.
|
||||
"""
|
||||
map = map.render()
|
||||
sprite_map = sprite_maps.get(map)
|
||||
sprite_name = String.unquoted(sprite).value
|
||||
sprite = sprite_map and sprite_map.get(sprite_name)
|
||||
if not sprite_map:
|
||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
||||
elif not sprite:
|
||||
log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True})
|
||||
if sprite:
|
||||
return String(sprite[1][0])
|
||||
return String.unquoted('')
|
||||
|
||||
|
||||
@register('sprites', 1)
|
||||
@register('sprite-names', 1)
|
||||
def sprites(map):
|
||||
map = map.render()
|
||||
sprite_map = sprite_maps.get(map, {})
|
||||
return List(list(String.unquoted(s) for s in sorted(s for s in sprite_map if not s.startswith('*'))))
|
||||
|
||||
|
||||
@register('sprite', 2)
|
||||
@register('sprite', 3)
|
||||
@register('sprite', 4)
|
||||
@register('sprite', 5)
|
||||
def sprite(map, sprite, offset_x=None, offset_y=None, cache_buster=True):
|
||||
"""
|
||||
Returns the image and background position for use in a single shorthand
|
||||
property
|
||||
"""
|
||||
map = map.render()
|
||||
sprite_map = sprite_maps.get(map)
|
||||
sprite_name = String.unquoted(sprite).value
|
||||
sprite = sprite_map and sprite_map.get(sprite_name)
|
||||
if not sprite_map:
|
||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
||||
elif not sprite:
|
||||
log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True})
|
||||
if sprite:
|
||||
url = '%s%s' % (config.ASSETS_URL, sprite_map['*f*'])
|
||||
if cache_buster:
|
||||
url += '?_=%s' % sprite_map['*t*']
|
||||
x = Number(offset_x or 0, 'px')
|
||||
y = Number(offset_y or 0, 'px')
|
||||
if not x.value or (x.value <= -1 or x.value >= 1) and not x.is_simple_unit('%'):
|
||||
x -= Number(sprite[2], 'px')
|
||||
if not y.value or (y.value <= -1 or y.value >= 1) and not y.is_simple_unit('%'):
|
||||
y -= Number(sprite[3], 'px')
|
||||
url = "url(%s)" % escape(url)
|
||||
return List([String.unquoted(url), x, y])
|
||||
return List([Number(0), Number(0)])
|
||||
|
||||
|
||||
@register('sprite-url', 1)
|
||||
@register('sprite-url', 2)
|
||||
def sprite_url(map, cache_buster=True):
|
||||
"""
|
||||
Returns a url to the sprite image.
|
||||
"""
|
||||
map = map.render()
|
||||
sprite_map = sprite_maps.get(map)
|
||||
if not sprite_map:
|
||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
||||
if sprite_map:
|
||||
url = '%s%s' % (config.ASSETS_URL, sprite_map['*f*'])
|
||||
if cache_buster:
|
||||
url += '?_=%s' % sprite_map['*t*']
|
||||
url = "url(%s)" % escape(url)
|
||||
return String.unquoted(url)
|
||||
return String.unquoted('')
|
||||
|
||||
|
||||
@register('sprite-position', 2)
|
||||
@register('sprite-position', 3)
|
||||
@register('sprite-position', 4)
|
||||
def sprite_position(map, sprite, offset_x=None, offset_y=None):
|
||||
"""
|
||||
Returns the position for the original image in the sprite.
|
||||
This is suitable for use as a value to background-position.
|
||||
"""
|
||||
map = map.render()
|
||||
sprite_map = sprite_maps.get(map)
|
||||
sprite_name = String.unquoted(sprite).value
|
||||
sprite = sprite_map and sprite_map.get(sprite_name)
|
||||
if not sprite_map:
|
||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
||||
elif not sprite:
|
||||
log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True})
|
||||
if sprite:
|
||||
x = None
|
||||
if offset_x is not None and not isinstance(offset_x, Number):
|
||||
x = offset_x
|
||||
if not x or x.value not in ('left', 'right', 'center'):
|
||||
if x:
|
||||
offset_x = None
|
||||
x = Number(offset_x or 0, 'px')
|
||||
if not x.value or (x.value <= -1 or x.value >= 1) and not x.is_simple_unit('%'):
|
||||
x -= Number(sprite[2], 'px')
|
||||
y = None
|
||||
if offset_y is not None and not isinstance(offset_y, Number):
|
||||
y = offset_y
|
||||
if not y or y.value not in ('top', 'bottom', 'center'):
|
||||
if y:
|
||||
offset_y = None
|
||||
y = Number(offset_y or 0, 'px')
|
||||
if not y.value or (y.value <= -1 or y.value >= 1) and not y.is_simple_unit('%'):
|
||||
y -= Number(sprite[3], 'px')
|
||||
return List([x, y])
|
||||
return List([Number(0), Number(0)])
|
||||
Reference in New Issue
Block a user