Coverage for /usr/lib/python3/dist-packages/matplotlib/_fontconfig_pattern.py: 76%
70 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
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 logically belongs in `matplotlib.font_manager`, but placing it
9# there would have created cyclical dependency problems, because it also needs
10# to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files).
12from functools import lru_cache
13import re
14import numpy as np
15from pyparsing import (Literal, ZeroOrMore, Optional, Regex, StringEnd,
16 ParseException, Suppress)
18family_punc = r'\\\-:,'
19family_unescape = re.compile(r'\\([%s])' % family_punc).sub
20family_escape = re.compile(r'([%s])' % family_punc).sub
22value_punc = r'\\=_:,'
23value_unescape = re.compile(r'\\([%s])' % value_punc).sub
24value_escape = re.compile(r'([%s])' % value_punc).sub
27class FontconfigPatternParser:
28 """
29 A simple pyparsing-based parser for `fontconfig patterns`_.
31 .. _fontconfig patterns:
32 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
33 """
35 _constants = {
36 'thin': ('weight', 'light'),
37 'extralight': ('weight', 'light'),
38 'ultralight': ('weight', 'light'),
39 'light': ('weight', 'light'),
40 'book': ('weight', 'book'),
41 'regular': ('weight', 'regular'),
42 'normal': ('weight', 'normal'),
43 'medium': ('weight', 'medium'),
44 'demibold': ('weight', 'demibold'),
45 'semibold': ('weight', 'semibold'),
46 'bold': ('weight', 'bold'),
47 'extrabold': ('weight', 'extra bold'),
48 'black': ('weight', 'black'),
49 'heavy': ('weight', 'heavy'),
50 'roman': ('slant', 'normal'),
51 'italic': ('slant', 'italic'),
52 'oblique': ('slant', 'oblique'),
53 'ultracondensed': ('width', 'ultra-condensed'),
54 'extracondensed': ('width', 'extra-condensed'),
55 'condensed': ('width', 'condensed'),
56 'semicondensed': ('width', 'semi-condensed'),
57 'expanded': ('width', 'expanded'),
58 'extraexpanded': ('width', 'extra-expanded'),
59 'ultraexpanded': ('width', 'ultra-expanded')
60 }
62 def __init__(self):
64 family = Regex(
65 r'([^%s]|(\\[%s]))*' % (family_punc, family_punc)
66 ).setParseAction(self._family)
68 size = Regex(
69 r"([0-9]+\.?[0-9]*|\.[0-9]+)"
70 ).setParseAction(self._size)
72 name = Regex(
73 r'[a-z]+'
74 ).setParseAction(self._name)
76 value = Regex(
77 r'([^%s]|(\\[%s]))*' % (value_punc, value_punc)
78 ).setParseAction(self._value)
80 families = (
81 family
82 + ZeroOrMore(
83 Literal(',')
84 + family)
85 ).setParseAction(self._families)
87 point_sizes = (
88 size
89 + ZeroOrMore(
90 Literal(',')
91 + size)
92 ).setParseAction(self._point_sizes)
94 property = (
95 (name
96 + Suppress(Literal('='))
97 + value
98 + ZeroOrMore(
99 Suppress(Literal(','))
100 + value))
101 | name
102 ).setParseAction(self._property)
104 pattern = (
105 Optional(
106 families)
107 + Optional(
108 Literal('-')
109 + point_sizes)
110 + ZeroOrMore(
111 Literal(':')
112 + property)
113 + StringEnd()
114 )
116 self._parser = pattern
117 self.ParseException = ParseException
119 def parse(self, pattern):
120 """
121 Parse the given fontconfig *pattern* and return a dictionary
122 of key/value pairs useful for initializing a
123 `.font_manager.FontProperties` object.
124 """
125 props = self._properties = {}
126 try:
127 self._parser.parseString(pattern)
128 except self.ParseException as e:
129 raise ValueError(
130 "Could not parse font string: '%s'\n%s" % (pattern, e)) from e
132 self._properties = None
134 self._parser.resetCache()
136 return props
138 def _family(self, s, loc, tokens):
139 return [family_unescape(r'\1', str(tokens[0]))]
141 def _size(self, s, loc, tokens):
142 return [float(tokens[0])]
144 def _name(self, s, loc, tokens):
145 return [str(tokens[0])]
147 def _value(self, s, loc, tokens):
148 return [value_unescape(r'\1', str(tokens[0]))]
150 def _families(self, s, loc, tokens):
151 self._properties['family'] = [str(x) for x in tokens]
152 return []
154 def _point_sizes(self, s, loc, tokens):
155 self._properties['size'] = [str(x) for x in tokens]
156 return []
158 def _property(self, s, loc, tokens):
159 if len(tokens) == 1:
160 if tokens[0] in self._constants:
161 key, val = self._constants[tokens[0]]
162 self._properties.setdefault(key, []).append(val)
163 else:
164 key = tokens[0]
165 val = tokens[1:]
166 self._properties.setdefault(key, []).extend(val)
167 return []
170# `parse_fontconfig_pattern` is a bottleneck during the tests because it is
171# repeatedly called when the rcParams are reset (to validate the default
172# fonts). In practice, the cache size doesn't grow beyond a few dozen entries
173# during the test suite.
174parse_fontconfig_pattern = lru_cache()(FontconfigPatternParser().parse)
177def _escape_val(val, escape_func):
178 """
179 Given a string value or a list of string values, run each value through
180 the input escape function to make the values into legal font config
181 strings. The result is returned as a string.
182 """
183 if not np.iterable(val) or isinstance(val, str):
184 val = [val]
186 return ','.join(escape_func(r'\\\1', str(x)) for x in val
187 if x is not None)
190def generate_fontconfig_pattern(d):
191 """
192 Given a dictionary of key/value pairs, generates a fontconfig
193 pattern string.
194 """
195 props = []
197 # Family is added first w/o a keyword
198 family = d.get_family()
199 if family is not None and family != []:
200 props.append(_escape_val(family, family_escape))
202 # The other keys are added as key=value
203 for key in ['style', 'variant', 'weight', 'stretch', 'file', 'size']:
204 val = getattr(d, 'get_' + key)()
205 # Don't use 'if not val' because 0 is a valid input.
206 if val is not None and val != []:
207 props.append(":%s=%s" % (key, _escape_val(val, value_escape)))
209 return ''.join(props)