Livereload css

This commit is contained in:
Ruud
2014-04-09 16:07:59 +02:00
parent 685210aee3
commit e8fe9da602
37 changed files with 13370 additions and 57 deletions
+2
View File
@@ -0,0 +1,2 @@
# Global cache of image sizes, shared between sprites and images libraries.
_image_size_cache = {}
+400
View File
@@ -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)
+654
View File
@@ -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)
+285
View File
@@ -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')
+346
View File
@@ -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])))
+524
View File
@@ -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)])