Coverage for /usr/lib/python3/dist-packages/matplotlib/mathtext.py: 55%
114 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1r"""
2A module for parsing a subset of the TeX math syntax and rendering it to a
3Matplotlib backend.
5For a tutorial of its usage, see :doc:`/tutorials/text/mathtext`. This
6document is primarily concerned with implementation details.
8The module uses pyparsing_ to parse the TeX expression.
10.. _pyparsing: https://pypi.org/project/pyparsing/
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"""
18from collections import namedtuple
19import functools
20import logging
22import numpy as np
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)
31_log = logging.getLogger(__name__)
34get_unicode_index.__module__ = __name__
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.
44 Subclasses need to override the following:
46 - :meth:`render_glyph`
47 - :meth:`render_rect_filled`
48 - :meth:`get_results`
50 And optionally, if you need to use a FreeType hinting style:
52 - :meth:`get_hinting_type`
53 """
54 def __init__(self):
55 self.width = 0
56 self.height = 0
57 self.depth = 0
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
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()
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()
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()
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
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__()
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)]
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)))
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'])
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)
141 def get_results(self, box):
142 self.image = None
143 self.mode = 'render'
144 return _mathtext.ship(box).to_raster()
146 def get_hinting_type(self):
147 from matplotlib.backends import backend_agg
148 return backend_agg.get_hinting_flag()
151@_api.deprecated("3.6")
152class MathtextBackendPath(MathtextBackend):
153 """
154 Store information to write a mathtext rendering to the text path
155 machinery.
156 """
158 _Result = namedtuple("_Result", "width height depth glyphs rects")
160 def __init__(self):
161 super().__init__()
162 self.glyphs = []
163 self.rects = []
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))
169 def render_rect_filled(self, x1, y1, x2, y2):
170 self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1))
172 def get_results(self, box):
173 return _mathtext.ship(box).to_vector()
176@_api.deprecated("3.6")
177class MathTextWarning(Warning):
178 pass
181##############################################################################
182# MAIN
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 }
196 def __init__(self, output):
197 """
198 Create a MathTextParser for the given backend *output*.
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())
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.
216 The results are cached, so multiple calls to `parse`
217 with the same expression should be fast.
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)
228 @functools.lru_cache(50)
229 def _parse_cached(self, s, dpi, prop):
230 from matplotlib.backends import backend_agg
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)
242 fontsize = prop.get_size_in_points()
244 if self._parser is None: # Cache the parser globally.
245 self.__class__._parser = _mathtext.Parser()
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()
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.
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
280 parser = MathTextParser('path')
281 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
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)
287 return depth