Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/fontconfig_pattern.py : 70%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2A module for parsing and generating `fontconfig patterns`_.
4.. _fontconfig patterns:
5 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
6"""
8# This class is defined here because it must be available in:
9# - The old-style config framework (:file:`rcsetup.py`)
10# - The font manager (:file:`font_manager.py`)
12# It probably logically belongs in :file:`font_manager.py`, but placing it
13# there would have created cyclical dependency problems.
15from functools import lru_cache
16import re
17import numpy as np
18from pyparsing import (Literal, ZeroOrMore, Optional, Regex, StringEnd,
19 ParseException, Suppress)
21family_punc = r'\\\-:,'
22family_unescape = re.compile(r'\\([%s])' % family_punc).sub
23family_escape = re.compile(r'([%s])' % family_punc).sub
25value_punc = r'\\=_:,'
26value_unescape = re.compile(r'\\([%s])' % value_punc).sub
27value_escape = re.compile(r'([%s])' % value_punc).sub
30class FontconfigPatternParser:
31 """
32 A simple pyparsing-based parser for `fontconfig patterns`_.
34 .. _fontconfig patterns:
35 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
36 """
38 _constants = {
39 'thin': ('weight', 'light'),
40 'extralight': ('weight', 'light'),
41 'ultralight': ('weight', 'light'),
42 'light': ('weight', 'light'),
43 'book': ('weight', 'book'),
44 'regular': ('weight', 'regular'),
45 'normal': ('weight', 'normal'),
46 'medium': ('weight', 'medium'),
47 'demibold': ('weight', 'demibold'),
48 'semibold': ('weight', 'semibold'),
49 'bold': ('weight', 'bold'),
50 'extrabold': ('weight', 'extra bold'),
51 'black': ('weight', 'black'),
52 'heavy': ('weight', 'heavy'),
53 'roman': ('slant', 'normal'),
54 'italic': ('slant', 'italic'),
55 'oblique': ('slant', 'oblique'),
56 'ultracondensed': ('width', 'ultra-condensed'),
57 'extracondensed': ('width', 'extra-condensed'),
58 'condensed': ('width', 'condensed'),
59 'semicondensed': ('width', 'semi-condensed'),
60 'expanded': ('width', 'expanded'),
61 'extraexpanded': ('width', 'extra-expanded'),
62 'ultraexpanded': ('width', 'ultra-expanded')
63 }
65 def __init__(self):
67 family = Regex(
68 r'([^%s]|(\\[%s]))*' % (family_punc, family_punc)
69 ).setParseAction(self._family)
71 size = Regex(
72 r"([0-9]+\.?[0-9]*|\.[0-9]+)"
73 ).setParseAction(self._size)
75 name = Regex(
76 r'[a-z]+'
77 ).setParseAction(self._name)
79 value = Regex(
80 r'([^%s]|(\\[%s]))*' % (value_punc, value_punc)
81 ).setParseAction(self._value)
83 families = (
84 family
85 + ZeroOrMore(
86 Literal(',')
87 + family)
88 ).setParseAction(self._families)
90 point_sizes = (
91 size
92 + ZeroOrMore(
93 Literal(',')
94 + size)
95 ).setParseAction(self._point_sizes)
97 property = (
98 (name
99 + Suppress(Literal('='))
100 + value
101 + ZeroOrMore(
102 Suppress(Literal(','))
103 + value))
104 | name
105 ).setParseAction(self._property)
107 pattern = (
108 Optional(
109 families)
110 + Optional(
111 Literal('-')
112 + point_sizes)
113 + ZeroOrMore(
114 Literal(':')
115 + property)
116 + StringEnd()
117 )
119 self._parser = pattern
120 self.ParseException = ParseException
122 def parse(self, pattern):
123 """
124 Parse the given fontconfig *pattern* and return a dictionary
125 of key/value pairs useful for initializing a
126 :class:`font_manager.FontProperties` object.
127 """
128 props = self._properties = {}
129 try:
130 self._parser.parseString(pattern)
131 except self.ParseException as e:
132 raise ValueError(
133 "Could not parse font string: '%s'\n%s" % (pattern, e))
135 self._properties = None
137 self._parser.resetCache()
139 return props
141 def _family(self, s, loc, tokens):
142 return [family_unescape(r'\1', str(tokens[0]))]
144 def _size(self, s, loc, tokens):
145 return [float(tokens[0])]
147 def _name(self, s, loc, tokens):
148 return [str(tokens[0])]
150 def _value(self, s, loc, tokens):
151 return [value_unescape(r'\1', str(tokens[0]))]
153 def _families(self, s, loc, tokens):
154 self._properties['family'] = [str(x) for x in tokens]
155 return []
157 def _point_sizes(self, s, loc, tokens):
158 self._properties['size'] = [str(x) for x in tokens]
159 return []
161 def _property(self, s, loc, tokens):
162 if len(tokens) == 1:
163 if tokens[0] in self._constants:
164 key, val = self._constants[tokens[0]]
165 self._properties.setdefault(key, []).append(val)
166 else:
167 key = tokens[0]
168 val = tokens[1:]
169 self._properties.setdefault(key, []).extend(val)
170 return []
173# `parse_fontconfig_pattern` is a bottleneck during the tests because it is
174# repeatedly called when the rcParams are reset (to validate the default
175# fonts). In practice, the cache size doesn't grow beyond a few dozen entries
176# during the test suite.
177parse_fontconfig_pattern = lru_cache()(FontconfigPatternParser().parse)
180def _escape_val(val, escape_func):
181 """
182 Given a string value or a list of string values, run each value through
183 the input escape function to make the values into legal font config
184 strings. The result is returned as a string.
185 """
186 if not np.iterable(val) or isinstance(val, str):
187 val = [val]
189 return ','.join(escape_func(r'\\\1', str(x)) for x in val
190 if x is not None)
193def generate_fontconfig_pattern(d):
194 """
195 Given a dictionary of key/value pairs, generates a fontconfig
196 pattern string.
197 """
198 props = []
200 # Family is added first w/o a keyword
201 family = d.get_family()
202 if family is not None and family != []:
203 props.append(_escape_val(family, family_escape))
205 # The other keys are added as key=value
206 for key in ['style', 'variant', 'weight', 'stretch', 'file', 'size']:
207 val = getattr(d, 'get_' + key)()
208 # Don't use 'if not val' because 0 is a valid input.
209 if val is not None and val != []:
210 props.append(":%s=%s" % (key, _escape_val(val, value_escape)))
212 return ''.join(props)