def _font_url(path, only_path=False, cache_buster=True, inline=False): filepath = StringValue(path).value 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: 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: path = open(_path, 'rb') else: filetime = 'NA' BASE_URL = config.STATIC_URL if path and inline: mime_type = mimetypes.guess_type(filepath)[0] url = 'data:' + mime_type + ';base64,' + base64.b64encode(path.read()) 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 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_maps = _get_cache('sprite_maps') 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)])
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)])
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)
def sprite_url(map): """ Returns a url to the sprite image. """ map = StringValue(map).value 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?_=%s' % (config.ASSETS_URL, sprite_map['*f*'], sprite_map['*t*']) url = "url(%s)" % escape(url) return StringValue(url) return StringValue(None)
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] except IndexError: filetime = None else: filetime = getmtime(_file, _storage) if filetime is None: filetime = 'NA' elif inline: file = _storage.open(_file) else: _path = os.path.join(FONTS_ROOT, filepath.strip('/')) filetime = getmtime(_path) if filetime is None: filetime = 'NA' elif inline: file = open(_path, 'rb') 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 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 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)
def radial_svg_gradient(*args): args = List.from_maybe_starargs(args) color_stops = args center = None if isinstance(args[-1], (String, Number)): center = args[-1] color_stops = args[:-1] color_stops = __color_stops(False, *color_stops) cx, cy = grad_point(center) 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)
def linear_svg_gradient(*args): args = List.from_maybe_starargs(args) color_stops = args start = None if isinstance(args[-1], (String, Number)): start = args[-1] color_stops = args[:-1] color_stops = __color_stops(False, *color_stops) x1, y1 = grad_point(start) x2, y2 = grad_point(opposite_position(start)) 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 linear_svg_gradient(*args): args = List.from_maybe_starargs(args) color_stops = args start = None if isinstance(args[-1], (StringValue, NumberValue, 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 StringValue(inline)
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 += '-' + make_filename_hash(key) 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 = make_data_url('image/png', contents) inline = 'url("%s")' % escape(url) return String.unquoted(inline)
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)
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] except IndexError: filetime = None else: filetime = getmtime(_file, _storage) if filetime is None: filetime = 'NA' elif inline: file = _storage.open(_file) else: _path = os.path.join(FONTS_ROOT, filepath.strip('/')) filetime = getmtime(_path) if filetime is None: filetime = 'NA' elif inline: file = open(_path, 'rb') 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 = make_data_url( (mime if '/' in mime else 'font/%s' % mime), 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 linear_svg_gradient(*args): if len(args) == 1 and isinstance(args[0], (list, tuple, ListValue)): args = ListValue(args[0]).values() color_stops = args start = None if isinstance(args[-1], (StringValue, NumberValue, basestring)): 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 StringValue(inline)
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('')
def radial_svg_gradient(*args): if len(args) == 1 and isinstance(args[0], (list, tuple, ListValue)): args = ListValue(args[0]).values() color_stops = args center = None if isinstance(args[-1], (StringValue, NumberValue, basestring)): 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 StringValue(inline)
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 += '-' + make_filename_hash(key) 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 = make_data_url('image/png', contents) inline = 'url("%s")' % escape(url) return String.unquoted(inline)
def _font_url(path, only_path=False, cache_buster=True, inline=False): filepath = String.unquoted(path).value file = None if callable(config.FONTS_ROOT): try: _file, _storage = list(config.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(config.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 if file and inline: # mime_type = mimetypes.guess_type(filepath)[0] 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 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() url = make_data_url('image/png', contents) inline = 'url("%s")' % escape(url) return String.unquoted(inline)
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 = StringIO() 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 font_url(sheet, type_, only_path=False, cache_buster=True): font_sheet = font_sheets.get(sheet.render()) type_ = String.unquoted(type_).render() if font_sheet: asset_files = font_sheet['*f*'] asset_file = asset_files.get(type_) if asset_file: url = '%s%s' % (config.ASSETS_URL, asset_file) params = [] # if type_ == 'eot': # params.append('#iefix') if cache_buster: params.append('v=%s' % font_sheet['*t*']) if type_ == 'svg': params.append('#' + font_sheet['*n*']) if params: url += '?' + '&'.join(params) if not only_path: url = "url('%s')" % escape(url) return String.unquoted(url) return String.unquoted('')
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 sprite(map, sprite, offset_x=None, offset_y=None): """ Returns the image and background position for use in a single shorthand property """ map = StringValue(map).value sprite_name = StringValue(sprite).value sprite_map = sprite_maps.get(map) 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?_=%s' % (config.ASSETS_URL, sprite_map['*f*'], sprite_map['*t*']) x = NumberValue(offset_x or 0, 'px') y = NumberValue(offset_y or 0, 'px') if not x or (x <= -1 or x >= 1) and x.unit != '%': x -= sprite[2] if not y or (y <= -1 or y >= 1) and y.unit != '%': y -= sprite[3] pos = "url(%s) %s %s" % (escape(url), x, y) return StringValue(pos) return StringValue('0 0')
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") 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 font_sheet(g, **kwargs): if not fontforge: raise Exception("Fonts manipulation require fontforge") now_time = time.time() g = String(g, quotes=None).value if g in font_sheets: font_sheets[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('') glyph_name = os.path.normpath(os.path.dirname(g)).replace('\\', '_').replace('/', '_') key = [f for (f, s) in files] + [repr(kwargs), config.ASSETS_URL] key = glyph_name + '-' + make_filename_hash(key) asset_files = { 'ttf': key + '.ttf', 'svg': key + '.svg', 'woff': key + '.woff', 'eot': key + '.eot', } ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets') asset_paths = dict((type_, os.path.join(ASSETS_ROOT, asset_file)) for type_, asset_file in asset_files.items()) cache_path = os.path.join(config.CACHE_ROOT or ASSETS_ROOT, key + '.cache') inline = Boolean(kwargs.get('inline', False)) font_sheet = None asset = None file_assets = {} inline_assets = {} if all(os.path.exists(asset_path) for asset_path in asset_paths.values()) or inline: try: save_time, file_assets, inline_assets, font_sheet, codepoints = pickle.load(open(cache_path)) if file_assets: file_asset = List([file_asset for file_asset in file_assets.values()], separator=",") font_sheets[file_asset.render()] = font_sheet if inline_assets: inline_asset = List([inline_asset for inline_asset in inline_assets.values()], separator=",") font_sheets[inline_asset.render()] = font_sheet if inline: asset = inline_asset else: asset = file_asset except: pass if font_sheet: 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_) font_sheet = None # Invalidate cached custom font break if font_sheet is None or asset is None: cache_buster = Boolean(kwargs.get('cache_buster', True)) autowidth = Boolean(kwargs.get('autowidth', True)) autohint = Boolean(kwargs.get('autohint', True)) font = fontforge.font() font.encoding = 'UnicodeFull' font.design_size = 16 font.em = 512 font.ascent = 448 font.descent = 64 font.fontname = glyph_name font.familyname = glyph_name font.fullname = glyph_name if autowidth: font.autoWidth(0, 0, 512) if autohint: font.autoHint() def glyphs(f=lambda x: x): for file_, storage in f(files): if storage is not None: _file = storage.open(file_) else: _file = open(file_) svgtext = _file.read() svgtext = svgtext.replace('<switch>', '') svgtext = svgtext.replace('</switch>', '') _glyph = tempfile.NamedTemporaryFile(delete=False, suffix=".svg") _glyph.file.write(svgtext) _glyph.file.close() yield _glyph.name names = tuple(os.path.splitext(os.path.basename(file_))[0] for file_, storage in files) codepoints = [] for i, glyph_filename in enumerate(glyphs()): codepoint = i + GLYPH_START codepoints.append(codepoint) glyph = font.createChar(codepoint, names[i]) glyph.importOutlines(glyph_filename) os.unlink(glyph_filename) if autowidth: glyph.left_side_bearing = glyph.right_side_bearing = 0 glyph.round() else: glyph.width = 512 filetime = int(now_time) # Generate font files if not inline: urls = {} for type_ in ('ttf', 'svg', 'woff', 'eot'): asset_path = asset_paths[type_] try: font.generate(asset_path) # svgtext.replace('<svg>', '<svg xmlns="http://www.w3.org/2000/svg">') asset_file = asset_files[type_] url = '%s%s' % (config.ASSETS_URL, asset_file) params = [] if type_ == 'eot': params.append('#iefix') if cache_buster: params.append('v=%s' % filetime) if type_ == 'svg': params.append('#' + glyph_name) if params: url += '?' + '&'.join(params) urls[type_] = url except IOError: inline = False if inline: urls = {} for type_ in ('ttf', 'svg', 'woff', 'eot'): _tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.' + type_) _tmp.file.close() font.generate(_tmp.name) with open(_tmp.name) as fh: contents = fh.read() os.unlink(_tmp.name) # contents.replace('<svg>', '<svg xmlns="http://www.w3.org/2000/svg">') mime_type = FONT_MIME_TYPES[type_] url = make_data_url(mime_type, contents) urls[type_] = url assets = {} for type_, url in urls.items(): format_ = FONT_FORMATS[type_] url = "url('%s')" % escape(url) if inline: assets[type_] = inline_assets[type_] = List([String.unquoted(url), String.unquoted(format_)]) else: assets[type_] = file_assets[type_] = List([String.unquoted(url), String.unquoted(format_)]) asset = List([a for a in assets.values()], separator=",") # Add the new object: font_sheet = dict(zip(names, zip(rfiles, codepoints))) font_sheet['*'] = now_time font_sheet['*f*'] = asset_files font_sheet['*k*'] = key font_sheet['*n*'] = glyph_name font_sheet['*t*'] = filetime codepoints = zip(files, codepoints) cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) pickle.dump((now_time, file_assets, inline_assets, font_sheet, codepoints), 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(font_sheets) > MAX_FONT_SHEETS: for a in sorted(font_sheets, key=lambda a: font_sheets[a]['*'], reverse=True)[KEEP_FONT_SHEETS:]: del font_sheets[a] log.warning("Exceeded maximum number of font sheets (%s)" % MAX_FONT_SHEETS) font_sheets[asset.render()] = font_sheet for file_, codepoint in codepoints: _font_sheet_cache[file_] = codepoint # TODO this sometimes returns an empty list, or is never assigned to return asset
url = add_cache_buster(url, filetime) if inline: output = six.BytesIO() new_image.save(output, format='PNG') contents = output.getvalue() output.close() if not mime_type: mime_type = 'image/%s' % _path.split('.')[-1] 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. """
def font_sheet(g, **kwargs): if not fontforge: raise Exception("Fonts manipulation require fontforge") 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 base_name = None glob_path = None glyph_name = None if _k_ in font_sheets: font_sheets[_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.basename(os.path.dirname(_glob)) _glyph_name, _, _glyph_type = base_name.partition('.') if _glyph_type: _glyph_type += '-' if not glyph_name: glyph_name = _glyph_name tfiles.extend([_glyph_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 = glyph_name + '-' + make_filename_hash(key) asset_files = { 'eot': key + '.eot', 'woff': key + '.woff', 'ttf': key + '.ttf', 'svg': key + '.svg', } ASSETS_ROOT = config.ASSETS_ROOT or os.path.join( config.STATIC_ROOT, 'assets') asset_paths = dict((type_, os.path.join(ASSETS_ROOT, asset_file)) for type_, asset_file in asset_files.items()) cache_path = os.path.join(config.CACHE_ROOT or ASSETS_ROOT, key + '.cache') inline = Boolean(kwargs.get('inline', False)) font_sheet = None asset = None file_assets = {} inline_assets = {} if all( os.path.exists(asset_path) for asset_path in asset_paths.values()) or inline: try: save_time, file_assets, inline_assets, font_sheet, codepoints = pickle.load( open(cache_path)) if file_assets: file_asset = List( [file_asset for file_asset in file_assets.values()], separator=",") font_sheets[file_asset.render()] = font_sheet if inline_assets: inline_asset = List([ inline_asset for inline_asset in inline_assets.values() ], separator=",") font_sheets[inline_asset.render()] = font_sheet if inline: asset = inline_asset else: asset = file_asset except: pass if font_sheet: 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_) font_sheet = None # Invalidate cached custom font break if font_sheet is None or asset is None: cache_buster = Boolean(kwargs.get('cache_buster', True)) autowidth = Boolean(kwargs.get('autowidth', False)) autohint = Boolean(kwargs.get('autohint', True)) font = fontforge.font() font.encoding = 'UnicodeFull' font.design_size = 16 font.em = GLYPH_HEIGHT font.ascent = GLYPH_ASCENT font.descent = GLYPH_DESCENT font.fontname = glyph_name font.familyname = glyph_name font.fullname = glyph_name def glyphs(f=lambda x: x): for file_, storage in f(files): if storage is not None: _file = storage.open(file_) else: _file = open(file_) svgtext = _file.read() svgtext = svgtext.replace('<switch>', '') svgtext = svgtext.replace('</switch>', '') svgtext = svgtext.replace( '<svg>', '<svg xmlns="http://www.w3.org/2000/svg">') m = GLYPH_WIDTH_RE.search(svgtext) if m: width = float(m.group(1)) else: width = None m = GLYPH_HEIGHT_RE.search(svgtext) if m: height = float(m.group(1)) else: height = None _glyph = tempfile.NamedTemporaryFile(delete=False, suffix=".svg") _glyph.file.write(svgtext) _glyph.file.close() yield _glyph.name, width, height 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)) codepoints = [] for i, (glyph_filename, glyph_width, glyph_height) in enumerate(glyphs()): if glyph_height and glyph_height != GLYPH_HEIGHT: warnings.warn("Glyphs should be %spx-high" % GLYPH_HEIGHT) codepoint = i + GLYPH_START name = names[i] codepoints.append(codepoint) glyph = font.createChar(codepoint, name) glyph.importOutlines(glyph_filename) os.unlink(glyph_filename) glyph.width = glyph_width or GLYPH_WIDTH if autowidth: # Autowidth removes side bearings glyph.left_side_bearing = glyph.right_side_bearing = 0 glyph.round() filetime = int(now_time) # Generate font files if not inline: urls = {} for type_ in reversed(FONT_TYPES): asset_path = asset_paths[type_] try: if type_ == 'eot': ttf_path = asset_paths['ttf'] with open(ttf_path) as ttf_fh, open( asset_path, 'wb') as asset_fh: contents = ttf2eot(ttf_fh.read()) if contents is None: continue asset_fh.write(contents) else: font.generate(asset_path) if type_ == 'ttf': contents = None if autohint: with open(asset_path) as asset_fh: contents = ttfautohint(asset_fh.read()) if contents is not None: with open(asset_path, 'wb') as asset_fh: asset_fh.write(contents) asset_file = asset_files[type_] url = '%s%s' % (config.ASSETS_URL, asset_file) params = [] if not urls: params.append('#iefix') if cache_buster: params.append('v=%s' % filetime) if type_ == 'svg': params.append('#' + glyph_name) if params: url += '?' + '&'.join(params) urls[type_] = url except IOError: inline = False if inline: urls = {} for type_ in reversed(FONT_TYPES): contents = None if type_ == 'eot': ttf_path = asset_paths['ttf'] with open(ttf_path) as ttf_fh: contents = ttf2eot(ttf_fh.read()) if contents is None: continue else: _tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.' + type_) _tmp.file.close() font.generate(_tmp.name) with open(_tmp.name) as asset_fh: if autohint: if type_ == 'ttf': _contents = asset_fh.read() contents = ttfautohint(_contents) if contents is None: contents = _contents os.unlink(_tmp.name) mime_type = FONT_MIME_TYPES[type_] url = make_data_url(mime_type, contents) urls[type_] = url assets = {} for type_, url in urls.items(): format_ = FONT_FORMATS[type_] url = "url('%s')" % escape(url) if inline: assets[type_] = inline_assets[type_] = List( [String.unquoted(url), String.unquoted(format_)]) else: assets[type_] = file_assets[type_] = List( [String.unquoted(url), String.unquoted(format_)]) asset = List( [assets[type_] for type_ in FONT_TYPES if type_ in assets], separator=",") # Add the new object: font_sheet = dict(zip(tnames, zip(rfiles, codepoints))) font_sheet['*'] = now_time font_sheet['*f*'] = asset_files font_sheet['*k*'] = key font_sheet['*n*'] = glyph_name font_sheet['*t*'] = filetime codepoints = zip(files, codepoints) cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) pickle.dump( (now_time, file_assets, inline_assets, font_sheet, codepoints), 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(font_sheets) > MAX_FONT_SHEETS: for a in sorted(font_sheets, key=lambda a: font_sheets[a]['*'], reverse=True)[KEEP_FONT_SHEETS:]: del font_sheets[a] log.warning("Exceeded maximum number of font sheets (%s)" % MAX_FONT_SHEETS) font_sheets[asset.render()] = font_sheet for file_, codepoint in codepoints: _font_sheet_cache[file_] = codepoint # TODO this sometimes returns an empty list, or is never assigned to return asset
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 __str__(self): if self.quotes: return self.quotes + escape(self.value) + self.quotes else: return self.value
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 = 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 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 SassMissingDependency('PIL', 'image manipulation') 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 + '-' + make_filename_hash(key) 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 = make_data_url('image/png', contents) inline = 'url("%s")' % escape(url) return String.unquoted(inline)
def font_sheet(g, **kwargs): if not fontforge: raise Exception("Fonts manipulation require fontforge") 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 base_name = None glob_path = None glyph_name = None if _k_ in font_sheets: font_sheets[_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.basename(os.path.dirname(_glob)) _glyph_name, _, _glyph_type = base_name.partition('.') if _glyph_type: _glyph_type += '-' if not glyph_name: glyph_name = _glyph_name tfiles.extend([_glyph_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 = glyph_name + '-' + make_filename_hash(key) asset_files = { 'eot': key + '.eot', 'woff': key + '.woff', 'ttf': key + '.ttf', 'svg': key + '.svg', } ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets') asset_paths = dict((type_, os.path.join(ASSETS_ROOT, asset_file)) for type_, asset_file in asset_files.items()) cache_path = os.path.join(config.CACHE_ROOT or ASSETS_ROOT, key + '.cache') inline = Boolean(kwargs.get('inline', False)) font_sheet = None asset = None file_assets = {} inline_assets = {} if all(os.path.exists(asset_path) for asset_path in asset_paths.values()) or inline: try: save_time, file_assets, inline_assets, font_sheet, codepoints = pickle.load(open(cache_path)) if file_assets: file_asset = List([file_asset for file_asset in file_assets.values()], separator=",") font_sheets[file_asset.render()] = font_sheet if inline_assets: inline_asset = List([inline_asset for inline_asset in inline_assets.values()], separator=",") font_sheets[inline_asset.render()] = font_sheet if inline: asset = inline_asset else: asset = file_asset except: pass if font_sheet: 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_) font_sheet = None # Invalidate cached custom font break if font_sheet is None or asset is None: cache_buster = Boolean(kwargs.get('cache_buster', True)) autowidth = Boolean(kwargs.get('autowidth', False)) autohint = Boolean(kwargs.get('autohint', True)) font = fontforge.font() font.encoding = 'UnicodeFull' font.design_size = 16 font.em = GLYPH_HEIGHT font.ascent = GLYPH_ASCENT font.descent = GLYPH_DESCENT font.fontname = glyph_name font.familyname = glyph_name font.fullname = glyph_name def glyphs(f=lambda x: x): for file_, storage in f(files): if storage is not None: _file = storage.open(file_) else: _file = open(file_) svgtext = _file.read() svgtext = svgtext.replace('<switch>', '') svgtext = svgtext.replace('</switch>', '') svgtext = svgtext.replace('<svg>', '<svg xmlns="http://www.w3.org/2000/svg">') m = GLYPH_WIDTH_RE.search(svgtext) if m: width = float(m.group(1)) else: width = None m = GLYPH_HEIGHT_RE.search(svgtext) if m: height = float(m.group(1)) else: height = None _glyph = tempfile.NamedTemporaryFile(delete=False, suffix=".svg") _glyph.file.write(svgtext) _glyph.file.close() yield _glyph.name, width, height 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)) codepoints = [] for i, (glyph_filename, glyph_width, glyph_height) in enumerate(glyphs()): if glyph_height and glyph_height != GLYPH_HEIGHT: warnings.warn("Glyphs should be %spx-high" % GLYPH_HEIGHT) codepoint = i + GLYPH_START name = names[i] codepoints.append(codepoint) glyph = font.createChar(codepoint, name) glyph.importOutlines(glyph_filename) os.unlink(glyph_filename) glyph.width = glyph_width or GLYPH_WIDTH if autowidth: # Autowidth removes side bearings glyph.left_side_bearing = glyph.right_side_bearing = 0 glyph.round() filetime = int(now_time) # Generate font files if not inline: urls = {} for type_ in reversed(FONT_TYPES): asset_path = asset_paths[type_] try: if type_ == 'eot': ttf_path = asset_paths['ttf'] with open(ttf_path) as ttf_fh: contents = ttf2eot(ttf_fh.read()) if contents is not None: with open(asset_path, 'wb') as asset_fh: asset_fh.write(contents) else: font.generate(asset_path) if type_ == 'ttf': contents = None if autohint: with open(asset_path) as asset_fh: contents = ttfautohint(asset_fh.read()) if contents is not None: with open(asset_path, 'wb') as asset_fh: asset_fh.write(contents) asset_file = asset_files[type_] url = '%s%s' % (config.ASSETS_URL, asset_file) params = [] if not urls: params.append('#iefix') if cache_buster: params.append('v=%s' % filetime) if type_ == 'svg': params.append('#' + glyph_name) if params: url += '?' + '&'.join(params) urls[type_] = url except IOError: inline = False if inline: urls = {} for type_ in reversed(FONT_TYPES): contents = None if type_ == 'eot': ttf_path = asset_paths['ttf'] with open(ttf_path) as ttf_fh: contents = ttf2eot(ttf_fh.read()) if contents is None: continue else: _tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.' + type_) _tmp.file.close() font.generate(_tmp.name) with open(_tmp.name) as asset_fh: if autohint: if type_ == 'ttf': _contents = asset_fh.read() contents = ttfautohint(_contents) if contents is None: contents = _contents os.unlink(_tmp.name) mime_type = FONT_MIME_TYPES[type_] url = make_data_url(mime_type, contents) urls[type_] = url assets = {} for type_, url in urls.items(): format_ = FONT_FORMATS[type_] url = "url('%s')" % escape(url) if inline: assets[type_] = inline_assets[type_] = List([String.unquoted(url), String.unquoted(format_)]) else: assets[type_] = file_assets[type_] = List([String.unquoted(url), String.unquoted(format_)]) asset = List([assets[type_] for type_ in FONT_TYPES if type_ in assets], separator=",") # Add the new object: font_sheet = dict(zip(tnames, zip(rfiles, codepoints))) font_sheet['*'] = now_time font_sheet['*f*'] = asset_files font_sheet['*k*'] = key font_sheet['*n*'] = glyph_name font_sheet['*t*'] = filetime codepoints = zip(files, codepoints) cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) pickle.dump((now_time, file_assets, inline_assets, font_sheet, codepoints), 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(font_sheets) > MAX_FONT_SHEETS: for a in sorted(font_sheets, key=lambda a: font_sheets[a]['*'], reverse=True)[KEEP_FONT_SHEETS:]: del font_sheets[a] log.warning("Exceeded maximum number of font sheets (%s)" % MAX_FONT_SHEETS) font_sheets[asset.render()] = font_sheet for file_, codepoint in codepoints: _font_sheet_cache[file_] = codepoint # TODO this sometimes returns an empty list, or is never assigned to return asset
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 = StringIO() 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 _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 _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)
def font_sheet(g, **kwargs): if not fontforge: raise Exception("Fonts manipulation require fontforge") now_time = time.time() g = String(g, quotes=None).value if g in font_sheets: font_sheets[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('') glyph_name = os.path.normpath(os.path.dirname(g)).replace('\\', '_').replace( '/', '_') key = [f for (f, s) in files] + [repr(kwargs), config.ASSETS_URL] key = glyph_name + '-' + make_filename_hash(key) asset_files = { 'ttf': key + '.ttf', 'svg': key + '.svg', 'woff': key + '.woff', 'eot': key + '.eot', } ASSETS_ROOT = config.ASSETS_ROOT or os.path.join( config.STATIC_ROOT, 'assets') asset_paths = dict((type_, os.path.join(ASSETS_ROOT, asset_file)) for type_, asset_file in asset_files.items()) cache_path = os.path.join(config.CACHE_ROOT or ASSETS_ROOT, key + '.cache') inline = Boolean(kwargs.get('inline', False)) font_sheet = None asset = None file_assets = {} inline_assets = {} if all( os.path.exists(asset_path) for asset_path in asset_paths.values()) or inline: try: save_time, file_assets, inline_assets, font_sheet, codepoints = pickle.load( open(cache_path)) if file_assets: file_asset = List( [file_asset for file_asset in file_assets.values()], separator=",") font_sheets[file_asset.render()] = font_sheet if inline_assets: inline_asset = List([ inline_asset for inline_asset in inline_assets.values() ], separator=",") font_sheets[inline_asset.render()] = font_sheet if inline: asset = inline_asset else: asset = file_asset except: pass if font_sheet: 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_) font_sheet = None # Invalidate cached custom font break if font_sheet is None or asset is None: cache_buster = Boolean(kwargs.get('cache_buster', True)) autowidth = Boolean(kwargs.get('autowidth', True)) autohint = Boolean(kwargs.get('autohint', True)) font = fontforge.font() font.encoding = 'UnicodeFull' font.design_size = 16 font.em = 512 font.ascent = 448 font.descent = 64 font.fontname = glyph_name font.familyname = glyph_name font.fullname = glyph_name if autowidth: font.autoWidth(0, 0, 512) if autohint: font.autoHint() def glyphs(f=lambda x: x): for file_, storage in f(files): if storage is not None: _file = storage.open(file_) else: _file = open(file_) svgtext = _file.read() svgtext = svgtext.replace('<switch>', '') svgtext = svgtext.replace('</switch>', '') _glyph = tempfile.NamedTemporaryFile(delete=False, suffix=".svg") _glyph.file.write(svgtext) _glyph.file.close() yield _glyph.name names = tuple( os.path.splitext(os.path.basename(file_))[0] for file_, storage in files) codepoints = [] for i, glyph_filename in enumerate(glyphs()): codepoint = i + GLYPH_START codepoints.append(codepoint) glyph = font.createChar(codepoint, names[i]) glyph.importOutlines(glyph_filename) os.unlink(glyph_filename) if autowidth: glyph.left_side_bearing = glyph.right_side_bearing = 0 glyph.round() else: glyph.width = 512 filetime = int(now_time) # Generate font files if not inline: urls = {} for type_ in ('ttf', 'svg', 'woff', 'eot'): asset_path = asset_paths[type_] try: font.generate(asset_path) # svgtext.replace('<svg>', '<svg xmlns="http://www.w3.org/2000/svg">') asset_file = asset_files[type_] url = '%s%s' % (config.ASSETS_URL, asset_file) params = [] if type_ == 'eot': params.append('#iefix') if cache_buster: params.append('v=%s' % filetime) if type_ == 'svg': params.append('#' + glyph_name) if params: url += '?' + '&'.join(params) urls[type_] = url except IOError: inline = False if inline: urls = {} for type_ in ('ttf', 'svg', 'woff', 'eot'): _tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.' + type_) _tmp.file.close() font.generate(_tmp.name) with open(_tmp.name) as fh: contents = fh.read() os.unlink(_tmp.name) # contents.replace('<svg>', '<svg xmlns="http://www.w3.org/2000/svg">') mime_type = FONT_MIME_TYPES[type_] url = make_data_url(mime_type, contents) urls[type_] = url assets = {} for type_, url in urls.items(): format_ = FONT_FORMATS[type_] url = "url('%s')" % escape(url) if inline: assets[type_] = inline_assets[type_] = List( [String.unquoted(url), String.unquoted(format_)]) else: assets[type_] = file_assets[type_] = List( [String.unquoted(url), String.unquoted(format_)]) asset = List([a for a in assets.values()], separator=",") # Add the new object: font_sheet = dict(zip(names, zip(rfiles, codepoints))) font_sheet['*'] = now_time font_sheet['*f*'] = asset_files font_sheet['*k*'] = key font_sheet['*n*'] = glyph_name font_sheet['*t*'] = filetime codepoints = zip(files, codepoints) cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) pickle.dump( (now_time, file_assets, inline_assets, font_sheet, codepoints), 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(font_sheets) > MAX_FONT_SHEETS: for a in sorted(font_sheets, key=lambda a: font_sheets[a]['*'], reverse=True)[KEEP_FONT_SHEETS:]: del font_sheets[a] log.warning("Exceeded maximum number of font sheets (%s)" % MAX_FONT_SHEETS) font_sheets[asset.render()] = font_sheet for file_, codepoint in codepoints: _font_sheet_cache[file_] = codepoint # TODO this sometimes returns an empty list, or is never assigned to return asset
def __unicode__(self): return '"%s"' % escape(self.value)
def background_noise(density=None, opacity=None, size=None, monochrome=False, intensity=None, color=None, background=None, inline=False): if not Image: raise Exception("Images manipulation require PIL") if isinstance(density, ListValue): density = [NumberValue(v).value for n, v in density.items()] else: density = [NumberValue(density).value] if isinstance(intensity, ListValue): intensity = [NumberValue(v).value for n, v in intensity.items()] else: intensity = [NumberValue(intensity).value] if isinstance(color, ListValue): color = [ColorValue(v).value for n, v in color.items() if v] else: color = [ColorValue(color).value] if color else [] if isinstance(opacity, ListValue): opacity = [NumberValue(v).value for n, v in opacity.items()] else: opacity = [NumberValue(opacity).value] size = int(NumberValue(size).value) if size else 0 if size < 1 or size > 512: size = 200 monochrome = bool(monochrome) background = ColorValue(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 = StringIO() 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 _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) 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('/'), 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 = 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 only_path: url = 'url(%s)' % escape(url) return String.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 = 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 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. """ g = StringValue(g).value if not Image: raise Exception("Images manipulation require PIL") if g in sprite_maps: sprite_maps[glob]["*"] = datetime.datetime.now() elif ".." not in g: # Protect against going to prohibited places... vertical = kwargs.get("direction", "vertical") == "vertical" repeat = StringValue(kwargs.get("repeat", "no-repeat")) position = NumberValue(kwargs.get("position", 0)) collapse_x = NumberValue(kwargs.get("collapse_x", 0)) collapse_y = NumberValue(kwargs.get("collapse_y", 0)) if position and position > -1 and position < 1: position.units = {"%": _units_weights.get("%", 1), "_": "%"} dst_colors = kwargs.get("dst_color") if isinstance(dst_colors, ListValue): dst_colors = [list(ColorValue(v).value[:3]) for n, v in dst_colors.items() if v] else: dst_colors = [list(ColorValue(dst_colors).value[:3])] if dst_colors else [] src_colors = kwargs.get("src_color") if isinstance(src_colors, ListValue): src_colors = [tuple(ColorValue(v).value[:3]) if v else (0, 0, 0) for n, v in src_colors.items()] else: src_colors = [tuple(ColorValue(src_colors).value[:3]) if src_colors else (0, 0, 0)] 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 = kwargs.get("spacing", 0) if isinstance(spacing, ListValue): spacing = [int(NumberValue(v).value) for n, v in spacing.items()] else: spacing = [int(NumberValue(spacing).value)] spacing = (spacing * 4)[:4] 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 = [(f[len(config.STATIC_ROOT) :], s) for f, s in files] if not files: log.error("Nothing found at '%s'", glob_path) return StringValue(None) times = [] for file, storage in files: try: d_obj = storage.modified_time(file) times.append(int(time.mktime(d_obj.timetuple()))) except: times.append(int(os.path.getmtime(file))) map_name = os.path.normpath(os.path.dirname(g)).replace("\\", "_").replace("/", "_") key = list(zip(*files)[0]) + times + [repr(kwargs), config.ASSETS_URL] key = map_name + "-" + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip("=").replace("-", "_") asset_file = key + ".png" asset_path = os.path.join(config.ASSETS_ROOT, asset_file) try: asset, map, sizes = pickle.load(open(asset_path + ".cache")) sprite_maps[asset] = map except: def images(): for file, storage in files: yield Image.open(storage.open(file)) if storage is not None else Image.open(file) names = tuple(os.path.splitext(os.path.basename(file))[0] for file, storage in files) positions = [] spacings = [] tot_spacings = [] for name in names: name = name.replace("-", "_") _position = kwargs.get(name + "_position") if _position is None: _position = position else: _position = NumberValue(_position) if _position and _position > -1 and _position < 1: _position.units = {"%": _units_weights.get("%", 1), "_": "%"} positions.append(_position) _spacing = kwargs.get(name + "_spacing") if _spacing is None: _spacing = spacing else: if isinstance(_spacing, ListValue): _spacing = [int(NumberValue(v).value) for n, v in _spacing.items()] else: _spacing = [int(NumberValue(_spacing).value)] _spacing = (_spacing * 4)[:4] spacings.append(_spacing) if _position and _position.unit != "%": if vertical: if _position > 0: tot_spacings.append((_spacing[0], _spacing[1], _spacing[2], _spacing[3] + _position)) else: if _position > 0: tot_spacings.append((_spacing[0] + _position, _spacing[1], _spacing[2], _spacing[3])) else: tot_spacings.append(_spacing) sizes = tuple((collapse_x or image.size[0], collapse_y or image.size[1]) for image in images()) _spacings = zip(*tot_spacings) if vertical: width = max(zip(*sizes)[0]) + max(_spacings[1]) + max(_spacings[3]) height = sum(zip(*sizes)[1]) + sum(_spacings[0]) + sum(_spacings[2]) else: width = sum(zip(*sizes)[0]) + sum(_spacings[1]) + sum(_spacings[3]) height = max(zip(*sizes)[1]) + max(_spacings[0]) + max(_spacings[2]) new_image = Image.new(mode="RGBA", size=(width, height), color=(0, 0, 0, 0)) offsets_x = [] offsets_y = [] offset = 0 for i, image in enumerate(images()): spacing = spacings[i] position = positions[i] iwidth, iheight = image.size width, height = sizes[i] if vertical: if position and position.unit == "%": x = width * position.value - (spacing[3] + height + spacing[1]) elif position.value < 0: x = width + position.value - (spacing[3] + height + spacing[1]) else: x = position.value offset += spacing[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] ) 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(x + spacing[3]), offset), cropped_image) cx += width cy += height else: new_image.paste(image, (int(x + spacing[3]), offset)) offsets_x.append(x) offsets_y.append(offset - spacing[0]) offset += height + spacing[2] else: if position and position.unit == "%": y = height * position.value - (spacing[0] + height + spacing[2]) elif position.value < 0: y = height + position.value - (spacing[0] + height + spacing[2]) else: y = position.value offset += spacing[3] 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] ) 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, (offset, int(y + spacing[0])), cropped_image) cx += width cy += height else: new_image.paste(image, (offset, int(y + spacing[0]))) offsets_x.append(offset - spacing[3]) offsets_y.append(y) offset += width + spacing[1] try: new_image.save(asset_path) except IOError: log.exception("Error while saving image") filetime = int(time.mktime(datetime.datetime.now().timetuple())) url = "%s%s?_=%s" % (config.ASSETS_URL, asset_file, filetime) asset = 'url("%s") %s' % (escape(url), repeat) # Use the sorted list to remove older elements (keep only 500 objects): if len(sprite_maps) > 1000: for a in sorted(sprite_maps, key=lambda a: sprite_maps[a]["*"], reverse=True)[500:]: del sprite_maps[a] # Add the new object: map = dict(zip(names, zip(sizes, rfiles, offsets_x, offsets_y))) map["*"] = datetime.datetime.now() map["*f*"] = asset_file map["*k*"] = key map["*n*"] = map_name map["*t*"] = filetime tmp_dir = config.ASSETS_ROOT cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=tmp_dir) pickle.dump((asset, map, zip(files, sizes)), cache_tmp) cache_tmp.close() os.rename(cache_tmp.name, asset_path + ".cache") sprite_maps[asset] = map for file, size in sizes: _image_size_cache[file] = size ret = StringValue(asset) return ret