Coverage for /usr/lib/python3/dist-packages/matplotlib/_fontconfig_pattern.py: 76%

70 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1""" 

2A module for parsing and generating `fontconfig patterns`_. 

3 

4.. _fontconfig patterns: 

5 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html 

6""" 

7 

8# This class logically belongs in `matplotlib.font_manager`, but placing it 

9# there would have created cyclical dependency problems, because it also needs 

10# to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files). 

11 

12from functools import lru_cache 

13import re 

14import numpy as np 

15from pyparsing import (Literal, ZeroOrMore, Optional, Regex, StringEnd, 

16 ParseException, Suppress) 

17 

18family_punc = r'\\\-:,' 

19family_unescape = re.compile(r'\\([%s])' % family_punc).sub 

20family_escape = re.compile(r'([%s])' % family_punc).sub 

21 

22value_punc = r'\\=_:,' 

23value_unescape = re.compile(r'\\([%s])' % value_punc).sub 

24value_escape = re.compile(r'([%s])' % value_punc).sub 

25 

26 

27class FontconfigPatternParser: 

28 """ 

29 A simple pyparsing-based parser for `fontconfig patterns`_. 

30 

31 .. _fontconfig patterns: 

32 https://www.freedesktop.org/software/fontconfig/fontconfig-user.html 

33 """ 

34 

35 _constants = { 

36 'thin': ('weight', 'light'), 

37 'extralight': ('weight', 'light'), 

38 'ultralight': ('weight', 'light'), 

39 'light': ('weight', 'light'), 

40 'book': ('weight', 'book'), 

41 'regular': ('weight', 'regular'), 

42 'normal': ('weight', 'normal'), 

43 'medium': ('weight', 'medium'), 

44 'demibold': ('weight', 'demibold'), 

45 'semibold': ('weight', 'semibold'), 

46 'bold': ('weight', 'bold'), 

47 'extrabold': ('weight', 'extra bold'), 

48 'black': ('weight', 'black'), 

49 'heavy': ('weight', 'heavy'), 

50 'roman': ('slant', 'normal'), 

51 'italic': ('slant', 'italic'), 

52 'oblique': ('slant', 'oblique'), 

53 'ultracondensed': ('width', 'ultra-condensed'), 

54 'extracondensed': ('width', 'extra-condensed'), 

55 'condensed': ('width', 'condensed'), 

56 'semicondensed': ('width', 'semi-condensed'), 

57 'expanded': ('width', 'expanded'), 

58 'extraexpanded': ('width', 'extra-expanded'), 

59 'ultraexpanded': ('width', 'ultra-expanded') 

60 } 

61 

62 def __init__(self): 

63 

64 family = Regex( 

65 r'([^%s]|(\\[%s]))*' % (family_punc, family_punc) 

66 ).setParseAction(self._family) 

67 

68 size = Regex( 

69 r"([0-9]+\.?[0-9]*|\.[0-9]+)" 

70 ).setParseAction(self._size) 

71 

72 name = Regex( 

73 r'[a-z]+' 

74 ).setParseAction(self._name) 

75 

76 value = Regex( 

77 r'([^%s]|(\\[%s]))*' % (value_punc, value_punc) 

78 ).setParseAction(self._value) 

79 

80 families = ( 

81 family 

82 + ZeroOrMore( 

83 Literal(',') 

84 + family) 

85 ).setParseAction(self._families) 

86 

87 point_sizes = ( 

88 size 

89 + ZeroOrMore( 

90 Literal(',') 

91 + size) 

92 ).setParseAction(self._point_sizes) 

93 

94 property = ( 

95 (name 

96 + Suppress(Literal('=')) 

97 + value 

98 + ZeroOrMore( 

99 Suppress(Literal(',')) 

100 + value)) 

101 | name 

102 ).setParseAction(self._property) 

103 

104 pattern = ( 

105 Optional( 

106 families) 

107 + Optional( 

108 Literal('-') 

109 + point_sizes) 

110 + ZeroOrMore( 

111 Literal(':') 

112 + property) 

113 + StringEnd() 

114 ) 

115 

116 self._parser = pattern 

117 self.ParseException = ParseException 

118 

119 def parse(self, pattern): 

120 """ 

121 Parse the given fontconfig *pattern* and return a dictionary 

122 of key/value pairs useful for initializing a 

123 `.font_manager.FontProperties` object. 

124 """ 

125 props = self._properties = {} 

126 try: 

127 self._parser.parseString(pattern) 

128 except self.ParseException as e: 

129 raise ValueError( 

130 "Could not parse font string: '%s'\n%s" % (pattern, e)) from e 

131 

132 self._properties = None 

133 

134 self._parser.resetCache() 

135 

136 return props 

137 

138 def _family(self, s, loc, tokens): 

139 return [family_unescape(r'\1', str(tokens[0]))] 

140 

141 def _size(self, s, loc, tokens): 

142 return [float(tokens[0])] 

143 

144 def _name(self, s, loc, tokens): 

145 return [str(tokens[0])] 

146 

147 def _value(self, s, loc, tokens): 

148 return [value_unescape(r'\1', str(tokens[0]))] 

149 

150 def _families(self, s, loc, tokens): 

151 self._properties['family'] = [str(x) for x in tokens] 

152 return [] 

153 

154 def _point_sizes(self, s, loc, tokens): 

155 self._properties['size'] = [str(x) for x in tokens] 

156 return [] 

157 

158 def _property(self, s, loc, tokens): 

159 if len(tokens) == 1: 

160 if tokens[0] in self._constants: 

161 key, val = self._constants[tokens[0]] 

162 self._properties.setdefault(key, []).append(val) 

163 else: 

164 key = tokens[0] 

165 val = tokens[1:] 

166 self._properties.setdefault(key, []).extend(val) 

167 return [] 

168 

169 

170# `parse_fontconfig_pattern` is a bottleneck during the tests because it is 

171# repeatedly called when the rcParams are reset (to validate the default 

172# fonts). In practice, the cache size doesn't grow beyond a few dozen entries 

173# during the test suite. 

174parse_fontconfig_pattern = lru_cache()(FontconfigPatternParser().parse) 

175 

176 

177def _escape_val(val, escape_func): 

178 """ 

179 Given a string value or a list of string values, run each value through 

180 the input escape function to make the values into legal font config 

181 strings. The result is returned as a string. 

182 """ 

183 if not np.iterable(val) or isinstance(val, str): 

184 val = [val] 

185 

186 return ','.join(escape_func(r'\\\1', str(x)) for x in val 

187 if x is not None) 

188 

189 

190def generate_fontconfig_pattern(d): 

191 """ 

192 Given a dictionary of key/value pairs, generates a fontconfig 

193 pattern string. 

194 """ 

195 props = [] 

196 

197 # Family is added first w/o a keyword 

198 family = d.get_family() 

199 if family is not None and family != []: 

200 props.append(_escape_val(family, family_escape)) 

201 

202 # The other keys are added as key=value 

203 for key in ['style', 'variant', 'weight', 'stretch', 'file', 'size']: 

204 val = getattr(d, 'get_' + key)() 

205 # Don't use 'if not val' because 0 is a valid input. 

206 if val is not None and val != []: 

207 props.append(":%s=%s" % (key, _escape_val(val, value_escape))) 

208 

209 return ''.join(props)