Coverage for /usr/lib/python3/dist-packages/matplotlib/mathtext.py: 41%

114 statements  

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

1r""" 

2A module for parsing a subset of the TeX math syntax and rendering it to a 

3Matplotlib backend. 

4 

5For a tutorial of its usage, see :doc:`/tutorials/text/mathtext`. This 

6document is primarily concerned with implementation details. 

7 

8The module uses pyparsing_ to parse the TeX expression. 

9 

10.. _pyparsing: https://pypi.org/project/pyparsing/ 

11 

12The Bakoma distribution of the TeX Computer Modern fonts, and STIX 

13fonts are supported. There is experimental support for using 

14arbitrary fonts, but results may vary without proper tweaking and 

15metrics for those fonts. 

16""" 

17 

18from collections import namedtuple 

19import functools 

20import logging 

21 

22import numpy as np 

23 

24import matplotlib as mpl 

25from matplotlib import _api, _mathtext 

26from matplotlib.ft2font import FT2Image, LOAD_NO_HINTING 

27from matplotlib.font_manager import FontProperties 

28from ._mathtext import ( # noqa: reexported API 

29 RasterParse, VectorParse, get_unicode_index) 

30 

31_log = logging.getLogger(__name__) 

32 

33 

34get_unicode_index.__module__ = __name__ 

35 

36 

37@_api.deprecated("3.6") 

38class MathtextBackend: 

39 """ 

40 The base class for the mathtext backend-specific code. `MathtextBackend` 

41 subclasses interface between mathtext and specific Matplotlib graphics 

42 backends. 

43 

44 Subclasses need to override the following: 

45 

46 - :meth:`render_glyph` 

47 - :meth:`render_rect_filled` 

48 - :meth:`get_results` 

49 

50 And optionally, if you need to use a FreeType hinting style: 

51 

52 - :meth:`get_hinting_type` 

53 """ 

54 def __init__(self): 

55 self.width = 0 

56 self.height = 0 

57 self.depth = 0 

58 

59 def set_canvas_size(self, w, h, d): 

60 """Set the dimension of the drawing canvas.""" 

61 self.width = w 

62 self.height = h 

63 self.depth = d 

64 

65 def render_glyph(self, ox, oy, info): 

66 """ 

67 Draw a glyph described by *info* to the reference point (*ox*, 

68 *oy*). 

69 """ 

70 raise NotImplementedError() 

71 

72 def render_rect_filled(self, x1, y1, x2, y2): 

73 """ 

74 Draw a filled black rectangle from (*x1*, *y1*) to (*x2*, *y2*). 

75 """ 

76 raise NotImplementedError() 

77 

78 def get_results(self, box): 

79 """ 

80 Return a backend-specific tuple to return to the backend after 

81 all processing is done. 

82 """ 

83 raise NotImplementedError() 

84 

85 def get_hinting_type(self): 

86 """ 

87 Get the FreeType hinting type to use with this particular 

88 backend. 

89 """ 

90 return LOAD_NO_HINTING 

91 

92 

93@_api.deprecated("3.6") 

94class MathtextBackendAgg(MathtextBackend): 

95 """ 

96 Render glyphs and rectangles to an FTImage buffer, which is later 

97 transferred to the Agg image by the Agg backend. 

98 """ 

99 def __init__(self): 

100 self.ox = 0 

101 self.oy = 0 

102 self.image = None 

103 self.mode = 'bbox' 

104 self.bbox = [0, 0, 0, 0] 

105 super().__init__() 

106 

107 def _update_bbox(self, x1, y1, x2, y2): 

108 self.bbox = [min(self.bbox[0], x1), 

109 min(self.bbox[1], y1), 

110 max(self.bbox[2], x2), 

111 max(self.bbox[3], y2)] 

112 

113 def set_canvas_size(self, w, h, d): 

114 super().set_canvas_size(w, h, d) 

115 if self.mode != 'bbox': 

116 self.image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0))) 

117 

118 def render_glyph(self, ox, oy, info): 

119 if self.mode == 'bbox': 

120 self._update_bbox(ox + info.metrics.xmin, 

121 oy - info.metrics.ymax, 

122 ox + info.metrics.xmax, 

123 oy - info.metrics.ymin) 

124 else: 

125 info.font.draw_glyph_to_bitmap( 

126 self.image, ox, oy - info.metrics.iceberg, info.glyph, 

127 antialiased=mpl.rcParams['text.antialiased']) 

128 

129 def render_rect_filled(self, x1, y1, x2, y2): 

130 if self.mode == 'bbox': 

131 self._update_bbox(x1, y1, x2, y2) 

132 else: 

133 height = max(int(y2 - y1) - 1, 0) 

134 if height == 0: 

135 center = (y2 + y1) / 2.0 

136 y = int(center - (height + 1) / 2.0) 

137 else: 

138 y = int(y1) 

139 self.image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height) 

140 

141 def get_results(self, box): 

142 self.image = None 

143 self.mode = 'render' 

144 return _mathtext.ship(box).to_raster() 

145 

146 def get_hinting_type(self): 

147 from matplotlib.backends import backend_agg 

148 return backend_agg.get_hinting_flag() 

149 

150 

151@_api.deprecated("3.6") 

152class MathtextBackendPath(MathtextBackend): 

153 """ 

154 Store information to write a mathtext rendering to the text path 

155 machinery. 

156 """ 

157 

158 _Result = namedtuple("_Result", "width height depth glyphs rects") 

159 

160 def __init__(self): 

161 super().__init__() 

162 self.glyphs = [] 

163 self.rects = [] 

164 

165 def render_glyph(self, ox, oy, info): 

166 oy = self.height - oy + info.offset 

167 self.glyphs.append((info.font, info.fontsize, info.num, ox, oy)) 

168 

169 def render_rect_filled(self, x1, y1, x2, y2): 

170 self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1)) 

171 

172 def get_results(self, box): 

173 return _mathtext.ship(box).to_vector() 

174 

175 

176@_api.deprecated("3.6") 

177class MathTextWarning(Warning): 

178 pass 

179 

180 

181############################################################################## 

182# MAIN 

183 

184 

185class MathTextParser: 

186 _parser = None 

187 _font_type_mapping = { 

188 'cm': _mathtext.BakomaFonts, 

189 'dejavuserif': _mathtext.DejaVuSerifFonts, 

190 'dejavusans': _mathtext.DejaVuSansFonts, 

191 'stix': _mathtext.StixFonts, 

192 'stixsans': _mathtext.StixSansFonts, 

193 'custom': _mathtext.UnicodeFonts, 

194 } 

195 

196 def __init__(self, output): 

197 """ 

198 Create a MathTextParser for the given backend *output*. 

199 

200 Parameters 

201 ---------- 

202 output : {"path", "agg"} 

203 Whether to return a `VectorParse` ("path") or a 

204 `RasterParse` ("agg", or its synonym "macosx"). 

205 """ 

206 self._output_type = _api.check_getitem( 

207 {"path": "vector", "agg": "raster", "macosx": "raster"}, 

208 output=output.lower()) 

209 

210 def parse(self, s, dpi=72, prop=None): 

211 """ 

212 Parse the given math expression *s* at the given *dpi*. If *prop* is 

213 provided, it is a `.FontProperties` object specifying the "default" 

214 font to use in the math expression, used for all non-math text. 

215 

216 The results are cached, so multiple calls to `parse` 

217 with the same expression should be fast. 

218 

219 Depending on the *output* type, this returns either a `VectorParse` or 

220 a `RasterParse`. 

221 """ 

222 # lru_cache can't decorate parse() directly because prop 

223 # is mutable; key the cache using an internal copy (see 

224 # text._get_text_metrics_with_cache for a similar case). 

225 prop = prop.copy() if prop is not None else None 

226 return self._parse_cached(s, dpi, prop) 

227 

228 @functools.lru_cache(50) 

229 def _parse_cached(self, s, dpi, prop): 

230 from matplotlib.backends import backend_agg 

231 

232 if prop is None: 

233 prop = FontProperties() 

234 fontset_class = _api.check_getitem( 

235 self._font_type_mapping, fontset=prop.get_math_fontfamily()) 

236 load_glyph_flags = { 

237 "vector": LOAD_NO_HINTING, 

238 "raster": backend_agg.get_hinting_flag(), 

239 }[self._output_type] 

240 fontset = fontset_class(prop, load_glyph_flags) 

241 

242 fontsize = prop.get_size_in_points() 

243 

244 if self._parser is None: # Cache the parser globally. 

245 self.__class__._parser = _mathtext.Parser() 

246 

247 box = self._parser.parse(s, fontset, fontsize, dpi) 

248 output = _mathtext.ship(box) 

249 if self._output_type == "vector": 

250 return output.to_vector() 

251 elif self._output_type == "raster": 

252 return output.to_raster() 

253 

254 

255def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None, 

256 *, color=None): 

257 """ 

258 Given a math expression, renders it in a closely-clipped bounding 

259 box to an image file. 

260 

261 Parameters 

262 ---------- 

263 s : str 

264 A math expression. The math portion must be enclosed in dollar signs. 

265 filename_or_obj : str or path-like or file-like 

266 Where to write the image data. 

267 prop : `.FontProperties`, optional 

268 The size and style of the text. 

269 dpi : float, optional 

270 The output dpi. If not set, the dpi is determined as for 

271 `.Figure.savefig`. 

272 format : str, optional 

273 The output format, e.g., 'svg', 'pdf', 'ps' or 'png'. If not set, the 

274 format is determined as for `.Figure.savefig`. 

275 color : str, optional 

276 Foreground color, defaults to :rc:`text.color`. 

277 """ 

278 from matplotlib import figure 

279 

280 parser = MathTextParser('path') 

281 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop) 

282 

283 fig = figure.Figure(figsize=(width / 72.0, height / 72.0)) 

284 fig.text(0, depth/height, s, fontproperties=prop, color=color) 

285 fig.savefig(filename_or_obj, dpi=dpi, format=format) 

286 

287 return depth