Coverage for /usr/lib/python3/dist-packages/matplotlib/textpath.py: 21%
192 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
1from collections import OrderedDict
2import logging
3import urllib.parse
5import numpy as np
7from matplotlib import _api, _text_helpers, dviread
8from matplotlib.font_manager import (
9 FontProperties, get_font, fontManager as _fontManager
10)
11from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_TARGET_LIGHT
12from matplotlib.mathtext import MathTextParser
13from matplotlib.path import Path
14from matplotlib.texmanager import TexManager
15from matplotlib.transforms import Affine2D
17_log = logging.getLogger(__name__)
20class TextToPath:
21 """A class that converts strings to paths."""
23 FONT_SCALE = 100.
24 DPI = 72
26 def __init__(self):
27 self.mathtext_parser = MathTextParser('path')
28 self._texmanager = None
30 def _get_font(self, prop):
31 """
32 Find the `FT2Font` matching font properties *prop*, with its size set.
33 """
34 filenames = _fontManager._find_fonts_by_props(prop)
35 font = get_font(filenames)
36 font.set_size(self.FONT_SCALE, self.DPI)
37 return font
39 def _get_hinting_flag(self):
40 return LOAD_NO_HINTING
42 def _get_char_id(self, font, ccode):
43 """
44 Return a unique id for the given font and character-code set.
45 """
46 return urllib.parse.quote(f"{font.postscript_name}-{ccode:x}")
48 def get_text_width_height_descent(self, s, prop, ismath):
49 fontsize = prop.get_size_in_points()
51 if ismath == "TeX":
52 return TexManager().get_text_width_height_descent(s, fontsize)
54 scale = fontsize / self.FONT_SCALE
56 if ismath:
57 prop = prop.copy()
58 prop.set_size(self.FONT_SCALE)
59 width, height, descent, *_ = \
60 self.mathtext_parser.parse(s, 72, prop)
61 return width * scale, height * scale, descent * scale
63 font = self._get_font(prop)
64 font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
65 w, h = font.get_width_height()
66 w /= 64.0 # convert from subpixels
67 h /= 64.0
68 d = font.get_descent()
69 d /= 64.0
70 return w * scale, h * scale, d * scale
72 def get_text_path(self, prop, s, ismath=False):
73 """
74 Convert text *s* to path (a tuple of vertices and codes for
75 matplotlib.path.Path).
77 Parameters
78 ----------
79 prop : `~matplotlib.font_manager.FontProperties`
80 The font properties for the text.
82 s : str
83 The text to be converted.
85 ismath : {False, True, "TeX"}
86 If True, use mathtext parser. If "TeX", use tex for rendering.
88 Returns
89 -------
90 verts : list
91 A list of numpy arrays containing the x and y coordinates of the
92 vertices.
94 codes : list
95 A list of path codes.
97 Examples
98 --------
99 Create a list of vertices and codes from a text, and create a `.Path`
100 from those::
102 from matplotlib.path import Path
103 from matplotlib.textpath import TextToPath
104 from matplotlib.font_manager import FontProperties
106 fp = FontProperties(family="Humor Sans", style="italic")
107 verts, codes = TextToPath().get_text_path(fp, "ABC")
108 path = Path(verts, codes, closed=False)
110 Also see `TextPath` for a more direct way to create a path from a text.
111 """
112 if ismath == "TeX":
113 glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)
114 elif not ismath:
115 font = self._get_font(prop)
116 glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s)
117 else:
118 glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s)
120 verts, codes = [], []
121 for glyph_id, xposition, yposition, scale in glyph_info:
122 verts1, codes1 = glyph_map[glyph_id]
123 verts.extend(verts1 * scale + [xposition, yposition])
124 codes.extend(codes1)
125 for verts1, codes1 in rects:
126 verts.extend(verts1)
127 codes.extend(codes1)
129 # Make sure an empty string or one with nothing to print
130 # (e.g. only spaces & newlines) will be valid/empty path
131 if not verts:
132 verts = np.empty((0, 2))
134 return verts, codes
136 def get_glyphs_with_font(self, font, s, glyph_map=None,
137 return_new_glyphs_only=False):
138 """
139 Convert string *s* to vertices and codes using the provided ttf font.
140 """
142 if glyph_map is None:
143 glyph_map = OrderedDict()
145 if return_new_glyphs_only:
146 glyph_map_new = OrderedDict()
147 else:
148 glyph_map_new = glyph_map
150 xpositions = []
151 glyph_ids = []
152 for item in _text_helpers.layout(s, font):
153 char_id = self._get_char_id(item.ft_object, ord(item.char))
154 glyph_ids.append(char_id)
155 xpositions.append(item.x)
156 if char_id not in glyph_map:
157 glyph_map_new[char_id] = item.ft_object.get_path()
159 ypositions = [0] * len(xpositions)
160 sizes = [1.] * len(xpositions)
162 rects = []
164 return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
165 glyph_map_new, rects)
167 def get_glyphs_mathtext(self, prop, s, glyph_map=None,
168 return_new_glyphs_only=False):
169 """
170 Parse mathtext string *s* and convert it to a (vertices, codes) pair.
171 """
173 prop = prop.copy()
174 prop.set_size(self.FONT_SCALE)
176 width, height, descent, glyphs, rects = self.mathtext_parser.parse(
177 s, self.DPI, prop)
179 if not glyph_map:
180 glyph_map = OrderedDict()
182 if return_new_glyphs_only:
183 glyph_map_new = OrderedDict()
184 else:
185 glyph_map_new = glyph_map
187 xpositions = []
188 ypositions = []
189 glyph_ids = []
190 sizes = []
192 for font, fontsize, ccode, ox, oy in glyphs:
193 char_id = self._get_char_id(font, ccode)
194 if char_id not in glyph_map:
195 font.clear()
196 font.set_size(self.FONT_SCALE, self.DPI)
197 font.load_char(ccode, flags=LOAD_NO_HINTING)
198 glyph_map_new[char_id] = font.get_path()
200 xpositions.append(ox)
201 ypositions.append(oy)
202 glyph_ids.append(char_id)
203 size = fontsize / self.FONT_SCALE
204 sizes.append(size)
206 myrects = []
207 for ox, oy, w, h in rects:
208 vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h),
209 (ox + w, oy), (ox, oy), (0, 0)]
210 code1 = [Path.MOVETO,
211 Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
212 Path.CLOSEPOLY]
213 myrects.append((vert1, code1))
215 return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
216 glyph_map_new, myrects)
218 @_api.deprecated("3.6", alternative="TexManager()")
219 def get_texmanager(self):
220 """Return the cached `~.texmanager.TexManager` instance."""
221 if self._texmanager is None:
222 self._texmanager = TexManager()
223 return self._texmanager
225 def get_glyphs_tex(self, prop, s, glyph_map=None,
226 return_new_glyphs_only=False):
227 """Convert the string *s* to vertices and codes using usetex mode."""
228 # Mostly borrowed from pdf backend.
230 dvifile = TexManager().make_dvi(s, self.FONT_SCALE)
231 with dviread.Dvi(dvifile, self.DPI) as dvi:
232 page, = dvi
234 if glyph_map is None:
235 glyph_map = OrderedDict()
237 if return_new_glyphs_only:
238 glyph_map_new = OrderedDict()
239 else:
240 glyph_map_new = glyph_map
242 glyph_ids, xpositions, ypositions, sizes = [], [], [], []
244 # Gather font information and do some setup for combining
245 # characters into strings.
246 for text in page.text:
247 font = get_font(text.font_path)
248 char_id = self._get_char_id(font, text.glyph)
249 if char_id not in glyph_map:
250 font.clear()
251 font.set_size(self.FONT_SCALE, self.DPI)
252 glyph_name_or_index = text.glyph_name_or_index
253 if isinstance(glyph_name_or_index, str):
254 index = font.get_name_index(glyph_name_or_index)
255 font.load_glyph(index, flags=LOAD_TARGET_LIGHT)
256 elif isinstance(glyph_name_or_index, int):
257 self._select_native_charmap(font)
258 font.load_char(
259 glyph_name_or_index, flags=LOAD_TARGET_LIGHT)
260 else: # Should not occur.
261 raise TypeError(f"Glyph spec of unexpected type: "
262 f"{glyph_name_or_index!r}")
263 glyph_map_new[char_id] = font.get_path()
265 glyph_ids.append(char_id)
266 xpositions.append(text.x)
267 ypositions.append(text.y)
268 sizes.append(text.font_size / self.FONT_SCALE)
270 myrects = []
272 for ox, oy, h, w in page.boxes:
273 vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h),
274 (ox, oy + h), (ox, oy), (0, 0)]
275 code1 = [Path.MOVETO,
276 Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
277 Path.CLOSEPOLY]
278 myrects.append((vert1, code1))
280 return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
281 glyph_map_new, myrects)
283 @staticmethod
284 def _select_native_charmap(font):
285 # Select the native charmap. (we can't directly identify it but it's
286 # typically an Adobe charmap).
287 for charmap_code in [
288 1094992451, # ADOBE_CUSTOM.
289 1094995778, # ADOBE_STANDARD.
290 ]:
291 try:
292 font.select_charmap(charmap_code)
293 except (ValueError, RuntimeError):
294 pass
295 else:
296 break
297 else:
298 _log.warning("No supported encoding in font (%s).", font.fname)
301text_to_path = TextToPath()
304class TextPath(Path):
305 """
306 Create a path from the text.
307 """
309 def __init__(self, xy, s, size=None, prop=None,
310 _interpolation_steps=1, usetex=False):
311 r"""
312 Create a path from the text. Note that it simply is a path,
313 not an artist. You need to use the `.PathPatch` (or other artists)
314 to draw this path onto the canvas.
316 Parameters
317 ----------
318 xy : tuple or array of two float values
319 Position of the text. For no offset, use ``xy=(0, 0)``.
321 s : str
322 The text to convert to a path.
324 size : float, optional
325 Font size in points. Defaults to the size specified via the font
326 properties *prop*.
328 prop : `matplotlib.font_manager.FontProperties`, optional
329 Font property. If not provided, will use a default
330 ``FontProperties`` with parameters from the
331 :ref:`rcParams<customizing-with-dynamic-rc-settings>`.
333 _interpolation_steps : int, optional
334 (Currently ignored)
336 usetex : bool, default: False
337 Whether to use tex rendering.
339 Examples
340 --------
341 The following creates a path from the string "ABC" with Helvetica
342 font face; and another path from the latex fraction 1/2::
344 from matplotlib.textpath import TextPath
345 from matplotlib.font_manager import FontProperties
347 fp = FontProperties(family="Helvetica", style="italic")
348 path1 = TextPath((12, 12), "ABC", size=12, prop=fp)
349 path2 = TextPath((0, 0), r"$\frac{1}{2}$", size=12, usetex=True)
351 Also see :doc:`/gallery/text_labels_and_annotations/demo_text_path`.
352 """
353 # Circular import.
354 from matplotlib.text import Text
356 prop = FontProperties._from_any(prop)
357 if size is None:
358 size = prop.get_size_in_points()
360 self._xy = xy
361 self.set_size(size)
363 self._cached_vertices = None
364 s, ismath = Text(usetex=usetex)._preprocess_math(s)
365 super().__init__(
366 *text_to_path.get_text_path(prop, s, ismath=ismath),
367 _interpolation_steps=_interpolation_steps,
368 readonly=True)
369 self._should_simplify = False
371 def set_size(self, size):
372 """Set the text size."""
373 self._size = size
374 self._invalid = True
376 def get_size(self):
377 """Get the text size."""
378 return self._size
380 @property
381 def vertices(self):
382 """
383 Return the cached path after updating it if necessary.
384 """
385 self._revalidate_path()
386 return self._cached_vertices
388 @property
389 def codes(self):
390 """
391 Return the codes
392 """
393 return self._codes
395 def _revalidate_path(self):
396 """
397 Update the path if necessary.
399 The path for the text is initially create with the font size of
400 `.FONT_SCALE`, and this path is rescaled to other size when necessary.
401 """
402 if self._invalid or self._cached_vertices is None:
403 tr = (Affine2D()
404 .scale(self._size / text_to_path.FONT_SCALE)
405 .translate(*self._xy))
406 self._cached_vertices = tr.transform(self._vertices)
407 self._cached_vertices.flags.writeable = False
408 self._invalid = False