1"""
2A module for parsing and generating `fontconfig patterns`_.
3
4.. _fontconfig patterns:
5 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
6"""
7
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).
11
12from functools import lru_cache, partial
13import re
14
15from pyparsing import (
16 Group, Optional, ParseException, Regex, StringEnd, Suppress, ZeroOrMore, oneOf)
17
18
19_family_punc = r'\\\-:,'
20_family_unescape = partial(re.compile(r'\\(?=[%s])' % _family_punc).sub, '')
21_family_escape = partial(re.compile(r'(?=[%s])' % _family_punc).sub, r'\\')
22_value_punc = r'\\=_:,'
23_value_unescape = partial(re.compile(r'\\(?=[%s])' % _value_punc).sub, '')
24_value_escape = partial(re.compile(r'(?=[%s])' % _value_punc).sub, r'\\')
25
26
27_CONSTANTS = {
28 'thin': ('weight', 'light'),
29 'extralight': ('weight', 'light'),
30 'ultralight': ('weight', 'light'),
31 'light': ('weight', 'light'),
32 'book': ('weight', 'book'),
33 'regular': ('weight', 'regular'),
34 'normal': ('weight', 'normal'),
35 'medium': ('weight', 'medium'),
36 'demibold': ('weight', 'demibold'),
37 'semibold': ('weight', 'semibold'),
38 'bold': ('weight', 'bold'),
39 'extrabold': ('weight', 'extra bold'),
40 'black': ('weight', 'black'),
41 'heavy': ('weight', 'heavy'),
42 'roman': ('slant', 'normal'),
43 'italic': ('slant', 'italic'),
44 'oblique': ('slant', 'oblique'),
45 'ultracondensed': ('width', 'ultra-condensed'),
46 'extracondensed': ('width', 'extra-condensed'),
47 'condensed': ('width', 'condensed'),
48 'semicondensed': ('width', 'semi-condensed'),
49 'expanded': ('width', 'expanded'),
50 'extraexpanded': ('width', 'extra-expanded'),
51 'ultraexpanded': ('width', 'ultra-expanded'),
52}
53
54
55@lru_cache # The parser instance is a singleton.
56def _make_fontconfig_parser():
57 def comma_separated(elem):
58 return elem + ZeroOrMore(Suppress(",") + elem)
59
60 family = Regex(fr"([^{_family_punc}]|(\\[{_family_punc}]))*")
61 size = Regex(r"([0-9]+\.?[0-9]*|\.[0-9]+)")
62 name = Regex(r"[a-z]+")
63 value = Regex(fr"([^{_value_punc}]|(\\[{_value_punc}]))*")
64 prop = Group((name + Suppress("=") + comma_separated(value)) | oneOf(_CONSTANTS))
65 return (
66 Optional(comma_separated(family)("families"))
67 + Optional("-" + comma_separated(size)("sizes"))
68 + ZeroOrMore(":" + prop("properties*"))
69 + StringEnd()
70 )
71
72
73# `parse_fontconfig_pattern` is a bottleneck during the tests because it is
74# repeatedly called when the rcParams are reset (to validate the default
75# fonts). In practice, the cache size doesn't grow beyond a few dozen entries
76# during the test suite.
77@lru_cache
78def parse_fontconfig_pattern(pattern):
79 """
80 Parse a fontconfig *pattern* into a dict that can initialize a
81 `.font_manager.FontProperties` object.
82 """
83 parser = _make_fontconfig_parser()
84 try:
85 parse = parser.parseString(pattern)
86 except ParseException as err:
87 # explain becomes a plain method on pyparsing 3 (err.explain(0)).
88 raise ValueError("\n" + ParseException.explain(err, 0)) from None
89 parser.resetCache()
90 props = {}
91 if "families" in parse:
92 props["family"] = [*map(_family_unescape, parse["families"])]
93 if "sizes" in parse:
94 props["size"] = [*parse["sizes"]]
95 for prop in parse.get("properties", []):
96 if len(prop) == 1:
97 prop = _CONSTANTS[prop[0]]
98 k, *v = prop
99 props.setdefault(k, []).extend(map(_value_unescape, v))
100 return props
101
102
103def generate_fontconfig_pattern(d):
104 """Convert a `.FontProperties` to a fontconfig pattern string."""
105 kvs = [(k, getattr(d, f"get_{k}")())
106 for k in ["style", "variant", "weight", "stretch", "file", "size"]]
107 # Families is given first without a leading keyword. Other entries (which
108 # are necessarily scalar) are given as key=value, skipping Nones.
109 return (",".join(_family_escape(f) for f in d.get_family())
110 + "".join(f":{k}={_value_escape(str(v))}"
111 for k, v in kvs if v is not None))