Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 is defined here because it must be available in: 

9# - The old-style config framework (:file:`rcsetup.py`) 

10# - The font manager (:file:`font_manager.py`) 

11 

12# It probably logically belongs in :file:`font_manager.py`, but placing it 

13# there would have created cyclical dependency problems. 

14 

15from functools import lru_cache 

16import re 

17import numpy as np 

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

19 ParseException, Suppress) 

20 

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

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

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

24 

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

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

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

28 

29 

30class FontconfigPatternParser: 

31 """ 

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

33 

34 .. _fontconfig patterns: 

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

36 """ 

37 

38 _constants = { 

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

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

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

42 'light': ('weight', 'light'), 

43 'book': ('weight', 'book'), 

44 'regular': ('weight', 'regular'), 

45 'normal': ('weight', 'normal'), 

46 'medium': ('weight', 'medium'), 

47 'demibold': ('weight', 'demibold'), 

48 'semibold': ('weight', 'semibold'), 

49 'bold': ('weight', 'bold'), 

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

51 'black': ('weight', 'black'), 

52 'heavy': ('weight', 'heavy'), 

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

54 'italic': ('slant', 'italic'), 

55 'oblique': ('slant', 'oblique'), 

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

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

58 'condensed': ('width', 'condensed'), 

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

60 'expanded': ('width', 'expanded'), 

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

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

63 } 

64 

65 def __init__(self): 

66 

67 family = Regex( 

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

69 ).setParseAction(self._family) 

70 

71 size = Regex( 

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

73 ).setParseAction(self._size) 

74 

75 name = Regex( 

76 r'[a-z]+' 

77 ).setParseAction(self._name) 

78 

79 value = Regex( 

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

81 ).setParseAction(self._value) 

82 

83 families = ( 

84 family 

85 + ZeroOrMore( 

86 Literal(',') 

87 + family) 

88 ).setParseAction(self._families) 

89 

90 point_sizes = ( 

91 size 

92 + ZeroOrMore( 

93 Literal(',') 

94 + size) 

95 ).setParseAction(self._point_sizes) 

96 

97 property = ( 

98 (name 

99 + Suppress(Literal('=')) 

100 + value 

101 + ZeroOrMore( 

102 Suppress(Literal(',')) 

103 + value)) 

104 | name 

105 ).setParseAction(self._property) 

106 

107 pattern = ( 

108 Optional( 

109 families) 

110 + Optional( 

111 Literal('-') 

112 + point_sizes) 

113 + ZeroOrMore( 

114 Literal(':') 

115 + property) 

116 + StringEnd() 

117 ) 

118 

119 self._parser = pattern 

120 self.ParseException = ParseException 

121 

122 def parse(self, pattern): 

123 """ 

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

125 of key/value pairs useful for initializing a 

126 :class:`font_manager.FontProperties` object. 

127 """ 

128 props = self._properties = {} 

129 try: 

130 self._parser.parseString(pattern) 

131 except self.ParseException as e: 

132 raise ValueError( 

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

134 

135 self._properties = None 

136 

137 self._parser.resetCache() 

138 

139 return props 

140 

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

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

143 

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

145 return [float(tokens[0])] 

146 

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

148 return [str(tokens[0])] 

149 

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

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

152 

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

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

155 return [] 

156 

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

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

159 return [] 

160 

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

162 if len(tokens) == 1: 

163 if tokens[0] in self._constants: 

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

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

166 else: 

167 key = tokens[0] 

168 val = tokens[1:] 

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

170 return [] 

171 

172 

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

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

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

176# during the test suite. 

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

178 

179 

180def _escape_val(val, escape_func): 

181 """ 

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

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

184 strings. The result is returned as a string. 

185 """ 

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

187 val = [val] 

188 

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

190 if x is not None) 

191 

192 

193def generate_fontconfig_pattern(d): 

194 """ 

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

196 pattern string. 

197 """ 

198 props = [] 

199 

200 # Family is added first w/o a keyword 

201 family = d.get_family() 

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

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

204 

205 # The other keys are added as key=value 

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

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

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

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

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

211 

212 return ''.join(props)