"""Functions used for generating custom fonts from SVG files."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import re
import errno
import glob
import logging
import os
import time
import tempfile
import subprocess
import warnings
import six
try:
import cPickle as pickle
except ImportError:
import pickle
try:
import fontforge
except:
fontforge = None
from scss import config
from scss.errors import SassMissingDependency
from scss.extension import Extension
from scss.namespace import Namespace
from scss.types import Boolean, List, String, Url
from scss.util import getmtime, make_data_url, make_filename_hash
from scss.extension import Cache
log = logging.getLogger(__name__)
TTFAUTOHINT_EXECUTABLE = 'ttfautohint'
TTF2EOT_EXECUTABLE = 'ttf2eot'
MAX_FONT_SHEETS = 4096
KEEP_FONT_SHEETS = int(MAX_FONT_SHEETS * 0.8)
FONT_TYPES = ('eot', 'woff', 'ttf', 'svg') # eot should be first for IE support
FONT_MIME_TYPES = {
'ttf': 'application/x-font-ttf',
'svg': 'image/svg+xml',
'woff': 'application/x-font-woff',
'eot': 'application/vnd.ms-fontobject',
}
FONT_FORMATS = {
'ttf': "format('truetype')",
'svg': "format('svg')",
'woff': "format('woff')",
'eot': "format('embedded-opentype')",
}
GLYPH_WIDTH_RE = re.compile(r'width="(\d+(\.\d+)?)')
GLYPH_HEIGHT_RE = re.compile(r'height="(\d+(\.\d+)?)')
GLYPH_HEIGHT = 512
GLYPH_ASCENT = 448
GLYPH_DESCENT = GLYPH_HEIGHT - GLYPH_ASCENT
GLYPH_WIDTH = GLYPH_HEIGHT
# Offset to work around Chrome Windows bug
GLYPH_START = 0xf100
class FontsExtension(Extension):
"""Functions for creating and manipulating fonts."""
name = 'fonts'
namespace = Namespace()
# Alias to make the below declarations less noisy
ns = FontsExtension.namespace
def _assets_root():
return config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets')
def _get_cache(prefix):
return Cache((config.CACHE_ROOT or _assets_root(), prefix))
def ttfautohint(ttf):
try:
proc = subprocess.Popen(
[TTFAUTOHINT_EXECUTABLE, '--hinting-limit=200', '--hinting-range-max=50', '--symbol'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
except OSError as e:
if e.errno in (errno.EACCES, errno.ENOENT):
warnings.warn('Could not autohint ttf font: The executable %s could not be run: %s' % (TTFAUTOHINT_EXECUTABLE, e))
return None
else:
raise e
output, output_err = proc.communicate(ttf)
if proc.returncode != 0:
warnings.warn("Could not autohint ttf font: Unknown error!")
return None
return output
def ttf2eot(ttf):
try:
proc = subprocess.Popen(
[TTF2EOT_EXECUTABLE],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
except OSError as e:
if e.errno in (errno.EACCES, errno.ENOENT):
warnings.warn('Could not generate eot font: The executable %s could not be run: %s' % (TTF2EOT_EXECUTABLE, e))
return None
else:
raise e
output, output_err = proc.communicate(ttf)
if proc.returncode != 0:
warnings.warn("Could not generate eot font: Unknown error!")
return None
return output
@ns.declare
def font_sheet(g, **kwargs):
if not fontforge:
raise SassMissingDependency('fontforge', 'font manipulation')
font_sheets = _get_cache('font_sheets')
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 = _assets_root()
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('', '')
svgtext = svgtext.replace('', '')
svgtext = svgtext.replace('