Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/textpath.py : 19%

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
1from collections import OrderedDict
2import functools
3import logging
4import urllib.parse
6import numpy as np
8from matplotlib import _text_layout, cbook, dviread, font_manager, rcParams
9from matplotlib.font_manager import FontProperties, get_font
10from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_TARGET_LIGHT
11from matplotlib.mathtext import MathTextParser
12from matplotlib.path import Path
13from matplotlib.transforms import Affine2D
15_log = logging.getLogger(__name__)
18class TextToPath:
19 """A class that converts strings to paths."""
21 FONT_SCALE = 100.
22 DPI = 72
24 def __init__(self):
25 self.mathtext_parser = MathTextParser('path')
26 self._texmanager = None
28 def _get_font(self, prop):
29 """
30 Find the `FT2Font` matching font properties *prop*, with its size set.
31 """
32 fname = font_manager.findfont(prop)
33 font = get_font(fname)
34 font.set_size(self.FONT_SCALE, self.DPI)
35 return font
37 def _get_hinting_flag(self):
38 return LOAD_NO_HINTING
40 def _get_char_id(self, font, ccode):
41 """
42 Return a unique id for the given font and character-code set.
43 """
44 return urllib.parse.quote('{}-{}'.format(font.postscript_name, ccode))
46 def _get_char_id_ps(self, font, ccode):
47 """
48 Return a unique id for the given font and character-code set (for tex).
49 """
50 ps_name = font.get_ps_font_info()[2]
51 char_id = urllib.parse.quote('%s-%d' % (ps_name, ccode))
52 return char_id
54 @cbook.deprecated(
55 "3.1",
56 alternative="font.get_path() and manual translation of the vertices")
57 def glyph_to_path(self, font, currx=0.):
58 """Convert the *font*'s current glyph to a (vertices, codes) pair."""
59 verts, codes = font.get_path()
60 if currx != 0.0:
61 verts[:, 0] += currx
62 return verts, codes
64 def get_text_width_height_descent(self, s, prop, ismath):
65 if rcParams['text.usetex']:
66 texmanager = self.get_texmanager()
67 fontsize = prop.get_size_in_points()
68 w, h, d = texmanager.get_text_width_height_descent(s, fontsize,
69 renderer=None)
70 return w, h, d
72 fontsize = prop.get_size_in_points()
73 scale = fontsize / self.FONT_SCALE
75 if ismath:
76 prop = prop.copy()
77 prop.set_size(self.FONT_SCALE)
79 width, height, descent, trash, used_characters = \
80 self.mathtext_parser.parse(s, 72, prop)
81 return width * scale, height * scale, descent * scale
83 font = self._get_font(prop)
84 font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
85 w, h = font.get_width_height()
86 w /= 64.0 # convert from subpixels
87 h /= 64.0
88 d = font.get_descent()
89 d /= 64.0
90 return w * scale, h * scale, d * scale
92 @cbook._delete_parameter("3.1", "usetex")
93 def get_text_path(self, prop, s, ismath=False, usetex=False):
94 """
95 Convert text *s* to path (a tuple of vertices and codes for
96 matplotlib.path.Path).
98 Parameters
99 ----------
100 prop : `matplotlib.font_manager.FontProperties` instance
101 The font properties for the text.
103 s : str
104 The text to be converted.
106 ismath : {False, True, "TeX"}
107 If True, use mathtext parser. If "TeX", use tex for renderering.
109 usetex : bool, optional
110 If set, forces *ismath* to True. This parameter is deprecated.
112 Returns
113 -------
114 verts, codes : tuple of lists
115 *verts* is a list of numpy arrays containing the x and y
116 coordinates of the vertices. *codes* is a list of path codes.
118 Examples
119 --------
120 Create a list of vertices and codes from a text, and create a `Path`
121 from those::
123 from matplotlib.path import Path
124 from matplotlib.textpath import TextToPath
125 from matplotlib.font_manager import FontProperties
127 fp = FontProperties(family="Humor Sans", style="italic")
128 verts, codes = TextToPath().get_text_path(fp, "ABC")
129 path = Path(verts, codes, closed=False)
131 Also see `TextPath` for a more direct way to create a path from a text.
132 """
133 if usetex:
134 ismath = "TeX"
135 if ismath == "TeX":
136 glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)
137 elif not ismath:
138 font = self._get_font(prop)
139 glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s)
140 else:
141 glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s)
143 verts, codes = [], []
145 for glyph_id, xposition, yposition, scale in glyph_info:
146 verts1, codes1 = glyph_map[glyph_id]
147 if len(verts1):
148 verts1 = np.array(verts1) * scale + [xposition, yposition]
149 verts.extend(verts1)
150 codes.extend(codes1)
152 for verts1, codes1 in rects:
153 verts.extend(verts1)
154 codes.extend(codes1)
156 return verts, codes
158 def get_glyphs_with_font(self, font, s, glyph_map=None,
159 return_new_glyphs_only=False):
160 """
161 Convert string *s* to vertices and codes using the provided ttf font.
162 """
164 if glyph_map is None:
165 glyph_map = OrderedDict()
167 if return_new_glyphs_only:
168 glyph_map_new = OrderedDict()
169 else:
170 glyph_map_new = glyph_map
172 xpositions = []
173 glyph_ids = []
174 for char, (_, x) in zip(s, _text_layout.layout(s, font)):
175 char_id = self._get_char_id(font, ord(char))
176 glyph_ids.append(char_id)
177 xpositions.append(x)
178 if char_id not in glyph_map:
179 glyph_map_new[char_id] = font.get_path()
181 ypositions = [0] * len(xpositions)
182 sizes = [1.] * len(xpositions)
184 rects = []
186 return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
187 glyph_map_new, rects)
189 def get_glyphs_mathtext(self, prop, s, glyph_map=None,
190 return_new_glyphs_only=False):
191 """
192 Parse mathtext string *s* and convert it to a (vertices, codes) pair.
193 """
195 prop = prop.copy()
196 prop.set_size(self.FONT_SCALE)
198 width, height, descent, glyphs, rects = self.mathtext_parser.parse(
199 s, self.DPI, prop)
201 if not glyph_map:
202 glyph_map = OrderedDict()
204 if return_new_glyphs_only:
205 glyph_map_new = OrderedDict()
206 else:
207 glyph_map_new = glyph_map
209 xpositions = []
210 ypositions = []
211 glyph_ids = []
212 sizes = []
214 for font, fontsize, ccode, ox, oy in glyphs:
215 char_id = self._get_char_id(font, ccode)
216 if char_id not in glyph_map:
217 font.clear()
218 font.set_size(self.FONT_SCALE, self.DPI)
219 font.load_char(ccode, flags=LOAD_NO_HINTING)
220 glyph_map_new[char_id] = font.get_path()
222 xpositions.append(ox)
223 ypositions.append(oy)
224 glyph_ids.append(char_id)
225 size = fontsize / self.FONT_SCALE
226 sizes.append(size)
228 myrects = []
229 for ox, oy, w, h in rects:
230 vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h),
231 (ox + w, oy), (ox, oy), (0, 0)]
232 code1 = [Path.MOVETO,
233 Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
234 Path.CLOSEPOLY]
235 myrects.append((vert1, code1))
237 return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
238 glyph_map_new, myrects)
240 def get_texmanager(self):
241 """Return the cached `~.texmanager.TexManager` instance."""
242 if self._texmanager is None:
243 from matplotlib.texmanager import TexManager
244 self._texmanager = TexManager()
245 return self._texmanager
247 def get_glyphs_tex(self, prop, s, glyph_map=None,
248 return_new_glyphs_only=False):
249 """Convert the string *s* to vertices and codes using usetex mode."""
250 # Mostly borrowed from pdf backend.
252 dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE)
253 with dviread.Dvi(dvifile, self.DPI) as dvi:
254 page, = dvi
256 if glyph_map is None:
257 glyph_map = OrderedDict()
259 if return_new_glyphs_only:
260 glyph_map_new = OrderedDict()
261 else:
262 glyph_map_new = glyph_map
264 glyph_ids, xpositions, ypositions, sizes = [], [], [], []
266 # Gather font information and do some setup for combining
267 # characters into strings.
268 for x1, y1, dvifont, glyph, width in page.text:
269 font, enc = self._get_ps_font_and_encoding(dvifont.texname)
270 char_id = self._get_char_id_ps(font, glyph)
272 if char_id not in glyph_map:
273 font.clear()
274 font.set_size(self.FONT_SCALE, self.DPI)
275 # See comments in _get_ps_font_and_encoding.
276 if enc is not None:
277 index = font.get_name_index(enc[glyph])
278 font.load_glyph(index, flags=LOAD_TARGET_LIGHT)
279 else:
280 font.load_char(glyph, flags=LOAD_TARGET_LIGHT)
281 glyph_map_new[char_id] = font.get_path()
283 glyph_ids.append(char_id)
284 xpositions.append(x1)
285 ypositions.append(y1)
286 sizes.append(dvifont.size / self.FONT_SCALE)
288 myrects = []
290 for ox, oy, h, w in page.boxes:
291 vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h),
292 (ox, oy + h), (ox, oy), (0, 0)]
293 code1 = [Path.MOVETO,
294 Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
295 Path.CLOSEPOLY]
296 myrects.append((vert1, code1))
298 return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
299 glyph_map_new, myrects)
301 @staticmethod
302 @functools.lru_cache(50)
303 def _get_ps_font_and_encoding(texname):
304 tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
305 font_bunch = tex_font_map[texname]
306 if font_bunch.filename is None:
307 raise ValueError(
308 f"No usable font file found for {font_bunch.psname} "
309 f"({texname}). The font may lack a Type-1 version.")
311 font = get_font(font_bunch.filename)
313 if font_bunch.encoding:
314 # If psfonts.map specifies an encoding, use it: it gives us a
315 # mapping of glyph indices to Adobe glyph names; use it to convert
316 # dvi indices to glyph names and use the FreeType-synthesized
317 # unicode charmap to convert glyph names to glyph indices (with
318 # FT_Get_Name_Index/get_name_index), and load the glyph using
319 # FT_Load_Glyph/load_glyph. (That charmap has a coverage at least
320 # as good as, and possibly better than, the native charmaps.)
321 enc = dviread._parse_enc(font_bunch.encoding)
322 else:
323 # If psfonts.map specifies no encoding, the indices directly
324 # map to the font's "native" charmap; so don't use the
325 # FreeType-synthesized charmap but the native ones (we can't
326 # directly identify it but it's typically an Adobe charmap), and
327 # directly load the dvi glyph indices using FT_Load_Char/load_char.
328 for charmap_code in [
329 1094992451, # ADOBE_CUSTOM.
330 1094995778, # ADOBE_STANDARD.
331 ]:
332 try:
333 font.select_charmap(charmap_code)
334 except (ValueError, RuntimeError):
335 pass
336 else:
337 break
338 else:
339 _log.warning("No supported encoding in font (%s).",
340 font_bunch.filename)
341 enc = None
343 return font, enc
346text_to_path = TextToPath()
349class TextPath(Path):
350 """
351 Create a path from the text.
352 """
354 def __init__(self, xy, s, size=None, prop=None,
355 _interpolation_steps=1, usetex=False,
356 *args, **kwargs):
357 r"""
358 Create a path from the text. Note that it simply is a path,
359 not an artist. You need to use the `~.PathPatch` (or other artists)
360 to draw this path onto the canvas.
362 Parameters
363 ----------
364 xy : tuple or array of two float values
365 Position of the text. For no offset, use ``xy=(0, 0)``.
367 s : str
368 The text to convert to a path.
370 size : float, optional
371 Font size in points. Defaults to the size specified via the font
372 properties *prop*.
374 prop : `matplotlib.font_manager.FontProperties`, optional
375 Font property. If not provided, will use a default
376 ``FontProperties`` with parameters from the
377 :ref:`rcParams <matplotlib-rcparams>`.
379 _interpolation_steps : integer, optional
380 (Currently ignored)
382 usetex : bool, optional
383 Whether to use tex rendering. Defaults to ``False``.
385 Examples
386 --------
387 The following creates a path from the string "ABC" with Helvetica
388 font face; and another path from the latex fraction 1/2::
390 from matplotlib.textpath import TextPath
391 from matplotlib.font_manager import FontProperties
393 fp = FontProperties(family="Helvetica", style="italic")
394 path1 = TextPath((12,12), "ABC", size=12, prop=fp)
395 path2 = TextPath((0,0), r"$\frac{1}{2}$", size=12, usetex=True)
397 Also see :doc:`/gallery/text_labels_and_annotations/demo_text_path`.
398 """
399 # Circular import.
400 from matplotlib.text import Text
402 if args or kwargs:
403 cbook.warn_deprecated(
404 "3.1", message="Additional arguments to TextPath used to be "
405 "ignored, but will trigger a TypeError %(removal)s.")
407 if prop is None:
408 prop = FontProperties()
409 if size is None:
410 size = prop.get_size_in_points()
412 self._xy = xy
413 self.set_size(size)
415 self._cached_vertices = None
416 s, ismath = Text(usetex=usetex)._preprocess_math(s)
417 self._vertices, self._codes = text_to_path.get_text_path(
418 prop, s, ismath=ismath)
419 self._should_simplify = False
420 self._simplify_threshold = rcParams['path.simplify_threshold']
421 self._interpolation_steps = _interpolation_steps
423 def set_size(self, size):
424 """Set the text size."""
425 self._size = size
426 self._invalid = True
428 def get_size(self):
429 """Get the text size."""
430 return self._size
432 @property
433 def vertices(self):
434 """
435 Return the cached path after updating it if necessary.
436 """
437 self._revalidate_path()
438 return self._cached_vertices
440 @property
441 def codes(self):
442 """
443 Return the codes
444 """
445 return self._codes
447 def _revalidate_path(self):
448 """
449 Update the path if necessary.
451 The path for the text is initially create with the font size of
452 `~.FONT_SCALE`, and this path is rescaled to other size when necessary.
453 """
454 if self._invalid or self._cached_vertices is None:
455 tr = (Affine2D()
456 .scale(self._size / text_to_path.FONT_SCALE)
457 .translate(*self._xy))
458 self._cached_vertices = tr.transform(self._vertices)
459 self._invalid = False
461 @cbook.deprecated("3.1")
462 def is_math_text(self, s):
463 """
464 Returns True if the given string *s* contains any mathtext.
465 """
466 # copied from Text.is_math_text -JJL
468 # Did we find an even number of non-escaped dollar signs?
469 # If so, treat is as math text.
470 dollar_count = s.count(r'$') - s.count(r'\$')
471 even_dollars = (dollar_count > 0 and dollar_count % 2 == 0)
473 if rcParams['text.usetex']:
474 return s, 'TeX'
476 if even_dollars:
477 return s, True
478 else:
479 return s.replace(r'\$', '$'), False
481 @cbook.deprecated("3.1", alternative="TextPath")
482 def text_get_vertices_codes(self, prop, s, usetex):
483 """
484 Convert string *s* to a (vertices, codes) pair using font property
485 *prop*.
486 """
487 # Mostly copied from backend_svg.py.
488 if usetex:
489 return text_to_path.get_text_path(prop, s, usetex=True)
490 else:
491 clean_line, ismath = self.is_math_text(s)
492 return text_to_path.get_text_path(prop, clean_line, ismath=ismath)