|
18 | 18 | from matplotlib import _api |
19 | 19 |
|
20 | 20 |
|
21 | | -family_punc = r'\\\-:,' |
22 | | -_family_unescape = partial(re.compile(r'\\(?=[%s])' % family_punc).sub, '') |
23 | | -_family_escape = partial(re.compile(r'(?=[%s])' % family_punc).sub, r'\\') |
24 | | -value_punc = r'\\=_:,' |
25 | | -_value_unescape = partial(re.compile(r'\\(?=[%s])' % value_punc).sub, '') |
26 | | -_value_escape = partial(re.compile(r'(?=[%s])' % value_punc).sub, r'\\') |
27 | | - |
28 | | -# Remove after module deprecation elapses (3.8); then remove underscores |
29 | | -# from _{family,value}_{un,}escape. |
30 | | -family_unescape = re.compile(r'\\([%s])' % family_punc).sub |
31 | | -value_unescape = re.compile(r'\\([%s])' % value_punc).sub |
32 | | -family_escape = re.compile(r'([%s])' % family_punc).sub |
33 | | -value_escape = re.compile(r'([%s])' % value_punc).sub |
34 | | - |
35 | | - |
36 | | -class FontconfigPatternParser: |
37 | | - """ |
38 | | - A simple pyparsing-based parser for `fontconfig patterns`_. |
39 | | -
|
40 | | - .. _fontconfig patterns: |
41 | | - https://www.freedesktop.org/software/fontconfig/fontconfig-user.html |
42 | | - """ |
43 | | - |
44 | | - _constants = { |
45 | | - 'thin': ('weight', 'light'), |
46 | | - 'extralight': ('weight', 'light'), |
47 | | - 'ultralight': ('weight', 'light'), |
48 | | - 'light': ('weight', 'light'), |
49 | | - 'book': ('weight', 'book'), |
50 | | - 'regular': ('weight', 'regular'), |
51 | | - 'normal': ('weight', 'normal'), |
52 | | - 'medium': ('weight', 'medium'), |
53 | | - 'demibold': ('weight', 'demibold'), |
54 | | - 'semibold': ('weight', 'semibold'), |
55 | | - 'bold': ('weight', 'bold'), |
56 | | - 'extrabold': ('weight', 'extra bold'), |
57 | | - 'black': ('weight', 'black'), |
58 | | - 'heavy': ('weight', 'heavy'), |
59 | | - 'roman': ('slant', 'normal'), |
60 | | - 'italic': ('slant', 'italic'), |
61 | | - 'oblique': ('slant', 'oblique'), |
62 | | - 'ultracondensed': ('width', 'ultra-condensed'), |
63 | | - 'extracondensed': ('width', 'extra-condensed'), |
64 | | - 'condensed': ('width', 'condensed'), |
65 | | - 'semicondensed': ('width', 'semi-condensed'), |
66 | | - 'expanded': ('width', 'expanded'), |
67 | | - 'extraexpanded': ('width', 'extra-expanded'), |
68 | | - 'ultraexpanded': ('width', 'ultra-expanded'), |
69 | | - } |
70 | | - |
71 | | - def __init__(self): |
72 | | - def comma_separated(elem): |
73 | | - return elem + ZeroOrMore(Suppress(",") + elem) |
74 | | - |
75 | | - family = Regex(r"([^%s]|(\\[%s]))*" % (family_punc, family_punc)) |
76 | | - size = Regex(r"([0-9]+\.?[0-9]*|\.[0-9]+)") |
77 | | - name = Regex(r"[a-z]+") |
78 | | - value = Regex(r"([^%s]|(\\[%s]))*" % (value_punc, value_punc)) |
79 | | - prop = ( |
80 | | - (name + Suppress("=") + comma_separated(value)) |
81 | | - | name # replace by oneOf(self._constants) in mpl 3.9. |
82 | | - ) |
83 | | - pattern = ( |
84 | | - Optional(comma_separated(family)("families")) |
85 | | - + Optional("-" + comma_separated(size)("sizes")) |
86 | | - + ZeroOrMore(":" + prop("properties*")) |
87 | | - + StringEnd() |
88 | | - ) |
89 | | - self._parser = pattern |
90 | | - self.ParseException = ParseException |
91 | | - |
92 | | - def parse(self, pattern): |
93 | | - """ |
94 | | - Parse the given fontconfig *pattern* and return a dictionary |
95 | | - of key/value pairs useful for initializing a |
96 | | - `.font_manager.FontProperties` object. |
97 | | - """ |
98 | | - try: |
99 | | - parse = self._parser.parseString(pattern) |
100 | | - except ParseException as err: |
101 | | - # explain becomes a plain method on pyparsing 3 (err.explain(0)). |
102 | | - raise ValueError("\n" + ParseException.explain(err, 0)) from None |
103 | | - self._parser.resetCache() |
104 | | - props = {} |
105 | | - if "families" in parse: |
106 | | - props["family"] = [*map(_family_unescape, parse["families"])] |
107 | | - if "sizes" in parse: |
108 | | - props["size"] = [*parse["sizes"]] |
109 | | - for prop in parse.get("properties", []): |
110 | | - if len(prop) == 1: |
111 | | - if prop[0] not in self._constants: |
112 | | - _api.warn_deprecated( |
113 | | - "3.7", message=f"Support for unknown constants " |
114 | | - f"({prop[0]!r}) is deprecated since %(since)s and " |
115 | | - f"will be removed %(removal)s.") |
116 | | - continue |
117 | | - prop = self._constants[prop[0]] |
118 | | - k, *v = prop |
119 | | - props.setdefault(k, []).extend(map(_value_unescape, v)) |
120 | | - return props |
| 21 | +_family_punc = r'\\\-:,' |
| 22 | +_family_unescape = partial(re.compile(r'\\(?=[%s])' % _family_punc).sub, '') |
| 23 | +_family_escape = partial(re.compile(r'(?=[%s])' % _family_punc).sub, r'\\') |
| 24 | +_value_punc = r'\\=_:,' |
| 25 | +_value_unescape = partial(re.compile(r'\\(?=[%s])' % _value_punc).sub, '') |
| 26 | +_value_escape = partial(re.compile(r'(?=[%s])' % _value_punc).sub, r'\\') |
| 27 | + |
| 28 | + |
| 29 | +_CONSTANTS = { |
| 30 | + 'thin': ('weight', 'light'), |
| 31 | + 'extralight': ('weight', 'light'), |
| 32 | + 'ultralight': ('weight', 'light'), |
| 33 | + 'light': ('weight', 'light'), |
| 34 | + 'book': ('weight', 'book'), |
| 35 | + 'regular': ('weight', 'regular'), |
| 36 | + 'normal': ('weight', 'normal'), |
| 37 | + 'medium': ('weight', 'medium'), |
| 38 | + 'demibold': ('weight', 'demibold'), |
| 39 | + 'semibold': ('weight', 'semibold'), |
| 40 | + 'bold': ('weight', 'bold'), |
| 41 | + 'extrabold': ('weight', 'extra bold'), |
| 42 | + 'black': ('weight', 'black'), |
| 43 | + 'heavy': ('weight', 'heavy'), |
| 44 | + 'roman': ('slant', 'normal'), |
| 45 | + 'italic': ('slant', 'italic'), |
| 46 | + 'oblique': ('slant', 'oblique'), |
| 47 | + 'ultracondensed': ('width', 'ultra-condensed'), |
| 48 | + 'extracondensed': ('width', 'extra-condensed'), |
| 49 | + 'condensed': ('width', 'condensed'), |
| 50 | + 'semicondensed': ('width', 'semi-condensed'), |
| 51 | + 'expanded': ('width', 'expanded'), |
| 52 | + 'extraexpanded': ('width', 'extra-expanded'), |
| 53 | + 'ultraexpanded': ('width', 'ultra-expanded'), |
| 54 | +} |
| 55 | + |
| 56 | + |
| 57 | +@lru_cache # The parser instance is a singleton. |
| 58 | +def _make_fontconfig_parser(): |
| 59 | + def comma_separated(elem): |
| 60 | + return elem + ZeroOrMore(Suppress(",") + elem) |
| 61 | + |
| 62 | + family = Regex(r"([^%s]|(\\[%s]))*" % (_family_punc, _family_punc)) |
| 63 | + size = Regex(r"([0-9]+\.?[0-9]*|\.[0-9]+)") |
| 64 | + name = Regex(r"[a-z]+") |
| 65 | + value = Regex(r"([^%s]|(\\[%s]))*" % (_value_punc, _value_punc)) |
| 66 | + # replace trailing `| name` by oneOf(_CONSTANTS) in mpl 3.9. |
| 67 | + prop = (name + Suppress("=") + comma_separated(value)) | name |
| 68 | + return ( |
| 69 | + Optional(comma_separated(family)("families")) |
| 70 | + + Optional("-" + comma_separated(size)("sizes")) |
| 71 | + + ZeroOrMore(":" + prop("properties*")) |
| 72 | + + StringEnd() |
| 73 | + ) |
121 | 74 |
|
122 | 75 |
|
123 | 76 | # `parse_fontconfig_pattern` is a bottleneck during the tests because it is |
124 | 77 | # repeatedly called when the rcParams are reset (to validate the default |
125 | 78 | # fonts). In practice, the cache size doesn't grow beyond a few dozen entries |
126 | 79 | # during the test suite. |
127 | | -parse_fontconfig_pattern = lru_cache()(FontconfigPatternParser().parse) |
| 80 | +@lru_cache |
| 81 | +def parse_fontconfig_pattern(pattern): |
| 82 | + """ |
| 83 | + Parse a fontconfig *pattern* into a dict that can initialize a |
| 84 | + `.font_manager.FontProperties` object. |
| 85 | + """ |
| 86 | + parser = _make_fontconfig_parser() |
| 87 | + try: |
| 88 | + parse = parser.parseString(pattern) |
| 89 | + except ParseException as err: |
| 90 | + # explain becomes a plain method on pyparsing 3 (err.explain(0)). |
| 91 | + raise ValueError("\n" + ParseException.explain(err, 0)) from None |
| 92 | + parser.resetCache() |
| 93 | + props = {} |
| 94 | + if "families" in parse: |
| 95 | + props["family"] = [*map(_family_unescape, parse["families"])] |
| 96 | + if "sizes" in parse: |
| 97 | + props["size"] = [*parse["sizes"]] |
| 98 | + for prop in parse.get("properties", []): |
| 99 | + if len(prop) == 1: |
| 100 | + if prop[0] not in _CONSTANTS: |
| 101 | + _api.warn_deprecated( |
| 102 | + "3.7", message=f"Support for unknown constants " |
| 103 | + f"({prop[0]!r}) is deprecated since %(since)s and " |
| 104 | + f"will be removed %(removal)s.") |
| 105 | + continue |
| 106 | + prop = _CONSTANTS[prop[0]] |
| 107 | + k, *v = prop |
| 108 | + props.setdefault(k, []).extend(map(_value_unescape, v)) |
| 109 | + return props |
128 | 110 |
|
129 | 111 |
|
130 | 112 | def generate_fontconfig_pattern(d): |
|
0 commit comments