|
| 1 | +""" |
| 2 | +A 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 | + |
| 12 | +fromfunctoolsimportlru_cache |
| 13 | +importre |
| 14 | +importnumpyasnp |
| 15 | +frompyparsingimport (Literal,ZeroOrMore,Optional,Regex,StringEnd, |
| 16 | +ParseException,Suppress) |
| 17 | + |
| 18 | +family_punc=r'\\\-:,' |
| 19 | +family_unescape=re.compile(r'\\([%s])'%family_punc).sub |
| 20 | +family_escape=re.compile(r'([%s])'%family_punc).sub |
| 21 | + |
| 22 | +value_punc=r'\\=_:,' |
| 23 | +value_unescape=re.compile(r'\\([%s])'%value_punc).sub |
| 24 | +value_escape=re.compile(r'([%s])'%value_punc).sub |
| 25 | + |
| 26 | + |
| 27 | +classFontconfigPatternParser: |
| 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 | +defparse(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 | +exceptself.ParseExceptionase: |
| 129 | +raiseValueError( |
| 130 | +"Could not parse font string: '%s'\n%s"% (pattern,e))frome |
| 131 | + |
| 132 | +self._properties=None |
| 133 | + |
| 134 | +self._parser.resetCache() |
| 135 | + |
| 136 | +returnprops |
| 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)forxintokens] |
| 152 | +return [] |
| 153 | + |
| 154 | +def_point_sizes(self,s,loc,tokens): |
| 155 | +self._properties['size']= [str(x)forxintokens] |
| 156 | +return [] |
| 157 | + |
| 158 | +def_property(self,s,loc,tokens): |
| 159 | +iflen(tokens)==1: |
| 160 | +iftokens[0]inself._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. |
| 174 | +parse_fontconfig_pattern=lru_cache()(FontconfigPatternParser().parse) |
| 175 | + |
| 176 | + |
| 177 | +def_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 | +ifnotnp.iterable(val)orisinstance(val,str): |
| 184 | +val= [val] |
| 185 | + |
| 186 | +return','.join(escape_func(r'\\\1',str(x))forxinval |
| 187 | +ifxisnotNone) |
| 188 | + |
| 189 | + |
| 190 | +defgenerate_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 | +iffamilyisnotNoneandfamily!= []: |
| 200 | +props.append(_escape_val(family,family_escape)) |
| 201 | + |
| 202 | +# The other keys are added as key=value |
| 203 | +forkeyin ['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 | +ifvalisnotNoneandval!= []: |
| 207 | +props.append(":%s=%s"% (key,_escape_val(val,value_escape))) |
| 208 | + |
| 209 | +return''.join(props) |