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
+25
View File
@@ -0,0 +1,25 @@
from __future__ import absolute_import
from scss.functions.library import FunctionLibrary
from scss.functions.core import CORE_LIBRARY
from scss.functions.extra import EXTRA_LIBRARY
from scss.functions.compass.sprites import COMPASS_SPRITES_LIBRARY
from scss.functions.compass.gradients import COMPASS_GRADIENTS_LIBRARY
from scss.functions.compass.helpers import COMPASS_HELPERS_LIBRARY
from scss.functions.compass.images import COMPASS_IMAGES_LIBRARY
ALL_BUILTINS_LIBRARY = FunctionLibrary()
ALL_BUILTINS_LIBRARY.inherit(
CORE_LIBRARY,
EXTRA_LIBRARY,
COMPASS_GRADIENTS_LIBRARY,
COMPASS_HELPERS_LIBRARY,
COMPASS_IMAGES_LIBRARY,
COMPASS_SPRITES_LIBRARY,
)
# TODO back-compat for the only codebase still using the old name :)
FunctionRegistry = FunctionLibrary
scss_builtins = ALL_BUILTINS_LIBRARY
+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)])
+784
View File
@@ -0,0 +1,784 @@
"""Functions from the Sass "standard library", i.e., built into the original
Ruby implementation.
"""
from __future__ import absolute_import
from __future__ import division
import logging
import math
from six.moves import xrange
from scss.functions.library import FunctionLibrary
from scss.types import Boolean, Color, List, Null, Number, String, Map, expect_type
log = logging.getLogger(__name__)
CORE_LIBRARY = FunctionLibrary()
register = CORE_LIBRARY.register
# ------------------------------------------------------------------------------
# Color creation
def _interpret_percentage(n, relto=1., clamp=True):
expect_type(n, Number, unit='%')
if n.is_unitless:
ret = n.value / relto
else:
ret = n.value / 100
if clamp:
if ret < 0:
return 0
elif ret > 1:
return 1
return ret
@register('rgba', 4)
def rgba(r, g, b, a):
r = _interpret_percentage(r, relto=255)
g = _interpret_percentage(g, relto=255)
b = _interpret_percentage(b, relto=255)
a = _interpret_percentage(a, relto=1)
return Color.from_rgb(r, g, b, a)
@register('rgb', 3)
def rgb(r, g, b, type='rgb'):
return rgba(r, g, b, Number(1.0))
@register('rgba', 1)
@register('rgba', 2)
def rgba2(color, a=None):
if a is None:
alpha = 1
else:
alpha = _interpret_percentage(a)
return Color.from_rgb(*color.rgba[:3], alpha=alpha)
@register('rgb', 1)
def rgb1(color):
return rgba2(color, a=Number(1))
@register('hsla', 4)
def hsla(h, s, l, a):
return Color.from_hsl(
h.value / 360 % 1,
# Ruby sass treats plain numbers for saturation and lightness as though
# they were percentages, just without the %
_interpret_percentage(s, relto=100),
_interpret_percentage(l, relto=100),
alpha=a.value,
)
@register('hsl', 3)
def hsl(h, s, l):
return hsla(h, s, l, Number(1))
@register('hsla', 1)
@register('hsla', 2)
def hsla2(color, a=None):
return rgba2(color, a)
@register('hsl', 1)
def hsl1(color):
return rgba2(color, a=Number(1))
@register('mix', 2)
@register('mix', 3)
def mix(color1, color2, weight=Number(50, "%")):
"""
Mixes together two colors. Specifically, takes the average of each of the
RGB components, optionally weighted by the given percentage.
The opacity of the colors is also considered when weighting the components.
Specifically, takes the average of each of the RGB components,
optionally weighted by the given percentage.
The opacity of the colors is also considered when weighting the components.
The weight specifies the amount of the first color that should be included
in the returned color.
50%, means that half the first color
and half the second color should be used.
25% means that a quarter of the first color
and three quarters of the second color should be used.
For example:
mix(#f00, #00f) => #7f007f
mix(#f00, #00f, 25%) => #3f00bf
mix(rgba(255, 0, 0, 0.5), #00f) => rgba(63, 0, 191, 0.75)
"""
# This algorithm factors in both the user-provided weight
# and the difference between the alpha values of the two colors
# to decide how to perform the weighted average of the two RGB values.
#
# It works by first normalizing both parameters to be within [-1, 1],
# where 1 indicates "only use color1", -1 indicates "only use color 0",
# and all values in between indicated a proportionately weighted average.
#
# Once we have the normalized variables w and a,
# we apply the formula (w + a)/(1 + w*a)
# to get the combined weight (in [-1, 1]) of color1.
# This formula has two especially nice properties:
#
# * When either w or a are -1 or 1, the combined weight is also that number
# (cases where w * a == -1 are undefined, and handled as a special case).
#
# * When a is 0, the combined weight is w, and vice versa
#
# Finally, the weight of color1 is renormalized to be within [0, 1]
# and the weight of color2 is given by 1 minus the weight of color1.
#
# Algorithm from the Sass project: http://sass-lang.com/
p = _interpret_percentage(weight)
# Scale weight to [-1, 1]
w = p * 2 - 1
# Compute difference in alpha channels
a = color1.alpha - color2.alpha
# Weight of first color
if w * a == -1:
# Avoid zero-div case
scaled_weight1 = w
else:
scaled_weight1 = (w + a) / (1 + w * a)
# Unscale back to [0, 1] and get the weight of the other color
w1 = (scaled_weight1 + 1) / 2
w2 = 1 - w1
# Do the scaling. Note that alpha isn't scaled by alpha, as that wouldn't
# make much sense; it uses the original untwiddled weight, p.
channels = [
ch1 * w1 + ch2 * w2
for (ch1, ch2) in zip(color1.rgba[:3], color2.rgba[:3])]
alpha = color1.alpha * p + color2.alpha * (1 - p)
return Color.from_rgb(*channels, alpha=alpha)
# ------------------------------------------------------------------------------
# Color inspection
@register('red', 1)
def red(color):
r, g, b, a = color.rgba
return Number(r * 255)
@register('green', 1)
def green(color):
r, g, b, a = color.rgba
return Number(g * 255)
@register('blue', 1)
def blue(color):
r, g, b, a = color.rgba
return Number(b * 255)
@register('opacity', 1)
@register('alpha', 1)
def alpha(color):
return Number(color.alpha)
@register('hue', 1)
def hue(color):
h, s, l = color.hsl
return Number(h * 360, "deg")
@register('saturation', 1)
def saturation(color):
h, s, l = color.hsl
return Number(s * 100, "%")
@register('lightness', 1)
def lightness(color):
h, s, l = color.hsl
return Number(l * 100, "%")
@register('ie-hex-str', 1)
def ie_hex_str(color):
c = Color(color).value
return String(u'#%02X%02X%02X%02X' % (round(c[3] * 255), round(c[0]), round(c[1]), round(c[2])))
# ------------------------------------------------------------------------------
# Color modification
@register('fade-in', 2)
@register('fadein', 2)
@register('opacify', 2)
def opacify(color, amount):
r, g, b, a = color.rgba
return Color.from_rgb(
r, g, b,
alpha=color.alpha + amount.value)
@register('fade-out', 2)
@register('fadeout', 2)
@register('transparentize', 2)
def transparentize(color, amount):
r, g, b, a = color.rgba
return Color.from_rgb(
r, g, b,
alpha=color.alpha - amount.value)
@register('lighten', 2)
def lighten(color, amount):
return adjust_color(color, lightness=amount)
@register('darken', 2)
def darken(color, amount):
return adjust_color(color, lightness=-amount)
@register('saturate', 2)
def saturate(color, amount):
return adjust_color(color, saturation=amount)
@register('desaturate', 2)
def desaturate(color, amount):
return adjust_color(color, saturation=-amount)
@register('greyscale', 1)
def greyscale(color):
h, s, l = color.hsl
return Color.from_hsl(h, 0, l, alpha=color.alpha)
@register('grayscale', 1)
def grayscale(color):
if isinstance(color, Number) and color.is_unitless:
# grayscale(n) is a CSS3 filter and should be left intact, but only
# when using the "a" spelling
return String.unquoted("grayscale(%d)" % (color.value,))
else:
return greyscale(color)
@register('spin', 2)
@register('adjust-hue', 2)
def adjust_hue(color, degrees):
h, s, l = color.hsl
delta = degrees.value / 360
return Color.from_hsl((h + delta) % 1, s, l, alpha=color.alpha)
@register('complement', 1)
def complement(color):
h, s, l = color.hsl
return Color.from_hsl((h + 0.5) % 1, s, l, alpha=color.alpha)
@register('invert', 1)
def invert(color):
"""
Returns the inverse (negative) of a color.
The red, green, and blue values are inverted, while the opacity is left alone.
"""
r, g, b, a = color.rgba
return Color.from_rgb(1 - r, 1 - g, 1 - b, alpha=a)
@register('adjust-lightness', 2)
def adjust_lightness(color, amount):
return adjust_color(color, lightness=amount)
@register('adjust-saturation', 2)
def adjust_saturation(color, amount):
return adjust_color(color, saturation=amount)
@register('scale-lightness', 2)
def scale_lightness(color, amount):
return scale_color(color, lightness=amount)
@register('scale-saturation', 2)
def scale_saturation(color, amount):
return scale_color(color, saturation=amount)
@register('adjust-color')
def adjust_color(color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None):
do_rgb = red or green or blue
do_hsl = hue or saturation or lightness
if do_rgb and do_hsl:
raise ValueError("Can't adjust both RGB and HSL channels at the same time")
zero = Number(0)
a = color.alpha + (alpha or zero).value
if do_rgb:
r, g, b = color.rgba[:3]
channels = [
current + (adjustment or zero).value / 255
for (current, adjustment) in zip(color.rgba, (red, green, blue))]
return Color.from_rgb(*channels, alpha=a)
else:
h, s, l = color.hsl
h = (h + (hue or zero).value / 360) % 1
s += _interpret_percentage(saturation or zero, relto=100, clamp=False)
l += _interpret_percentage(lightness or zero, relto=100, clamp=False)
return Color.from_hsl(h, s, l, a)
def _scale_channel(channel, scaleby):
if scaleby is None:
return channel
expect_type(scaleby, Number)
if not scaleby.is_simple_unit('%'):
raise ValueError("Expected percentage, got %r" % (scaleby,))
factor = scaleby.value / 100
if factor > 0:
# Add x% of the remaining range, up to 1
return channel + (1 - channel) * factor
else:
# Subtract x% of the existing channel. We add here because the factor
# is already negative
return channel * (1 + factor)
@register('scale-color')
def scale_color(color, red=None, green=None, blue=None, saturation=None, lightness=None, alpha=None):
do_rgb = red or green or blue
do_hsl = saturation or lightness
if do_rgb and do_hsl:
raise ValueError("Can't scale both RGB and HSL channels at the same time")
scaled_alpha = _scale_channel(color.alpha, alpha)
if do_rgb:
channels = [
_scale_channel(channel, scaleby)
for channel, scaleby in zip(color.rgba, (red, green, blue))]
return Color.from_rgb(*channels, alpha=scaled_alpha)
else:
channels = [
_scale_channel(channel, scaleby)
for channel, scaleby in zip(color.hsl, (None, saturation, lightness))]
return Color.from_hsl(*channels, alpha=scaled_alpha)
@register('change-color')
def change_color(color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None):
do_rgb = red or green or blue
do_hsl = hue or saturation or lightness
if do_rgb and do_hsl:
raise ValueError("Can't change both RGB and HSL channels at the same time")
if alpha is None:
alpha = color.alpha
else:
alpha = alpha.value
if do_rgb:
channels = list(color.rgba[:3])
if red:
channels[0] = _interpret_percentage(red, relto=255)
if green:
channels[1] = _interpret_percentage(green, relto=255)
if blue:
channels[2] = _interpret_percentage(blue, relto=255)
return Color.from_rgb(*channels, alpha=alpha)
else:
channels = list(color.hsl)
if hue:
expect_type(hue, Number, unit=None)
channels[0] = (hue.value / 360) % 1
# Ruby sass treats plain numbers for saturation and lightness as though
# they were percentages, just without the %
if saturation:
channels[1] = _interpret_percentage(saturation, relto=100)
if lightness:
channels[2] = _interpret_percentage(lightness, relto=100)
return Color.from_hsl(*channels, alpha=alpha)
# ------------------------------------------------------------------------------
# String functions
@register('e', 1)
@register('escape', 1)
@register('unquote')
def unquote(*args):
arg = List.from_maybe_starargs(args).maybe()
if isinstance(arg, String):
return String(arg.value, quotes=None)
else:
return String(arg.render(), quotes=None)
@register('quote')
def quote(*args):
arg = List.from_maybe_starargs(args).maybe()
if isinstance(arg, String):
return String(arg.value, quotes='"')
else:
return String(arg.render(), quotes='"')
@register('str-length', 1)
def str_length(string):
expect_type(string, String)
# nb: can't use `len(string)`, because that gives the Sass list length,
# which is 1
return Number(len(string.value))
# TODO this and several others should probably also require integers
# TODO and assert that the indexes are valid
@register('str-insert', 3)
def str_insert(string, insert, index):
expect_type(string, String)
expect_type(insert, String)
expect_type(index, Number, unit=None)
py_index = index.to_python_index(len(string.value), check_bounds=False)
return String(
string.value[:py_index] +
insert.value +
string.value[py_index:],
quotes=string.quotes)
@register('str-index', 2)
def str_index(string, substring):
expect_type(string, String)
expect_type(substring, String)
# 1-based indexing, with 0 for failure
return Number(string.value.find(substring.value) + 1)
@register('str-slice', 2)
@register('str-slice', 3)
def str_slice(string, start_at, end_at=None):
expect_type(string, String)
expect_type(start_at, Number, unit=None)
py_start_at = start_at.to_python_index(len(string.value))
if end_at is None:
py_end_at = None
else:
expect_type(end_at, Number, unit=None)
# Endpoint is inclusive, unlike Python
py_end_at = end_at.to_python_index(len(string.value)) + 1
return String(
string.value[py_start_at:py_end_at],
quotes=string.quotes)
@register('to-upper-case', 1)
def to_upper_case(string):
expect_type(string, String)
return String(string.value.upper(), quotes=string.quotes)
@register('to-lower-case', 1)
def to_lower_case(string):
expect_type(string, String)
return String(string.value.lower(), quotes=string.quotes)
# ------------------------------------------------------------------------------
# Number functions
@register('percentage', 1)
def percentage(value):
expect_type(value, Number, unit=None)
return value * Number(100, unit='%')
CORE_LIBRARY.add(Number.wrap_python_function(abs), 'abs', 1)
CORE_LIBRARY.add(Number.wrap_python_function(round), 'round', 1)
CORE_LIBRARY.add(Number.wrap_python_function(math.ceil), 'ceil', 1)
CORE_LIBRARY.add(Number.wrap_python_function(math.floor), 'floor', 1)
# ------------------------------------------------------------------------------
# List functions
def __parse_separator(separator, default_from=None):
if separator is None:
separator = 'auto'
separator = String.unquoted(separator).value
if separator == 'comma':
return True
elif separator == 'space':
return False
elif separator == 'auto':
if not default_from:
return True
elif len(default_from) < 2:
return True
else:
return default_from.use_comma
else:
raise ValueError('Separator must be auto, comma, or space')
# TODO get the compass bit outta here
@register('-compass-list-size')
@register('length')
def _length(*lst):
if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)):
lst = lst[0]
return Number(len(lst))
@register('set-nth', 3)
def set_nth(list, n, value):
expect_type(n, Number, unit=None)
py_n = n.to_python_index(len(list))
return List(
tuple(list[:py_n]) + (value,) + tuple(list[py_n + 1:]),
use_comma=list.use_comma)
# TODO get the compass bit outta here
@register('-compass-nth', 2)
@register('nth', 2)
def nth(lst, n):
"""Return the nth item in the list."""
expect_type(n, (String, Number), unit=None)
if isinstance(n, String):
if n.value.lower() == 'first':
i = 0
elif n.value.lower() == 'last':
i = -1
else:
raise ValueError("Invalid index %r" % (n,))
else:
# DEVIATION: nth treats lists as circular lists
i = n.to_python_index(len(lst), circular=True)
return lst[i]
@register('join', 2)
@register('join', 3)
def join(lst1, lst2, separator=None):
ret = []
ret.extend(List.from_maybe(lst1))
ret.extend(List.from_maybe(lst2))
use_comma = __parse_separator(separator, default_from=lst1)
return List(ret, use_comma=use_comma)
@register('min')
def min_(*lst):
if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)):
lst = lst[0]
return min(lst)
@register('max')
def max_(*lst):
if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)):
lst = lst[0]
return max(lst)
@register('append', 2)
@register('append', 3)
def append(lst, val, separator=None):
ret = []
ret.extend(List.from_maybe(lst))
ret.append(val)
use_comma = __parse_separator(separator, default_from=lst)
return List(ret, use_comma=use_comma)
@register('index', 2)
def index(lst, val):
for i in xrange(len(lst)):
if lst.value[i] == val:
return Number(i + 1)
return Boolean(False)
@register('zip')
def zip_(*lists):
return List(
[List(zipped) for zipped in zip(*lists)],
use_comma=True)
# TODO need a way to use "list" as the arg name without shadowing the builtin
@register('list-separator', 1)
def list_separator(list):
if list.use_comma:
return String.unquoted('comma')
else:
return String.unquoted('space')
# ------------------------------------------------------------------------------
# Map functions
@register('map-get', 2)
def map_get(map, key):
return map.to_dict().get(key, Null())
@register('map-merge', 2)
def map_merge(*maps):
key_order = []
index = {}
for map in maps:
for key, value in map.to_pairs():
if key not in index:
key_order.append(key)
index[key] = value
pairs = [(key, index[key]) for key in key_order]
return Map(pairs, index=index)
@register('map-keys', 1)
def map_keys(map):
return List(
[k for (k, v) in map.to_pairs()],
use_comma=True)
@register('map-values', 1)
def map_values(map):
return List(
[v for (k, v) in map.to_pairs()],
use_comma=True)
@register('map-has-key', 2)
def map_has_key(map, key):
return Boolean(key in map.to_dict())
# DEVIATIONS: these do not exist in ruby sass
@register('map-get', 3)
def map_get3(map, key, default):
return map.to_dict().get(key, default)
@register('map-get-nested', 2)
@register('map-get-nested', 3)
def map_get_nested3(map, keys, default=Null()):
for key in keys:
map = map.to_dict().get(key, None)
if map is None:
return default
return map
@register('map-merge-deep', 2)
def map_merge_deep(*maps):
pairs = []
keys = set()
for map in maps:
for key, value in map.to_pairs():
keys.add(key)
for key in keys:
values = [map.to_dict().get(key, None) for map in maps]
values = [v for v in values if v is not None]
if all(isinstance(v, Map) for v in values):
pairs.append((key, map_merge_deep(*values)))
else:
pairs.append((key, values[-1]))
return Map(pairs)
# ------------------------------------------------------------------------------
# Meta functions
@register('type-of', 1)
def _type_of(obj): # -> bool, number, string, color, list
return String(obj.sass_type_name)
@register('unit', 1)
def unit(number): # -> px, em, cm, etc.
numer = '*'.join(sorted(number.unit_numer))
denom = '*'.join(sorted(number.unit_denom))
if denom:
ret = numer + '/' + denom
else:
ret = numer
return String.unquoted(ret)
@register('unitless', 1)
def unitless(value):
if not isinstance(value, Number):
raise TypeError("Expected number, got %r" % (value,))
return Boolean(value.is_unitless)
@register('comparable', 2)
def comparable(number1, number2):
left = number1.to_base_units()
right = number2.to_base_units()
return Boolean(
left.unit_numer == right.unit_numer
and left.unit_denom == right.unit_denom)
# ------------------------------------------------------------------------------
# Miscellaneous
@register('if', 2)
@register('if', 3)
def if_(condition, if_true, if_false=Null()):
return if_true if condition else if_false
+471
View File
@@ -0,0 +1,471 @@
"""Functions new to the pyScss library."""
from __future__ import absolute_import
import base64
import hashlib
import logging
import os.path
import random
import six
from six.moves import xrange
from scss import config
from scss.functions.library import FunctionLibrary
from scss.types import Color, Number, String, List
from scss.util import escape
try:
from PIL import Image, ImageDraw
except ImportError:
try:
import Image
import ImageDraw
except:
Image = None
log = logging.getLogger(__name__)
EXTRA_LIBRARY = FunctionLibrary()
register = EXTRA_LIBRARY.register
# ------------------------------------------------------------------------------
# Image stuff
def _image_noise(pixdata, size, density=None, intensity=None, color=None, opacity=None, monochrome=None, background=None):
if not density:
density = [0.8]
elif not isinstance(density, (tuple, list)):
density = [density]
if not intensity:
intensity = [0.5]
elif not isinstance(intensity, (tuple, list)):
intensity = [intensity]
if not color:
color = [(0, 0, 0, 0)]
elif not isinstance(color, (tuple, list)) or not isinstance(color[0], (tuple, list)):
color = [color]
if not opacity:
opacity = [0.2]
elif not isinstance(opacity, (tuple, list)):
opacity = [opacity]
if not monochrome:
monochrome = [False]
elif not isinstance(monochrome, (tuple, list)):
monochrome = [monochrome]
pixels = {}
if background:
for y in xrange(size):
for x in xrange(size):
ca = float(background[3])
pixels[(x, y)] = (background[0] * ca, background[1] * ca, background[2] * ca, ca)
loops = max(map(len, (density, intensity, color, opacity, monochrome)))
for l in range(loops):
_density = density[l % len(density)]
_intensity = intensity[l % len(intensity)]
_color = color[l % len(color)]
_opacity = opacity[l % len(opacity)]
_monochrome = monochrome[l % len(monochrome)]
_intensity = 1 - _intensity
if _intensity < 0.5:
cx = 255 * _intensity
cm = cx
else:
cx = 255 * (1 - _intensity)
cm = 255 * _intensity
xa = int(cm - cx)
xb = int(cm + cx)
if xa > 0:
xa &= 255
else:
xa = 0
if xb > 0:
xb &= 255
else:
xb = 0
r, g, b, a = _color
for i in xrange(int(round(_density * size ** 2))):
x = random.randint(1, size)
y = random.randint(1, size)
cc = random.randint(xa, xb)
cr = (cc) * (1 - a) + a * r
cg = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * g
cb = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * b
ca = random.random() * _opacity
ica = 1 - ca
pos = (x - 1, y - 1)
dst = pixels.get(pos, (0, 0, 0, 0))
src = (cr * ca, cg * ca, cb * ca, ca)
pixels[pos] = (src[0] + dst[0] * ica, src[1] + dst[1] * ica, src[2] + dst[2] * ica, src[3] + dst[3] * ica)
for pos, col in pixels.items():
ca = col[3]
if ca:
pixdata[pos] = tuple(int(round(c)) for c in (col[0] / ca, col[1] / ca, col[2] / ca, ca * 255))
def _image_brushed(pixdata, size, density=None, intensity=None, color=None, opacity=None, monochrome=None, direction=None, spread=None, background=None):
if not density:
density = [0.8]
elif not isinstance(density, (tuple, list)):
density = [density]
if not intensity:
intensity = [0.5]
elif not isinstance(intensity, (tuple, list)):
intensity = [intensity]
if not color:
color = [(0, 0, 0, 0)]
elif not isinstance(color, (tuple, list)) or not isinstance(color[0], (tuple, list)):
color = [color]
if not opacity:
opacity = [0.2]
elif not isinstance(opacity, (tuple, list)):
opacity = [opacity]
if not monochrome:
monochrome = [False]
elif not isinstance(monochrome, (tuple, list)):
monochrome = [monochrome]
if not direction:
direction = [0]
elif not isinstance(direction, (tuple, list)):
direction = [direction]
if not spread:
spread = [0]
elif not isinstance(spread, (tuple, list)):
spread = [spread]
def ppgen(d):
if d is None:
return
d = d % 4
if d == 0:
pp = lambda x, y, o: ((x - o) % size, y)
elif d == 1:
pp = lambda x, y, o: ((x - o) % size, (y + x - o) % size)
elif d == 2:
pp = lambda x, y, o: (y, (x - o) % size)
else:
pp = lambda x, y, o: ((x - o) % size, (y - x - o) % size)
return pp
pixels = {}
if background:
for y in xrange(size):
for x in xrange(size):
ca = float(background[3])
pixels[(x, y)] = (background[0] * ca, background[1] * ca, background[2] * ca, ca)
loops = max(map(len, (density, intensity, color, opacity, monochrome, direction, spread)))
for l in range(loops):
_density = density[l % len(density)]
_intensity = intensity[l % len(intensity)]
_color = color[l % len(color)]
_opacity = opacity[l % len(opacity)]
_monochrome = monochrome[l % len(monochrome)]
_direction = direction[l % len(direction)]
_spread = spread[l % len(spread)]
_intensity = 1 - _intensity
if _intensity < 0.5:
cx = 255 * _intensity
cm = cx
else:
cx = 255 * (1 - _intensity)
cm = 255 * _intensity
xa = int(cm - cx)
xb = int(cm + cx)
if xa > 0:
xa &= 255
else:
xa = 0
if xb > 0:
xb &= 255
else:
xb = 0
r, g, b, a = _color
pp = ppgen(_direction)
if pp:
for y in xrange(size):
if _spread and (y + (l % 2)) % _spread:
continue
o = random.randint(1, size)
cc = random.randint(xa, xb)
cr = (cc) * (1 - a) + a * r
cg = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * g
cb = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * b
da = random.randint(0, 255) * _opacity
ip = round((size / 2.0 * _density) / int(1 / _density))
iq = round((size / 2.0 * (1 - _density)) / int(1 / _density))
if ip:
i = da / ip
aa = 0
else:
i = 0
aa = da
d = 0
p = ip
for x in xrange(size):
if d == 0:
if p > 0:
p -= 1
aa += i
else:
d = 1
q = iq
elif d == 1:
if q > 0:
q -= 1
else:
d = 2
p = ip
elif d == 2:
if p > 0:
p -= 1
aa -= i
else:
d = 3
q = iq
elif d == 3:
if q > 0:
q -= 1
else:
d = 0
p = ip
if aa > 0:
ca = aa / 255.0
else:
ca = 0.0
ica = 1 - ca
pos = pp(x, y, o)
dst = pixels.get(pos, (0, 0, 0, 0))
src = (cr * ca, cg * ca, cb * ca, ca)
pixels[pos] = (src[0] + dst[0] * ica, src[1] + dst[1] * ica, src[2] + dst[2] * ica, src[3] + dst[3] * ica)
for pos, col in pixels.items():
ca = col[3]
if ca:
pixdata[pos] = tuple(int(round(c)) for c in (col[0] / ca, col[1] / ca, col[2] / ca, ca * 255))
@register('background-noise', 0)
@register('background-noise', 1)
@register('background-noise', 2)
@register('background-noise', 3)
@register('background-noise', 4)
@register('background-noise', 5)
@register('background-noise', 6)
@register('background-noise', 7)
def background_noise(density=None, opacity=None, size=None, monochrome=False, intensity=(), color=None, background=None, inline=False):
if not Image:
raise Exception("Images manipulation require PIL")
density = [Number(v).value for v in List.from_maybe(density)]
intensity = [Number(v).value for v in List.from_maybe(intensity)]
color = [Color(v).value for v in List.from_maybe(color) if v]
opacity = [Number(v).value for v in List.from_maybe(opacity)]
size = int(Number(size).value) if size else 0
if size < 1 or size > 512:
size = 200
monochrome = bool(monochrome)
background = Color(background).value if background else None
new_image = Image.new(
mode='RGBA',
size=(size, size)
)
pixdata = new_image.load()
_image_noise(pixdata, size, density, intensity, color, opacity, monochrome)
if not inline:
key = (size, density, intensity, color, opacity, monochrome)
asset_file = 'noise-%s%sx%s' % ('mono-' if monochrome else '', size, size)
# asset_file += '-[%s][%s]' % ('-'.join(to_str(s).replace('.', '_') for s in density or []), '-'.join(to_str(s).replace('.', '_') for s in opacity or []))
asset_file += '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
asset_file += '.png'
asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file)
try:
new_image.save(asset_path)
except IOError:
log.exception("Error while saving image")
inline = True # Retry inline version
url = '%s%s' % (config.ASSETS_URL, asset_file)
if inline:
output = six.BytesIO()
new_image.save(output, format='PNG')
contents = output.getvalue()
output.close()
url = 'data:image/png;base64,' + base64.b64encode(contents)
inline = 'url("%s")' % escape(url)
return String.unquoted(inline)
@register('background-brushed', 0)
@register('background-brushed', 1)
@register('background-brushed', 2)
@register('background-brushed', 3)
@register('background-brushed', 4)
@register('background-brushed', 5)
@register('background-brushed', 6)
@register('background-brushed', 7)
@register('background-brushed', 8)
@register('background-brushed', 9)
def background_brushed(density=None, intensity=None, color=None, opacity=None, size=None, monochrome=False, direction=(), spread=(), background=None, inline=False):
if not Image:
raise Exception("Images manipulation require PIL")
density = [Number(v).value for v in List.from_maybe(density)]
intensity = [Number(v).value for v in List.from_maybe(intensity)]
color = [Color(v).value for v in List.from_maybe(color) if v]
opacity = [Number(v).value for v in List.from_maybe(opacity)]
size = int(Number(size).value) if size else -1
if size < 0 or size > 512:
size = 200
monochrome = bool(monochrome)
direction = [Number(v).value for v in List.from_maybe(direction)]
spread = [Number(v).value for v in List.from_maybe(spread)]
background = Color(background).value if background else None
new_image = Image.new(
mode='RGBA',
size=(size, size)
)
pixdata = new_image.load()
_image_brushed(pixdata, size, density, intensity, color, opacity, monochrome, direction, spread, background)
if not inline:
key = (size, density, intensity, color, opacity, monochrome, direction, spread, background)
asset_file = 'brushed-%s%sx%s' % ('mono-' if monochrome else '', size, size)
# asset_file += '-[%s][%s][%s]' % ('-'.join(to_str(s).replace('.', '_') for s in density or []), '-'.join(to_str(s).replace('.', '_') for s in opacity or []), '-'.join(to_str(s).replace('.', '_') for s in direction or []))
asset_file += '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
asset_file += '.png'
asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file)
try:
new_image.save(asset_path)
except IOError:
log.exception("Error while saving image")
inline = True # Retry inline version
url = '%s%s' % (config.ASSETS_URL, asset_file)
if inline:
output = six.BytesIO()
new_image.save(output, format='PNG')
contents = output.getvalue()
output.close()
url = 'data:image/png;base64,' + base64.b64encode(contents)
inline = 'url("%s")' % escape(url)
return String.unquoted(inline)
@register('grid-image', 4)
@register('grid-image', 5)
def _grid_image(left_gutter, width, right_gutter, height, columns=1, grid_color=None, baseline_color=None, background_color=None, inline=False):
if not Image:
raise Exception("Images manipulation require PIL")
if grid_color is None:
grid_color = (120, 170, 250, 15)
else:
c = Color(grid_color).value
grid_color = (c[0], c[1], c[2], int(c[3] * 255.0))
if baseline_color is None:
baseline_color = (120, 170, 250, 30)
else:
c = Color(baseline_color).value
baseline_color = (c[0], c[1], c[2], int(c[3] * 255.0))
if background_color is None:
background_color = (0, 0, 0, 0)
else:
c = Color(background_color).value
background_color = (c[0], c[1], c[2], int(c[3] * 255.0))
_height = int(height) if height >= 1 else int(height * 1000.0)
_width = int(width) if width >= 1 else int(width * 1000.0)
_left_gutter = int(left_gutter) if left_gutter >= 1 else int(left_gutter * 1000.0)
_right_gutter = int(right_gutter) if right_gutter >= 1 else int(right_gutter * 1000.0)
if _height <= 0 or _width <= 0 or _left_gutter <= 0 or _right_gutter <= 0:
raise ValueError
_full_width = (_left_gutter + _width + _right_gutter)
new_image = Image.new(
mode='RGBA',
size=(_full_width * int(columns), _height),
color=background_color
)
draw = ImageDraw.Draw(new_image)
for i in range(int(columns)):
draw.rectangle((i * _full_width + _left_gutter, 0, i * _full_width + _left_gutter + _width - 1, _height - 1), fill=grid_color)
if _height > 1:
draw.rectangle((0, _height - 1, _full_width * int(columns) - 1, _height - 1), fill=baseline_color)
if not inline:
grid_name = 'grid_'
if left_gutter:
grid_name += str(int(left_gutter)) + '+'
grid_name += str(int(width))
if right_gutter:
grid_name += '+' + str(int(right_gutter))
if height and height > 1:
grid_name += 'x' + str(int(height))
key = (columns, grid_color, baseline_color, background_color)
key = grid_name + '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
asset_file = key + '.png'
asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file)
try:
new_image.save(asset_path)
except IOError:
log.exception("Error while saving image")
inline = True # Retry inline version
url = '%s%s' % (config.ASSETS_URL, asset_file)
if inline:
output = six.BytesIO()
new_image.save(output, format='PNG')
contents = output.getvalue()
output.close()
url = 'data:image/png;base64,' + base64.b64encode(contents)
inline = 'url("%s")' % escape(url)
return String.unquoted(inline)
@register('image-color', 1)
@register('image-color', 2)
@register('image-color', 3)
def image_color(color, width=1, height=1):
if not Image:
raise Exception("Images manipulation require PIL")
w = int(Number(width).value)
h = int(Number(height).value)
if w <= 0 or h <= 0:
raise ValueError
new_image = Image.new(
mode='RGB' if color.alpha == 1 else 'RGBA',
size=(w, h),
color=color.rgba255,
)
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)
inline = 'url("%s")' % escape(url)
return String.unquoted(inline)
+81
View File
@@ -0,0 +1,81 @@
"""Defines `FunctionLibrary`, a class that collects functions to be exposed to
SCSS during compilation.
"""
# TODO the constant versions of this should be frozen
class FunctionLibrary(object):
"""Contains a set of functions to be exposed to SCSS.
Functions are keyed by both their name _and_ arity; this allows for
function overloading somewhat beyond what Python easily allows, and is
required for several functions in the standard Sass library.
Functions may also have arbitrary arity, in which case they accept any
number of arguments, though a function of the same name with a specific
arity takes precedence.
"""
def __init__(self):
self._functions = {}
def derive(self):
"""Returns a new registry object, pre-populated with all the functions
in this registry.
"""
new = type(self)()
new.inherit(self)
return new
def inherit(self, *other_libraries):
"""Import all the functions from the given other libraries into this one.
Note that existing functions ARE NOT replaced -- which also means that
functions from the first library take precedence over functions from
the second library, etc.
"""
new_functions = dict()
# dict.update replaces keys; go through the other libraries in reverse,
# so the first one wins
for library in reversed(other_libraries):
new_functions.update(library._functions)
new_functions.update(self._functions)
self._functions = new_functions
def register(self, name, argc=None):
"""Decorator for adding a function to this library."""
# XXX: this should allow specifying names of keyword arguments, as
# well. currently none of these functions support kwargs, i think.
# XXX automatically inferring the name and args would be...
# interesting
# XXX also it would be nice if a function which EXISTS but has the
# wrong number of args threw a useful error; atm i think it'll become
# literal (yikes!)
key = (name, argc)
def decorator(f):
self._functions[key] = f
return f
return decorator
def add(self, function, name, argc=None):
"""Add a function to this library imperatively."""
key = (name, argc)
self._functions[key] = function
def lookup(self, name, argc=None):
"""Find a function given its name and the number of arguments it takes.
Falls back to a function with the same name that takes any number of
arguments.
"""
# Try the given arity first
key = name, argc
if key in self._functions:
return self._functions[key]
# Fall back to arbitrary-arity (or KeyError if neither exists)
return self._functions[name, None]