fontconfig_pattern.py 6.6 KB

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