def test_rgba(calc): # four args (css-style) assert calc('rgba(128, 192, 224, 0.5)') == ColorValue.from_rgb(128/255, 192/255, 224/255, 0.5) assert calc('rgba(20%, 40%, 60%, 0.7)') == ColorValue.from_rgb(0.2, 0.4, 0.6, 0.7) # two args (modify alpha of existing color) assert calc('rgba(red, 0.4)') == ColorValue.from_rgb(1., 0., 0., 0.4)
def _color_type(color, a, type): color = ColorValue(color).value a = NumberValue(a).value if a is not None else color[3] col = list(color[:3]) col += [0.0 if a < 0 else 1.0 if a > 1 else a] col += [type] return ColorValue(col)
def __hsl_op(op, color, h, s, l): color = ColorValue(color) c = color.value h = None if h is None else NumberValue(h) s = None if s is None else NumberValue(s) l = None if l is None else NumberValue(l) a = [ None if h is None else h.value / 360.0, None if s is None else _apply_percentage(s), None if l is None else _apply_percentage(l), ] # Convert to HSL: h, l, s = list( colorsys.rgb_to_hls(c[0] / 255.0, c[1] / 255.0, c[2] / 255.0)) c = h, s, l # Do the additions: c = [ 0.0 if c[i] < 0 else 1.0 if c[i] > 1 else op(c[i], a[i]) if op is not None and a[i] is not None else a[i] if a[i] is not None else c[i] for i in range(3) ] # Validations: c[0] = (c[0] * 360.0) % 360 r = 360.0, 1.0, 1.0 c = [0.0 if c[i] < 0 else r[i] if c[i] > r[i] else c[i] for i in range(3)] # Convert back to RGB: c = colorsys.hls_to_rgb(c[0] / 360.0, 0.99999999 if c[2] == 1 else c[2], 0.99999999 if c[1] == 1 else c[1]) color.value = (c[0] * 255.0, c[1] * 255.0, c[2] * 255.0, color.value[3]) return color
def test_hsl(calc): # Examples from the CSS 3 color spec, which Sass uses # (http://www.w3.org/TR/css3-color/#hsl-color) assert calc('hsl(0, 100%, 50%)') == ColorValue.from_rgb(1., 0., 0.) assert calc('hsl(120, 100%, 50%)') == ColorValue.from_rgb(0., 1., 0.) assert calc('hsl(120, 100%, 25%)') == ColorValue.from_rgb(0., 0.5, 0.) assert calc('hsl(120, 100%, 75%)') == ColorValue.from_rgb(0.5, 1., 0.5) assert calc('hsl(120, 75%, 75%)') == ColorValue.from_rgb(0.5625, 0.9375, 0.5625)
def test_rgba(calc): # four args (css-style) assert calc('rgba(128, 192, 224, 0.5)') == ColorValue.from_rgb( 128 / 255, 192 / 255, 224 / 255, 0.5) assert calc('rgba(20%, 40%, 60%, 0.7)') == ColorValue.from_rgb( 0.2, 0.4, 0.6, 0.7) # two args (modify alpha of existing color) assert calc('rgba(red, 0.4)') == ColorValue.from_rgb(1., 0., 0., 0.4)
def test_hsl(calc): # Examples from the CSS 3 color spec, which Sass uses # (http://www.w3.org/TR/css3-color/#hsl-color) assert calc('hsl(0, 100%, 50%)') == ColorValue.from_rgb(1., 0., 0.) assert calc('hsl(120, 100%, 50%)') == ColorValue.from_rgb(0., 1., 0.) assert calc('hsl(120, 100%, 25%)') == ColorValue.from_rgb(0., 0.5, 0.) assert calc('hsl(120, 100%, 75%)') == ColorValue.from_rgb(0.5, 1., 0.5) assert calc('hsl(120, 75%, 75%)') == ColorValue.from_rgb( 0.5625, 0.9375, 0.5625)
def test_hsla(calc): # Examples from the CSS 3 color spec assert calc('hsla(120, 100%, 50%, 1)') == ColorValue.from_rgb( 0., 1., 0., ) assert calc('hsla(240, 100%, 50%, 0.5)') == ColorValue.from_rgb( 0., 0., 1., 0.5) assert calc('hsla(30, 100%, 50%, 0.1)') == ColorValue.from_rgb( 1., 0.5, 0., 0.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. """ col = ColorValue(color) c = list(col.value) c[0] = 255.0 - c[0] c[1] = 255.0 - c[1] c[2] = 255.0 - c[2] col.value = tuple(c) return col
def parse_bareword(word): if word in _colors: # TODO tidy this up once constructors are more reliable ret = ColorValue(ParserValue(_colors[word])) ret.tokens = ParserValue(word) return ret elif word in ("null", "undefined"): return Null() elif word == "true": return BooleanValue(True) elif word == "false": return BooleanValue(False) else: return String(word, quotes=None)
def background_brushed(density=None, intensity=None, color=None, opacity=None, size=None, monochrome=False, direction=None, spread=None, background=None, inline=False): if not Image: raise Exception("Images manipulation require PIL") density = [NumberValue(v).value for v in List.from_maybe(density)] intensity = [NumberValue(v).value for v in List.from_maybe(intensity)] color = [ColorValue(v).value for v in List.from_maybe(color) if v] opacity = [NumberValue(v).value for v in List.from_maybe(opacity)] size = int(NumberValue(size).value) if size else -1 if size < 0 or size > 512: size = 200 monochrome = bool(monochrome) direction = [NumberValue(v).value for v in List.from_maybe(direction)] spread = [NumberValue(v).value for v in List.from_maybe(spread)] background = ColorValue(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 StringValue(inline)
def __rgba_op(op, color, r, g, b, a): color = ColorValue(color) c = color.value a = [ None if r is None else NumberValue(r).value, None if g is None else NumberValue(g).value, None if b is None else NumberValue(b).value, None if a is None else NumberValue(a).value, ] # Do the additions: c = [op(c[i], a[i]) if op is not None and a[i] is not None else a[i] if a[i] is not None else c[i] for i in range(4)] # Validations: r = 255.0, 255.0, 255.0, 1.0 c = [0.0 if c[i] < 0 else r[i] if c[i] > r[i] else c[i] for i in range(4)] color.value = tuple(c) return color
def rgba(r, g, b, a, type='rgba'): channels = [] for ch in (r, g, b): channels.append(_constrain(_apply_percentage(ch, relto=255), 0, 255)) channels.append(_constrain(_apply_percentage(a), 0, 1)) channels.append(type) return ColorValue(channels)
def test_adjust_hue(calc): # Examples from the Ruby docs assert calc('adjust-hue(hsl(120, 30%, 90%), 60deg)') == calc( 'hsl(180, 30%, 90%)') assert calc('adjust-hue(hsl(120, 30%, 90%), -60deg)') == calc( 'hsl(60, 30%, 90%)') assert calc('adjust-hue(#811, 45deg)') == ColorValue.from_rgb( 136 / 255, 106.25 / 255, 17 / 255)
def __rgba_op(op, color, r, g, b, a): color = ColorValue(color) c = color.value a = [ None if r is None else NumberValue(r).value, None if g is None else NumberValue(g).value, None if b is None else NumberValue(b).value, None if a is None else NumberValue(a).value, ] # Do the additions: c = [ op(c[i], a[i]) if op is not None and a[i] is not None else a[i] if a[i] is not None else c[i] for i in range(4) ] # Validations: r = 255.0, 255.0, 255.0, 1.0 c = [0.0 if c[i] < 0 else r[i] if c[i] > r[i] else c[i] for i in range(4)] color.value = tuple(c) return color
def parse_bareword(word): if word in COLOR_NAMES: return ColorValue.from_name(word) elif word in ('null', 'undefined'): return Null() elif word == 'true': return BooleanValue(True) elif word == 'false': return BooleanValue(False) else: return String(word, quotes=None)
def parse_bareword(word): if word in COLOR_NAMES: return ColorValue.from_name(word) elif word == 'null': return Null() elif word == 'undefined': return Undefined() elif word == 'true': return BooleanValue(True) elif word == 'false': return BooleanValue(False) else: return String(word, quotes=None)
def hsla(h, s, l, a, type='hsla'): rgb = colorsys.hls_to_rgb( _apply_percentage(h, relto=360) % 360 / 360, # Ruby sass treats plain numbers for saturation and lightness as though # they were percentages, just without the % _constrain(_apply_percentage(l, relto=100), 0, 100) / 100, _constrain(_apply_percentage(s, relto=100), 0, 100) / 100, ) channels = list(ch * 255 for ch in rgb) channels.append(_constrain(_apply_percentage(a), 0, 1)) channels.append(type) return ColorValue(channels)
def __hsl_op(op, color, h, s, l): color = ColorValue(color) c = color.value h = None if h is None else NumberValue(h) s = None if s is None else NumberValue(s) l = None if l is None else NumberValue(l) a = [ None if h is None else h.value / 360.0, None if s is None else _apply_percentage(s), None if l is None else _apply_percentage(l), ] # Convert to HSL: h, l, s = list(colorsys.rgb_to_hls(c[0] / 255.0, c[1] / 255.0, c[2] / 255.0)) c = h, s, l # Do the additions: c = [0.0 if c[i] < 0 else 1.0 if c[i] > 1 else op(c[i], a[i]) if op is not None and a[i] is not None else a[i] if a[i] is not None else c[i] for i in range(3)] # Validations: c[0] = (c[0] * 360.0) % 360 r = 360.0, 1.0, 1.0 c = [0.0 if c[i] < 0 else r[i] if c[i] > r[i] else c[i] for i in range(3)] # Convert back to RGB: c = colorsys.hls_to_rgb(c[0] / 360.0, 0.99999999 if c[2] == 1 else c[2], 0.99999999 if c[1] == 1 else c[1]) color.value = (c[0] * 255.0, c[1] * 255.0, c[2] * 255.0, color.value[3]) return color
def image_color(color, width=1, height=1): if not Image: raise Exception("Images manipulation require PIL") c = ColorValue(color).value w = int(NumberValue(width).value) h = int(NumberValue(height).value) if w <= 0 or h <= 0: raise ValueError new_image = Image.new( mode='RGB' if c[3] == 1 else 'RGBA', size=(w, h), color=(c[0], c[1], c[2], int(c[3] * 255.0)) ) 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 StringValue(inline)
def atom(self): _token_ = self._peek(self.u_expr_chks) if _token_ == 'LPAR': LPAR = self._scan('LPAR') expr_lst = self.expr_lst() RPAR = self._scan('RPAR') return Parentheses(expr_lst) elif _token_ == 'ID': ID = self._scan('ID') return Literal(parse_bareword(ID)) elif _token_ == 'BANG_IMPORTANT': BANG_IMPORTANT = self._scan('BANG_IMPORTANT') return Literal(String(BANG_IMPORTANT, quotes=None)) elif _token_ == 'FNCT': FNCT = self._scan('FNCT') v = ArgspecLiteral([]) LPAR = self._scan('LPAR') if self._peek(self.atom_rsts) != 'RPAR': argspec = self.argspec() v = argspec RPAR = self._scan('RPAR') return CallOp(FNCT, v) elif _token_ == 'NUM': NUM = self._scan('NUM') if self._peek(self.atom_rsts_) == 'UNITS': UNITS = self._scan('UNITS') return Literal(NumberValue(float(NUM), unit=UNITS.lower())) return Literal(NumberValue(float(NUM))) elif _token_ == 'STR': STR = self._scan('STR') return Literal(String(STR[1:-1], quotes="'")) elif _token_ == 'QSTR': QSTR = self._scan('QSTR') return Literal(String(QSTR[1:-1], quotes='"')) elif _token_ == 'COLOR': COLOR = self._scan('COLOR') return Literal(ColorValue(ParserValue(COLOR))) else: # == 'VAR' VAR = self._scan('VAR') return Variable(VAR)
def test_complement(calc): assert calc('complement(black)') == ColorValue.from_rgb(0., 0., 0.) assert calc('complement(white)') == ColorValue.from_rgb(1., 1., 1.) assert calc('complement(yellow)') == ColorValue.from_rgb(0., 0., 1.)
def test_grayscale(calc): assert calc('grayscale(black)') == ColorValue.from_rgb(0., 0., 0.) assert calc('grayscale(white)') == ColorValue.from_rgb(1., 1., 1.) assert calc('grayscale(yellow)') == ColorValue.from_rgb(0.5, 0.5, 0.5)
def test_desaturate(calc): # Examples from the Ruby docs assert calc('desaturate(hsl(120, 30%, 90%), 20%)') == calc('hsl(120, 10%, 90%)') assert calc('desaturate(#855, 20%)') == ColorValue.from_rgb(113.9/255, 107.1/255, 107.1/255)
def test_saturate(calc): # Examples from the Ruby docs assert calc('saturate(hsl(120, 30%, 90%), 20%)') == calc('hsl(120, 50%, 90%)') assert calc('saturate(#855, 20%)') == ColorValue.from_rgb(158.1/255, 62.9/255, 62.9/255)
def test_hsla(calc): # Examples from the CSS 3 color spec assert calc('hsla(120, 100%, 50%, 1)') == ColorValue.from_rgb(0., 1., 0.,) assert calc('hsla(240, 100%, 50%, 0.5)') == ColorValue.from_rgb(0., 0., 1., 0.5) assert calc('hsla(30, 100%, 50%, 0.1)') == ColorValue.from_rgb(1., 0.5, 0., 0.1)
def hue(color): c = ColorValue(color).value h, l, s = colorsys.rgb_to_hls(c[0] / 255.0, c[1] / 255.0, c[2] / 255.0) return NumberValue(h * 360, unit='deg')
def test_invert(calc): assert calc('invert(black)') == ColorValue.from_rgb(1., 1., 1.) assert calc('invert(white)') == ColorValue.from_rgb(0., 0., 0.) assert calc('invert(yellow)') == ColorValue.from_rgb(0., 0., 1.)
def blue(color): c = ColorValue(color).value return NumberValue(c[2])
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 = ColorValue(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 = ColorValue(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 = ColorValue(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 StringValue(inline)
def test_rgb(calc): assert calc('rgb(128, 192, 224)') == ColorValue.from_rgb( 128 / 255, 192 / 255, 224 / 255) assert calc('rgb(20%, 40%, 60%)') == ColorValue.from_rgb(0.2, 0.4, 0.6)
def test_rgb(calc): assert calc('rgb(128, 192, 224)') == ColorValue.from_rgb(128/255, 192/255, 224/255) assert calc('rgb(20%, 40%, 60%)') == ColorValue.from_rgb(0.2, 0.4, 0.6)
def ie_hex_str(color): c = ColorValue(color).value return String(u'#%02X%02X%02X%02X' % (round(c[3] * 255), round(c[0]), round(c[1]), round(c[2])))
def alpha(color): c = ColorValue(color).value return NumberValue(c[3])
def lightness(color): c = ColorValue(color).value h, l, s = colorsys.rgb_to_hls(c[0] / 255.0, c[1] / 255.0, c[2] / 255.0) return NumberValue(l * 100, unit='%')
def test_adjust_hue(calc): # Examples from the Ruby docs assert calc('adjust-hue(hsl(120, 30%, 90%), 60deg)') == calc('hsl(180, 30%, 90%)') assert calc('adjust-hue(hsl(120, 30%, 90%), -60deg)') == calc('hsl(60, 30%, 90%)') assert calc('adjust-hue(#811, 45deg)') == ColorValue.from_rgb(136/255, 106.25/255, 17/255)
def saturation(color): c = ColorValue(color).value h, l, s = colorsys.rgb_to_hls(c[0] / 255.0, c[1] / 255.0, c[2] / 255.0) return NumberValue(s * 100, unit='%')
def mix(color1, color2, weight=None): """ 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/ c1 = ColorValue(color1).value c2 = ColorValue(color2).value p = NumberValue(weight).value if weight is not None else 0.5 p = 0.0 if p < 0 else 1.0 if p > 1 else p w = p * 2 - 1 a = c1[3] - c2[3] w1 = ((w if (w * a == -1) else (w + a) / (1 + w * a)) + 1) / 2.0 w2 = 1 - w1 q = [w1, w1, w1, p] r = [w2, w2, w2, 1 - p] color = ColorValue(None).merge(c1).merge(c2) color.value = [c1[i] * q[i] + c2[i] * r[i] for i in range(4)] return color
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 = StringValue(path).value mime_type = inline and (StringValue(mime_type).value or mimetypes.guess_type(filepath)[0]) path = None 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())) if inline or dst_color or spacing: path = _storage.open(_file) except: filetime = 'NA' else: _path = os.path.join(config.STATIC_ROOT, 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.STATIC_URL if path: dst_colors = [ list(ColorValue(v).value[:3]) for v in List.from_maybe(dst_color) if v ] src_colors = src_color src_colors = [ tuple(ColorValue(v).value[:3]) if v else (0, 0, 0) 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 = [ int(NumberValue(v).value) if v else 0 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: image = Image.open(path) 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 = '%s%s' % (config.ASSETS_URL, asset_file) 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 = '%s%s' % (BASE_URL, filepath) if cache_buster: url = add_cache_buster(url, filetime) if not only_path: url = 'url("%s")' % escape(url) return StringValue(url)
def test_saturate(calc): # Examples from the Ruby docs assert calc('saturate(hsl(120, 30%, 90%), 20%)') == calc( 'hsl(120, 50%, 90%)') assert calc('saturate(#855, 20%)') == ColorValue.from_rgb( 158.1 / 255, 62.9 / 255, 62.9 / 255)
def test_desaturate(calc): # Examples from the Ruby docs assert calc('desaturate(hsl(120, 30%, 90%), 20%)') == calc( 'hsl(120, 10%, 90%)') assert calc('desaturate(#855, 20%)') == ColorValue.from_rgb( 113.9 / 255, 107.1 / 255, 107.1 / 255)
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 = StringValue(g).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 StringValue(None) 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') sprite_map = None if os.path.exists(asset_path): try: save_time, asset, sprite_map, sizes = pickle.load(open(cache_path)) sprite_maps[asset] = sprite_map 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: direction = StringValue(kwargs.get('direction', config.SPRTE_MAP_DIRECTION)).value repeat = StringValue(kwargs.get('repeat', 'no-repeat')).value collapse = kwargs.get('collapse') or 0 if isinstance(collapse, List): collapse_x = int(NumberValue(collapse[0]).value) collapse_y = int(NumberValue(collapse[-1]).value) else: collapse_x = collapse_y = int(NumberValue(collapse).value) if 'collapse_x' in kwargs: collapse_x = int(NumberValue(kwargs['collapse_x']).value) if 'collapse_y' in kwargs: collapse_y = int(NumberValue(kwargs['collapse_y']).value) position = kwargs.get('position', 0) position = NumberValue(position) if position.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', 0)) padding = [int(NumberValue(v).value) for v in List.from_maybe(padding)] padding = (padding * 4)[:4] dst_colors = kwargs.get('dst_color') dst_colors = [list(ColorValue(v).value[:3]) for v in List.from_maybe(dst_colors) if v] src_colors = kwargs.get('src_color') src_colors = [tuple(ColorValue(v).value[:3]) if v else (0, 0, 0) 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 = NumberValue(_position) if _position.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(NumberValue(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(ColorValue(v).value[:3]) for v in List.from_maybe(_dst_colors) if v] _src_colors = kwargs.get(name + '_src_color') if _src_colors is None: _src_colors = src_colors else: _src_colors = [tuple(ColorValue(v).value[:3]) if v else (0, 0, 0) 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) try: new_image.save(asset_path) except IOError: log.exception("Error while saving image") filetime = int(now_time) url = '%s%s?_=%s' % (config.ASSETS_URL, asset_file, filetime) asset = 'url("%s") %s' % (escape(url), 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, 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] = sprite_map for file_, size in sizes: _image_size_cache[file_] = size ret = StringValue(asset) return ret
def green(color): c = ColorValue(color).value return NumberValue(c[1])