""" A module for parsing and generating `fontconfig patterns`_. .. _fontconfig patterns: https://www.freedesktop.org/software/fontconfig/fontconfig-user.html """ # This class is defined here because it must be available in: # - The old-style config framework (:file:`rcsetup.py`) # - The font manager (:file:`font_manager.py`) # It probably logically belongs in :file:`font_manager.py`, but placing it # there would have created cyclical dependency problems. from functools import lru_cache import re from pyparsing import (Literal, ZeroOrMore, Optional, Regex, StringEnd, ParseException, Suppress) family_punc = r'\\\-:,' family_unescape = re.compile(r'\\([%s])' % family_punc).sub family_escape = re.compile(r'([%s])' % family_punc).sub value_punc = r'\\=_:,' value_unescape = re.compile(r'\\([%s])' % value_punc).sub value_escape = re.compile(r'([%s])' % value_punc).sub class FontconfigPatternParser(object): """ A simple pyparsing-based parser for `fontconfig patterns`_. .. _fontconfig patterns: https://www.freedesktop.org/software/fontconfig/fontconfig-user.html """ _constants = { 'thin' : ('weight', 'light'), 'extralight' : ('weight', 'light'), 'ultralight' : ('weight', 'light'), 'light' : ('weight', 'light'), 'book' : ('weight', 'book'), 'regular' : ('weight', 'regular'), 'normal' : ('weight', 'normal'), 'medium' : ('weight', 'medium'), 'demibold' : ('weight', 'demibold'), 'semibold' : ('weight', 'semibold'), 'bold' : ('weight', 'bold'), 'extrabold' : ('weight', 'extra bold'), 'black' : ('weight', 'black'), 'heavy' : ('weight', 'heavy'), 'roman' : ('slant', 'normal'), 'italic' : ('slant', 'italic'), 'oblique' : ('slant', 'oblique'), 'ultracondensed' : ('width', 'ultra-condensed'), 'extracondensed' : ('width', 'extra-condensed'), 'condensed' : ('width', 'condensed'), 'semicondensed' : ('width', 'semi-condensed'), 'expanded' : ('width', 'expanded'), 'extraexpanded' : ('width', 'extra-expanded'), 'ultraexpanded' : ('width', 'ultra-expanded') } def __init__(self): family = Regex(r'([^%s]|(\\[%s]))*' % (family_punc, family_punc)) \ .setParseAction(self._family) size = Regex(r"([0-9]+\.?[0-9]*|\.[0-9]+)") \ .setParseAction(self._size) name = Regex(r'[a-z]+') \ .setParseAction(self._name) value = Regex(r'([^%s]|(\\[%s]))*' % (value_punc, value_punc)) \ .setParseAction(self._value) families =(family + ZeroOrMore( Literal(',') + family) ).setParseAction(self._families) point_sizes =(size + ZeroOrMore( Literal(',') + size) ).setParseAction(self._point_sizes) property =( (name + Suppress(Literal('=')) + value + ZeroOrMore( Suppress(Literal(',')) + value) ) | name ).setParseAction(self._property) pattern =(Optional( families) + Optional( Literal('-') + point_sizes) + ZeroOrMore( Literal(':') + property) + StringEnd() ) self._parser = pattern self.ParseException = ParseException def parse(self, pattern): """ Parse the given fontconfig *pattern* and return a dictionary of key/value pairs useful for initializing a :class:`font_manager.FontProperties` object. """ props = self._properties = {} try: self._parser.parseString(pattern) except self.ParseException as e: raise ValueError( "Could not parse font string: '%s'\n%s" % (pattern, e)) self._properties = None self._parser.resetCache() return props def _family(self, s, loc, tokens): return [family_unescape(r'\1', str(tokens[0]))] def _size(self, s, loc, tokens): return [float(tokens[0])] def _name(self, s, loc, tokens): return [str(tokens[0])] def _value(self, s, loc, tokens): return [value_unescape(r'\1', str(tokens[0]))] def _families(self, s, loc, tokens): self._properties['family'] = [str(x) for x in tokens] return [] def _point_sizes(self, s, loc, tokens): self._properties['size'] = [str(x) for x in tokens] return [] def _property(self, s, loc, tokens): if len(tokens) == 1: if tokens[0] in self._constants: key, val = self._constants[tokens[0]] self._properties.setdefault(key, []).append(val) else: key = tokens[0] val = tokens[1:] self._properties.setdefault(key, []).extend(val) return [] # `parse_fontconfig_pattern` is a bottleneck during the tests because it is # repeatedly called when the rcParams are reset (to validate the default # fonts). In practice, the cache size doesn't grow beyond a few dozen entries # during the test suite. parse_fontconfig_pattern = lru_cache()(FontconfigPatternParser().parse) def generate_fontconfig_pattern(d): """ Given a dictionary of key/value pairs, generates a fontconfig pattern string. """ props = [] for key in 'family style variant weight stretch file size'.split(): val = getattr(d, 'get_' + key)() if val is not None and val != []: if type(val) == list: val = [value_escape(r'\\\1', str(x)) for x in val if x is not None] if val != []: val = ','.join(val) props.append(":%s=%s" % (key, val)) return ''.join(props)