187 lines
6.3 KiB
Python
187 lines
6.3 KiB
Python
|
"""
|
||
|
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)
|