diff --git a/weasyprint/css/descriptors.py b/weasyprint/css/descriptors.py index 6bce22e85c..0328fb4dba 100644 --- a/weasyprint/css/descriptors.py +++ b/weasyprint/css/descriptors.py @@ -52,24 +52,33 @@ def decorator(function): @descriptor() -def font_family(tokens): +def font_family(tokens, allow_spaces=False): """``font-family`` descriptor validation.""" + allowed_types = ['IDENT'] + if allow_spaces: + allowed_types.append('S') if len(tokens) == 1 and tokens[0].type == 'STRING': return tokens[0].value - elif tokens and all(token.type == 'IDENT' for token in tokens): - return ' '.join(token.value for token in tokens) + if tokens and all(token.type in allowed_types for token in tokens): + return ' '.join( + token.value for token in tokens if token.type == 'IDENT') @descriptor(wants_base_url=True) @comma_separated_list def src(tokens, base_url): """``src`` descriptor validation.""" - token = tokens.pop() - if token.type == 'URI': - if token.value.startswith('#'): - return 'internal', unquote(token.value[1:]) - else: - return 'external', safe_urljoin(base_url, token.value) + if len(tokens) <= 2: + token = tokens.pop() + if token.type == 'FUNCTION' and token.function_name == 'format': + token = tokens.pop() + if token.type == 'FUNCTION' and token.function_name == 'local': + return 'local', font_family(token.content, allow_spaces=True) + if token.type == 'URI': + if token.value.startswith('#'): + return 'internal', unquote(token.value[1:]) + else: + return 'external', safe_urljoin(base_url, token.value) @descriptor() diff --git a/weasyprint/text.py b/weasyprint/text.py index 14e0f8c706..f8b1a78285 100644 --- a/weasyprint/text.py +++ b/weasyprint/text.py @@ -15,6 +15,7 @@ import os import re +import sys import tempfile import pyphen @@ -27,6 +28,8 @@ ffi = cffi.FFI() ffi.cdef(''' + // Pango + typedef enum { PANGO_STYLE_NORMAL, PANGO_STYLE_OBLIQUE, @@ -203,6 +206,18 @@ void pango_cairo_show_layout_line (cairo_t *cr, PangoLayoutLine *line); + // FontConfig + + typedef enum _FcResult { + FcResultMatch, FcResultNoMatch, FcResultTypeMismatch, FcResultNoId, + FcResultOutOfMemory + } FcResult; + typedef enum _FcMatchKind { + FcMatchPattern, FcMatchFont, FcMatchScan + } FcMatchKind; + + typedef struct _FcConfig FcConfig; + typedef struct _FcPattern FcPattern; typedef unsigned char FcChar8; typedef int FcBool; typedef struct _FcConfig FcConfig; @@ -211,6 +226,15 @@ FcConfig * FcConfigGetCurrent (void); FcBool FcConfigParseAndLoad (FcConfig *config, const FcChar8 *file, FcBool complain); + FcPattern * FcPatternCreate (void); + FcPattern * FcFontMatch (FcConfig *config, FcPattern *p); + FcBool FcPatternAddString + (FcPattern *p, const char *object, const FcChar8 *s); + FcBool FcConfigSubstitute + (FcConfig *config, FcPattern *p, FcMatchKind kind); + void FcDefaultSubstitute (FcPattern *pattern); + FcResult FcPatternGetString + (FcPattern *p, const char *object, int n, FcChar8 **s); ''') @@ -1164,56 +1188,93 @@ def show_first_line(context, pango_layout, hinting): def add_font_face(rule_descriptors): """Add a font into the Fontconfig application.""" - if not fontconfig: - LOGGER.warning('@font-face is currently supported only on Linux') + if not fontconfig or ( + sys.platform.startswith('win') or + sys.platform.startswith('darwin')): + LOGGER.warning( + '@font-face is currently not supported on Windows and OSX') return - config = fontconfig.FcConfigGetCurrent() - for font in rule_descriptors['src']: - if font[0] == 'external': + for font_type, url in rule_descriptors['src']: + config = fontconfig.FcConfigGetCurrent() + if font_type in ('external', 'local'): + if font_type == 'local': + font_name = url.encode('utf-8') + pattern = fontconfig.FcPatternCreate() + fontconfig.FcConfigSubstitute( + config, pattern, fontconfig.FcMatchFont) + fontconfig.FcDefaultSubstitute(pattern) + fontconfig.FcPatternAddString( + pattern, b'fullname', font_name) + fontconfig.FcPatternAddString( + pattern, b'postscriptname', font_name) + family = ffi.new('FcChar8 **') + postscript = ffi.new('FcChar8 **') + matching_pattern = fontconfig.FcFontMatch(config, pattern) + # TODO: do many fonts have multiple family values? + fontconfig.FcPatternGetString( + matching_pattern, b'fullname', 0, family) + fontconfig.FcPatternGetString( + matching_pattern, b'postscriptname', 0, postscript) + family = ffi.string(family[0]) + postscript = ffi.string(postscript[0]) + if font_name.lower() in (family.lower(), postscript.lower()): + filename = ffi.new('FcChar8 **') + matching_pattern = fontconfig.FcFontMatch( + ffi.NULL, pattern) + fontconfig.FcPatternGetString( + matching_pattern, b'file', 0, filename) + url = u'file://' + ffi.string(filename[0]).decode('utf-8') + else: + LOGGER.warning( + 'Failed to load local font "%s"', + font_name.decode('utf-8')) + continue + try: + font = urlopen(url).read() + except Exception as exc: + LOGGER.warning('Failed to load font at "%s" (%s)', url, exc) + continue _, filename = tempfile.mkstemp() with open(filename, 'wb') as fd: - fd.write(urlopen(font[1]).read()) - else: - filename = font[1] - xml = ''' - - - - %s - - %s - - - %s - - - %s - - - %s - - - ''' % ( - filename, - rule_descriptors['font_family'], - FONTCONFIG_STYLE_CONSTANTS[ - rule_descriptors.get('font_style', 'normal')], - FONTCONFIG_WEIGHT_CONSTANTS[ - rule_descriptors.get('font_weight', 'normal')], - FONTCONFIG_STRETCH_CONSTANTS[ - rule_descriptors.get('font_stretch', 'normal')], - ) - _, conf_filename = tempfile.mkstemp() - with open(conf_filename, 'wb') as fd: - # TODO: encoding is OK for , but what about s? - fd.write(xml.encode(FILESYSTEM_ENCODING)) - fontconfig.FcConfigParseAndLoad( - config, conf_filename.encode(FILESYSTEM_ENCODING), True) - os.remove(conf_filename) - font_added = fontconfig.FcConfigAppFontAddFile( - config, filename.encode(FILESYSTEM_ENCODING)) - if font_added: - # TODO: we should mask the local fonts with the same name too - return filename + fd.write(font) + xml = ''' + + + + %s + + %s + + + %s + + + %s + + + %s + + + ''' % ( + filename, + rule_descriptors['font_family'], + FONTCONFIG_STYLE_CONSTANTS[ + rule_descriptors.get('font_style', 'normal')], + FONTCONFIG_WEIGHT_CONSTANTS[ + rule_descriptors.get('font_weight', 'normal')], + FONTCONFIG_STRETCH_CONSTANTS[ + rule_descriptors.get('font_stretch', 'normal')]) + _, conf_filename = tempfile.mkstemp() + with open(conf_filename, 'wb') as fd: + # TODO: encoding is OK for , but what about s? + fd.write(xml.encode(FILESYSTEM_ENCODING)) + fontconfig.FcConfigParseAndLoad( + config, conf_filename.encode(FILESYSTEM_ENCODING), True) + os.remove(conf_filename) + font_added = fontconfig.FcConfigAppFontAddFile( + config, filename.encode(FILESYSTEM_ENCODING)) + if font_added: + # TODO: we should mask the local fonts with the same name too + return filename LOGGER.warning( 'Font-face "%s" cannot be loaded' % rule_descriptors['font_family'])