File size: 4,734 Bytes
fe41391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""
A module for parsing and generating `fontconfig patterns`_.

.. _fontconfig patterns:
   https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
"""

# This class logically belongs in `matplotlib.font_manager`, but placing it
# there would have created cyclical dependency problems, because it also needs
# to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files).

from functools import lru_cache, partial
import re

from pyparsing import (
    Group, Optional, ParseException, Regex, StringEnd, Suppress, ZeroOrMore)

from matplotlib import _api


_family_punc = r'\\\-:,'
_family_unescape = partial(re.compile(r'\\(?=[%s])' % _family_punc).sub, '')
_family_escape = partial(re.compile(r'(?=[%s])' % _family_punc).sub, r'\\')
_value_punc = r'\\=_:,'
_value_unescape = partial(re.compile(r'\\(?=[%s])' % _value_punc).sub, '')
_value_escape = partial(re.compile(r'(?=[%s])' % _value_punc).sub, r'\\')


_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'),
}


@lru_cache  # The parser instance is a singleton.
def _make_fontconfig_parser():
    def comma_separated(elem):
        return elem + ZeroOrMore(Suppress(",") + elem)

    family = Regex(fr"([^{_family_punc}]|(\\[{_family_punc}]))*")
    size = Regex(r"([0-9]+\.?[0-9]*|\.[0-9]+)")
    name = Regex(r"[a-z]+")
    value = Regex(fr"([^{_value_punc}]|(\\[{_value_punc}]))*")
    # replace trailing `| name` by oneOf(_CONSTANTS) in mpl 3.9.
    prop = Group((name + Suppress("=") + comma_separated(value)) | name)
    return (
        Optional(comma_separated(family)("families"))
        + Optional("-" + comma_separated(size)("sizes"))
        + ZeroOrMore(":" + prop("properties*"))
        + StringEnd()
    )


# `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.
@lru_cache
def parse_fontconfig_pattern(pattern):
    """
    Parse a fontconfig *pattern* into a dict that can initialize a
    `.font_manager.FontProperties` object.
    """
    parser = _make_fontconfig_parser()
    try:
        parse = parser.parseString(pattern)
    except ParseException as err:
        # explain becomes a plain method on pyparsing 3 (err.explain(0)).
        raise ValueError("\n" + ParseException.explain(err, 0)) from None
    parser.resetCache()
    props = {}
    if "families" in parse:
        props["family"] = [*map(_family_unescape, parse["families"])]
    if "sizes" in parse:
        props["size"] = [*parse["sizes"]]
    for prop in parse.get("properties", []):
        if len(prop) == 1:
            if prop[0] not in _CONSTANTS:
                _api.warn_deprecated(
                    "3.7", message=f"Support for unknown constants "
                    f"({prop[0]!r}) is deprecated since %(since)s and "
                    f"will be removed %(removal)s.")
                continue
            prop = _CONSTANTS[prop[0]]
        k, *v = prop
        props.setdefault(k, []).extend(map(_value_unescape, v))
    return props


def generate_fontconfig_pattern(d):
    """Convert a `.FontProperties` to a fontconfig pattern string."""
    kvs = [(k, getattr(d, f"get_{k}")())
           for k in ["style", "variant", "weight", "stretch", "file", "size"]]
    # Families is given first without a leading keyword.  Other entries (which
    # are necessarily scalar) are given as key=value, skipping Nones.
    return (",".join(_family_escape(f) for f in d.get_family())
            + "".join(f":{k}={_value_escape(str(v))}"
                      for k, v in kvs if v is not None))