123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- """
- 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))
|