def test_addition(): # Numbers are a little complicated, what with all the units # Simple case assert Number(123) + Number(456) == Number(579) # Simple equal units assert Number(1, "px") + Number(2, "px") == Number(3, "px") # Unitless values inherit units of the other operand assert Number(5) + Number(6, "px") == Number(11, "px") # Zero values can cast to any units assert Number(0, "in") + Number(24, "deg") == Number(24, "deg") # With different units, the left operand wins assert Number(10, "cm") + Number(100, "mm") == Number(20, "cm") assert Number(100, "mm") + Number(10, "cm") == Number(200, "mm") # Unconvertible units raise an error with pytest.raises(ValueError): Number(1, "px") + Number(1, "em") # Adding anything to a string makes a string assert Number(123) + String('abc') == String('123abc') assert String('abc') + Number(123) == String('abc123') ret = String('abc', quotes=None) + String('def', quotes=None) assert ret == String('abcdef') assert ret.quotes is None ret = String('abc', quotes='"') + String('def', quotes=None) assert ret == String('abcdef') assert ret.quotes is '"' ret = String('abc', quotes=None) + String('def', quotes='"') assert ret == String('abcdef') assert ret.quotes is None assert Color.from_hex('#010305') + Color.from_hex('#050301') == Color.from_hex('#060606') assert Color.from_name('white') + Color.from_name('white') == Color.from_name('white')
def test_reference_operations(): """Test the example expressions in the reference document: http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#operations """ # TODO: break this into its own file and add the entire reference guide # Need to build the calculator manually to get at its namespace, and need # to use calculate() instead of evaluate_expression() so interpolation # works ns = CoreExtension.namespace.derive() calc = Calculator(ns).calculate # Simple example assert calc('1in + 8pt') == Number(1.1111111111111112, "in") # Division ns.set_variable('$width', Number(1000, "px")) ns.set_variable('$font-size', Number(12, "px")) ns.set_variable('$line-height', Number(30, "px")) assert calc('10px/8px') == String('10px / 8px') # plain CSS; no division assert calc('$width/2') == Number(500, "px") # uses a variable; does division assert calc('(500px/2)') == Number(250, "px") # uses parens; does division assert calc('5px + 8px/2px') == Number(9, "px") # uses +; does division # TODO, again: Ruby Sass correctly renders this without spaces assert calc('#{$font-size}/#{$line-height}') == String('12px / 30px') # uses #{}; does no division # Color operations ns.set_variable('$translucent-red', Color.from_rgb(1, 0, 0, 0.5)) ns.set_variable('$green', Color.from_name('lime')) assert calc('#010203 + #040506') == Color.from_hex('#050709') assert calc('#010203 * 2') == Color.from_hex('#020406') assert calc( 'rgba(255, 0, 0, 0.75) + rgba(0, 255, 0, 0.75)') == Color.from_rgb( 1, 1, 0, 0.75) assert calc('opacify($translucent-red, 0.3)') == Color.from_rgb( 1, 0, 0, 0.8) assert calc('transparentize($translucent-red, 0.25)') == Color.from_rgb( 1, 0, 0, 0.25) assert calc( "progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#{ie-hex-str($green)}', endColorstr='#{ie-hex-str($translucent-red)}')" ).render( ) == "progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#FF00FF00', endColorstr='#80FF0000')" # String operations ns.set_variable('$value', Null()) assert_strict_string_eq(calc('e + -resize'), String('e-resize', quotes=None)) assert_strict_string_eq(calc('"Foo " + Bar'), String('Foo Bar', quotes='"')) assert_strict_string_eq(calc('sans- + "serif"'), String('sans-serif', quotes=None)) assert calc('3px + 4px auto') == List( [Number(7, "px"), String('auto', quotes=None)]) assert_strict_string_eq(calc('"I ate #{5 + 10} pies!"'), String('I ate 15 pies!', quotes='"')) assert_strict_string_eq(calc('"I ate #{$value} pies!"'), String('I ate pies!', quotes='"'))
def test_linear_gradient(): # Set up some values to = String.unquoted('to') bottom = String.unquoted('bottom') left = String.unquoted('left') red = Color.from_name('red') blue = Color.from_name('blue') start = Number(0, "%") middle = Number(50, "%") end = Number(100, "%") assert (linear_gradient(left, List((red, start)), List( (blue, middle))) == String('linear-gradient(left, red, blue 50%)')) assert (linear_gradient(List((to, bottom)), blue, List( (red, end))) == String('linear-gradient(to bottom, blue, red)'))
def test_reference_operations(): """Test the example expressions in the reference document: http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#operations """ # TODO: break this into its own file and add the entire reference guide # Need to build the calculator manually to get at its namespace, and need # to use calculate() instead of evaluate_expression() so interpolation # works ns = CoreExtension.namespace.derive() calc = Calculator(ns).calculate # Simple example assert calc('1in + 8pt') == Number(1.1111111111111112, "in") # Division ns.set_variable('$width', Number(1000, "px")) ns.set_variable('$font-size', Number(12, "px")) ns.set_variable('$line-height', Number(30, "px")) assert calc('10px/8px') == String('10px / 8px') # plain CSS; no division assert calc('$width/2') == Number(500, "px") # uses a variable; does division assert calc('(500px/2)') == Number(250, "px") # uses parens; does division assert calc('5px + 8px/2px') == Number(9, "px") # uses +; does division # TODO, again: Ruby Sass correctly renders this without spaces assert calc('#{$font-size}/#{$line-height}') == String('12px / 30px') # uses #{}; does no division # Modulo assert calc('29 % 12') == Number(5) assert calc('29px % 12') == Number(5, 'px') assert calc('29px % 12px') == Number(5, 'px') # Color operations ns.set_variable('$translucent-red', Color.from_rgb(1, 0, 0, 0.5)) ns.set_variable('$green', Color.from_name('lime')) assert calc('#010203 + #040506') == Color.from_hex('#050709') assert calc('#010203 * 2') == Color.from_hex('#020406') assert calc('rgba(255, 0, 0, 0.75) + rgba(0, 255, 0, 0.75)') == Color.from_rgb(1, 1, 0, 0.75) assert calc('opacify($translucent-red, 0.3)') == Color.from_rgb(1, 0, 0, 0.8) assert calc('transparentize($translucent-red, 0.25)') == Color.from_rgb(1, 0, 0, 0.25) assert calc("progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#{ie-hex-str($green)}', endColorstr='#{ie-hex-str($translucent-red)}')" ).render() == "progid:DXImageTransform.Microsoft.gradient(enabled='false', startColorstr='#FF00FF00', endColorstr='#80FF0000')" # String operations ns.set_variable('$value', Null()) assert_strict_string_eq(calc('e + -resize'), String('e-resize', quotes=None)) assert_strict_string_eq(calc('"Foo " + Bar'), String('Foo Bar', quotes='"')) assert_strict_string_eq(calc('sans- + "serif"'), String('sans-serif', quotes=None)) assert calc('3px + 4px auto') == List([Number(7, "px"), String('auto', quotes=None)]) assert_strict_string_eq(calc('"I ate #{5 + 10} pies!"'), String('I ate 15 pies!', quotes='"')) assert_strict_string_eq(calc('"I ate #{$value} pies!"'), String('I ate pies!', quotes='"'))
def parse_bareword(word): if word in COLOR_NAMES: return Color.from_name(word) elif word == 'null': return Null() elif word == 'undefined': return Undefined() elif word == 'true': return Boolean(True) elif word == 'false': return Boolean(False) else: return String(word, quotes=None)
def test_linear_gradient(): # Set up some values to = String.unquoted('to') bottom = String.unquoted('bottom') left = String.unquoted('left') red = Color.from_name('red') blue = Color.from_name('blue') start = Number(0, "%") middle = Number(50, "%") end = Number(100, "%") assert ( linear_gradient(left, List((red, start)), List((blue, middle))) == String('linear-gradient(left, red, blue 50%)') ) assert ( linear_gradient(List((to, bottom)), blue, List((red, end))) == String('linear-gradient(to bottom, blue, red)') )
def test_addition(): # Numbers are a little complicated, what with all the units # Simple case assert Number(123) + Number(456) == Number(579) # Simple equal units assert Number(1, "px") + Number(2, "px") == Number(3, "px") # Unitless values inherit units of the other operand assert Number(5) + Number(6, "px") == Number(11, "px") # Zero values can cast to any units assert Number(0, "in") + Number(24, "deg") == Number(24, "deg") # With different units, the left operand wins assert Number(10, "cm") + Number(100, "mm") == Number(20, "cm") assert Number(100, "mm") + Number(10, "cm") == Number(200, "mm") # Unconvertible units raise an error with pytest.raises(ValueError): Number(1, "px") + Number(1, "em") # Adding anything to a string makes a string assert Number(123) + String('abc') == String('123abc') assert String('abc') + Number(123) == String('abc123') ret = String('abc', quotes=None) + String('def', quotes=None) assert ret == String('abcdef') assert ret.quotes is None ret = String('abc', quotes='"') + String('def', quotes=None) assert ret == String('abcdef') assert ret.quotes == '"' ret = String('abc', quotes=None) + String('def', quotes='"') assert ret == String('abcdef') assert ret.quotes is None assert Color.from_hex('#010305') + Color.from_hex( '#050301') == Color.from_hex('#060606') assert Color.from_name('white') + Color.from_name( 'white') == Color.from_name('white')
def from_bareword(cls, word): if word in COLOR_NAMES: value = Color.from_name(word) elif word == 'null': value = Null() elif word == 'undefined': value = Undefined() elif word == 'true': value = Boolean(True) elif word == 'false': value = Boolean(False) else: value = String(word, quotes=None) return cls(value)
def test_addition(calc): assert calc('123 + 456') == Number(579) assert calc('1px + 2px') == Number(3, "px") assert calc('123 + abc') == String('123abc') assert calc('abc + 123') == String('abc123') assert calc('abc + def') == String('abcdef') assert calc('abc + "def"') == String('abcdef') ret = calc('"abc" + def') assert ret == String('abcdef') assert ret.quotes == '"' ret = calc('"abc" + "def"') assert ret == String('abcdef') assert ret.quotes == '"' assert calc('#010305 + #050301') == Color.from_hex('#060606') assert calc('#ffffff + #ffffff') == Color.from_name('white')
def _image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, inline=False, mime_type=None, spacing=None, collapse_x=None, collapse_y=None): """ src_color - a list of or a single color to be replaced by each corresponding dst_color colors spacing - spaces to be added to the image collapse_x, collapse_y - collapsable (layered) image of the given size (x, y) """ if inline or dst_color or spacing: if not Image: raise Exception("Images manipulation require PIL") filepath = String.unquoted(path).value fileext = os.path.splitext(filepath)[1].lstrip('.').lower() if mime_type: mime_type = String.unquoted(mime_type).value if not mime_type: mime_type = mimetypes.guess_type(filepath)[0] if not mime_type: mime_type = 'image/%s' % fileext path = None IMAGES_ROOT = _images_root() if callable(IMAGES_ROOT): try: _file, _storage = list(IMAGES_ROOT(filepath))[0] except IndexError: filetime = None else: filetime = getmtime(_file, _storage) if filetime is None: filetime = 'NA' elif inline or dst_color or spacing: path = _storage.open(_file) else: _path = os.path.join(IMAGES_ROOT.rstrip('/'), filepath.strip('/')) filetime = getmtime(_path) if filetime is None: filetime = 'NA' elif inline or dst_color or spacing: path = open(_path, 'rb') BASE_URL = config.IMAGES_URL or config.STATIC_URL if path: dst_colors = [ list(Color(v).value[:3]) for v in List.from_maybe(dst_color) if v ] src_color = Color.from_name( 'black') if src_color is None else src_color src_colors = [ tuple(Color(v).value[:3]) for v in List.from_maybe(src_color) ] len_colors = max(len(dst_colors), len(src_colors)) dst_colors = (dst_colors * len_colors)[:len_colors] src_colors = (src_colors * len_colors)[:len_colors] spacing = Number(0) if spacing is None else spacing spacing = [int(Number(v).value) for v in List.from_maybe(spacing)] spacing = (spacing * 4)[:4] file_name, file_ext = os.path.splitext( os.path.normpath(filepath).replace('\\', '_').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 = 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 = getmtime(asset_path) url = add_cache_buster(url, filetime) else: width, height = collapse_x or image.size[ 0], collapse_y or image.size[1] new_image = Image.new(mode='RGBA', size=(width + spacing[1] + spacing[3], height + spacing[0] + spacing[2]), color=(0, 0, 0, 0)) for i, dst_color in enumerate(dst_colors): src_color = src_colors[i] pixdata = image.load() for _y in xrange(image.size[1]): for _x in xrange(image.size[0]): pixel = pixdata[_x, _y] if pixel[:3] == src_color: pixdata[_x, _y] = tuple( [int(c) for c in dst_color] + [pixel[3] if len(pixel) == 4 else 255]) iwidth, iheight = image.size if iwidth != width or iheight != height: cy = 0 while cy < iheight: cx = 0 while cx < iwidth: cropped_image = image.crop( (cx, cy, cx + width, cy + height)) new_image.paste(cropped_image, (int(spacing[3]), int(spacing[0])), cropped_image) cx += width cy += height else: new_image.paste(image, (int(spacing[3]), int(spacing[0]))) if not inline: try: new_image.save(asset_path) filepath = asset_file BASE_URL = config.ASSETS_URL if cache_buster: filetime = getmtime(asset_path) except IOError: log.exception("Error while saving image") inline = True # Retry inline version url = os.path.join(config.ASSETS_URL.rstrip('/'), 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)
def _image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, inline=False, mime_type=None, spacing=None, collapse_x=None, collapse_y=None): """ src_color - a list of or a single color to be replaced by each corresponding dst_color colors spacing - spaces to be added to the image collapse_x, collapse_y - collapsable (layered) image of the given size (x, y) """ if inline or dst_color or spacing: if not Image: raise Exception("Images manipulation require PIL") filepath = String.unquoted(path).value fileext = os.path.splitext(filepath)[1].lstrip('.').lower() if mime_type: mime_type = String.unquoted(mime_type).value if not mime_type: mime_type = mimetypes.guess_type(filepath)[0] if not mime_type: mime_type = 'image/%s' % fileext path = None IMAGES_ROOT = _images_root() if callable(IMAGES_ROOT): try: _file, _storage = list(IMAGES_ROOT(filepath))[0] except IndexError: filetime = None else: filetime = getmtime(_file, _storage) if filetime is None: filetime = 'NA' elif inline or dst_color or spacing: path = _storage.open(_file) else: _path = os.path.join(IMAGES_ROOT.rstrip(os.sep), filepath.strip('\\/')) filetime = getmtime(_path) if filetime is None: filetime = 'NA' elif inline or dst_color or spacing: path = open(_path, 'rb') BASE_URL = config.IMAGES_URL or config.STATIC_URL if path: dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_color) if v] src_color = Color.from_name('black') if src_color is None else src_color src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_color)] len_colors = max(len(dst_colors), len(src_colors)) dst_colors = (dst_colors * len_colors)[:len_colors] src_colors = (src_colors * len_colors)[:len_colors] spacing = Number(0) if spacing is None else spacing spacing = [int(Number(v).value) for v in List.from_maybe(spacing)] spacing = (spacing * 4)[:4] file_name, file_ext = os.path.splitext(os.path.normpath(filepath).replace(os.sep, '_')) key = (filetime, src_color, dst_color, spacing) asset_file = file_name + '-' + make_filename_hash(key) + file_ext ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets') asset_path = os.path.join(ASSETS_ROOT, asset_file) if os.path.exists(asset_path): filepath = asset_file BASE_URL = config.ASSETS_URL if inline: path = open(asset_path, 'rb') url = make_data_url(mime_type, path.read()) else: url = '%s%s' % (BASE_URL, filepath) if cache_buster: filetime = getmtime(asset_path) url = add_cache_buster(url, filetime) else: simply_process = False image = None if fileext in ('cur',): simply_process = True else: try: image = Image.open(path) except IOError: if not collapse_x and not collapse_y and not dst_colors: simply_process = True if simply_process: if inline: url = make_data_url(mime_type, path.read()) else: url = '%s%s' % (BASE_URL, filepath) if cache_buster: filetime = getmtime(asset_path) url = add_cache_buster(url, filetime) else: width, height = collapse_x or image.size[0], collapse_y or image.size[1] new_image = Image.new( mode='RGBA', size=(width + spacing[1] + spacing[3], height + spacing[0] + spacing[2]), color=(0, 0, 0, 0) ) for i, dst_color in enumerate(dst_colors): src_color = src_colors[i] pixdata = image.load() for _y in xrange(image.size[1]): for _x in xrange(image.size[0]): pixel = pixdata[_x, _y] if pixel[:3] == src_color: pixdata[_x, _y] = tuple([int(c) for c in dst_color] + [pixel[3] if len(pixel) == 4 else 255]) iwidth, iheight = image.size if iwidth != width or iheight != height: cy = 0 while cy < iheight: cx = 0 while cx < iwidth: cropped_image = image.crop((cx, cy, cx + width, cy + height)) new_image.paste(cropped_image, (int(spacing[3]), int(spacing[0])), cropped_image) cx += width cy += height else: new_image.paste(image, (int(spacing[3]), int(spacing[0]))) if not inline: try: new_image.save(asset_path) filepath = asset_file BASE_URL = config.ASSETS_URL if cache_buster: filetime = getmtime(asset_path) except IOError: log.exception("Error while saving image") inline = True # Retry inline version url = os.path.join(config.ASSETS_URL.rstrip(os.sep), asset_file.lstrip(os.sep)) if cache_buster: url = add_cache_buster(url, filetime) if inline: output = six.BytesIO() new_image.save(output, format='PNG') contents = output.getvalue() output.close() url = make_data_url(mime_type, contents) else: url = os.path.join(BASE_URL.rstrip('/'), filepath.lstrip('\\/')) if cache_buster and filetime != 'NA': url = add_cache_buster(url, filetime) if not os.sep == '/': url = url.replace(os.sep, '/') if only_path: return String.unquoted(url) else: return Url.unquoted(url)
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 = [f for (f, s) in files] + [repr(kwargs), config.ASSETS_URL] key = map_name + '-' + make_filename_hash(key) 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: _time = getmtime(file_, storage) 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 = make_data_url(mime_type, 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 sizes = zip(files, sizes) cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) pickle.dump((now_time, file_asset, inline_asset, sprite_map, 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
def test_background_noise(): libextra.background_noise(Number(0.5), Number(0.5), Number(100), Boolean(True), color=Color.from_name('green'))
def test_background_brushed(): libextra.background_brushed(Number(0.5), Number(0.5), Color.from_name('red'), Number(0.5))
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 SassMissingDependency('PIL', 'image manipulation') sprite_maps = _get_cache('sprite_maps') now_time = time.time() globs = String(g, quotes=None).value globs = sorted(g.strip() for g in globs.split(',')) _k_ = ','.join(globs) files = None rfiles = None tfiles = None map_name = None if _k_ in sprite_maps: sprite_maps[_k_]['*'] = now_time else: files = [] rfiles = [] tfiles = [] for _glob in globs: if '..' not in _glob: # Protect against going to prohibited places... if callable(config.STATIC_ROOT): _glob_path = _glob _rfiles = _files = sorted(config.STATIC_ROOT(_glob)) else: _glob_path = os.path.join(config.STATIC_ROOT, _glob) _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 _files: files.extend(_files) rfiles.extend(_rfiles) base_name = os.path.normpath( os.path.dirname(_glob)).replace('\\', '_').replace('/', '_') _map_name, _, _map_type = base_name.partition('.') if _map_type: _map_type += '-' if not map_name: map_name = _map_name tfiles.extend([_map_type] * len(_files)) else: glob_path = _glob_path if files is not None: if not files: log.error("Nothing found at '%s'", glob_path) return String.unquoted('') key = [f for (f, s) in files] + [repr(kwargs), config.ASSETS_URL] key = map_name + '-' + make_filename_hash(key) asset_file = key + '.png' ASSETS_ROOT = _assets_root() 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: _time = getmtime(file_, storage) 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) tnames = tuple(tfiles[i] + n for i, n in enumerate(names)) 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 = make_data_url(mime_type, 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(tnames, 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 sizes = zip(files, sizes) cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) pickle.dump( (now_time, file_asset, inline_asset, sprite_map, sizes), cache_tmp) cache_tmp.close() if sys.platform == 'win32' and os.path.isfile(cache_path): # on windows, cannot rename a file to a path that matches # an existing file, we have to remove it first os.remove(cache_path) 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 image_size_cache = _get_cache('image_size_cache') for file_, size in sizes: image_size_cache[file_] = size # TODO this sometimes returns an empty list, or is never assigned to return asset
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)
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 mime_type = inline and (String.unquoted(mime_type).value if mime_type else 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(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: 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 String.unquoted(url)