fontconfig_pattern.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. """
  2. A module for parsing and generating `fontconfig patterns`_.
  3. .. _fontconfig patterns:
  4. https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
  5. """
  6. # This class logically belongs in `matplotlib.font_manager`, but placing it
  7. # there would have created cyclical dependency problems, because it also needs
  8. # to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files).
  9. from functools import lru_cache
  10. import re
  11. import numpy as np
  12. from pyparsing import (Literal, ZeroOrMore, Optional, Regex, StringEnd,
  13. ParseException, Suppress)
  14. family_punc = r'\\\-:,'
  15. family_unescape = re.compile(r'\\([%s])' % family_punc).sub
  16. family_escape = re.compile(r'([%s])' % family_punc).sub
  17. value_punc = r'\\=_:,'
  18. value_unescape = re.compile(r'\\([%s])' % value_punc).sub
  19. value_escape = re.compile(r'([%s])' % value_punc).sub
  20. class FontconfigPatternParser:
  21. """
  22. A simple pyparsing-based parser for `fontconfig patterns`_.
  23. .. _fontconfig patterns:
  24. https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
  25. """
  26. _constants = {
  27. 'thin': ('weight', 'light'),
  28. 'extralight': ('weight', 'light'),
  29. 'ultralight': ('weight', 'light'),
  30. 'light': ('weight', 'light'),
  31. 'book': ('weight', 'book'),
  32. 'regular': ('weight', 'regular'),
  33. 'normal': ('weight', 'normal'),
  34. 'medium': ('weight', 'medium'),
  35. 'demibold': ('weight', 'demibold'),
  36. 'semibold': ('weight', 'semibold'),
  37. 'bold': ('weight', 'bold'),
  38. 'extrabold': ('weight', 'extra bold'),
  39. 'black': ('weight', 'black'),
  40. 'heavy': ('weight', 'heavy'),
  41. 'roman': ('slant', 'normal'),
  42. 'italic': ('slant', 'italic'),
  43. 'oblique': ('slant', 'oblique'),
  44. 'ultracondensed': ('width', 'ultra-condensed'),
  45. 'extracondensed': ('width', 'extra-condensed'),
  46. 'condensed': ('width', 'condensed'),
  47. 'semicondensed': ('width', 'semi-condensed'),
  48. 'expanded': ('width', 'expanded'),
  49. 'extraexpanded': ('width', 'extra-expanded'),
  50. 'ultraexpanded': ('width', 'ultra-expanded')
  51. }
  52. def __init__(self):
  53. family = Regex(
  54. r'([^%s]|(\\[%s]))*' % (family_punc, family_punc)
  55. ).setParseAction(self._family)
  56. size = Regex(
  57. r"([0-9]+\.?[0-9]*|\.[0-9]+)"
  58. ).setParseAction(self._size)
  59. name = Regex(
  60. r'[a-z]+'
  61. ).setParseAction(self._name)
  62. value = Regex(
  63. r'([^%s]|(\\[%s]))*' % (value_punc, value_punc)
  64. ).setParseAction(self._value)
  65. families = (
  66. family
  67. + ZeroOrMore(
  68. Literal(',')
  69. + family)
  70. ).setParseAction(self._families)
  71. point_sizes = (
  72. size
  73. + ZeroOrMore(
  74. Literal(',')
  75. + size)
  76. ).setParseAction(self._point_sizes)
  77. property = (
  78. (name
  79. + Suppress(Literal('='))
  80. + value
  81. + ZeroOrMore(
  82. Suppress(Literal(','))
  83. + value))
  84. | name
  85. ).setParseAction(self._property)
  86. pattern = (
  87. Optional(
  88. families)
  89. + Optional(
  90. Literal('-')
  91. + point_sizes)
  92. + ZeroOrMore(
  93. Literal(':')
  94. + property)
  95. + StringEnd()
  96. )
  97. self._parser = pattern
  98. self.ParseException = ParseException
  99. def parse(self, pattern):
  100. """
  101. Parse the given fontconfig *pattern* and return a dictionary
  102. of key/value pairs useful for initializing a
  103. `.font_manager.FontProperties` object.
  104. """
  105. props = self._properties = {}
  106. try:
  107. self._parser.parseString(pattern)
  108. except self.ParseException as e:
  109. raise ValueError(
  110. "Could not parse font string: '%s'\n%s" % (pattern, e)) from e
  111. self._properties = None
  112. self._parser.resetCache()
  113. return props
  114. def _family(self, s, loc, tokens):
  115. return [family_unescape(r'\1', str(tokens[0]))]
  116. def _size(self, s, loc, tokens):
  117. return [float(tokens[0])]
  118. def _name(self, s, loc, tokens):
  119. return [str(tokens[0])]
  120. def _value(self, s, loc, tokens):
  121. return [value_unescape(r'\1', str(tokens[0]))]
  122. def _families(self, s, loc, tokens):
  123. self._properties['family'] = [str(x) for x in tokens]
  124. return []
  125. def _point_sizes(self, s, loc, tokens):
  126. self._properties['size'] = [str(x) for x in tokens]
  127. return []
  128. def _property(self, s, loc, tokens):
  129. if len(tokens) == 1:
  130. if tokens[0] in self._constants:
  131. key, val = self._constants[tokens[0]]
  132. self._properties.setdefault(key, []).append(val)
  133. else:
  134. key = tokens[0]
  135. val = tokens[1:]
  136. self._properties.setdefault(key, []).extend(val)
  137. return []
  138. # `parse_fontconfig_pattern` is a bottleneck during the tests because it is
  139. # repeatedly called when the rcParams are reset (to validate the default
  140. # fonts). In practice, the cache size doesn't grow beyond a few dozen entries
  141. # during the test suite.
  142. parse_fontconfig_pattern = lru_cache()(FontconfigPatternParser().parse)
  143. def _escape_val(val, escape_func):
  144. """
  145. Given a string value or a list of string values, run each value through
  146. the input escape function to make the values into legal font config
  147. strings. The result is returned as a string.
  148. """
  149. if not np.iterable(val) or isinstance(val, str):
  150. val = [val]
  151. return ','.join(escape_func(r'\\\1', str(x)) for x in val
  152. if x is not None)
  153. def generate_fontconfig_pattern(d):
  154. """
  155. Given a dictionary of key/value pairs, generates a fontconfig
  156. pattern string.
  157. """
  158. props = []
  159. # Family is added first w/o a keyword
  160. family = d.get_family()
  161. if family is not None and family != []:
  162. props.append(_escape_val(family, family_escape))
  163. # The other keys are added as key=value
  164. for key in ['style', 'variant', 'weight', 'stretch', 'file', 'size']:
  165. val = getattr(d, 'get_' + key)()
  166. # Don't use 'if not val' because 0 is a valid input.
  167. if val is not None and val != []:
  168. props.append(":%s=%s" % (key, _escape_val(val, value_escape)))
  169. return ''.join(props)