Hide keyboard shortcuts

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 

5 

6import numpy as np 

7 

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 

14 

15_log = logging.getLogger(__name__) 

16 

17 

18class TextToPath: 

19 """A class that converts strings to paths.""" 

20 

21 FONT_SCALE = 100. 

22 DPI = 72 

23 

24 def __init__(self): 

25 self.mathtext_parser = MathTextParser('path') 

26 self._texmanager = None 

27 

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 

36 

37 def _get_hinting_flag(self): 

38 return LOAD_NO_HINTING 

39 

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)) 

45 

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 

53 

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 

63 

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 

71 

72 fontsize = prop.get_size_in_points() 

73 scale = fontsize / self.FONT_SCALE 

74 

75 if ismath: 

76 prop = prop.copy() 

77 prop.set_size(self.FONT_SCALE) 

78 

79 width, height, descent, trash, used_characters = \ 

80 self.mathtext_parser.parse(s, 72, prop) 

81 return width * scale, height * scale, descent * scale 

82 

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 

91 

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). 

97 

98 Parameters 

99 ---------- 

100 prop : `matplotlib.font_manager.FontProperties` instance 

101 The font properties for the text. 

102 

103 s : str 

104 The text to be converted. 

105 

106 ismath : {False, True, "TeX"} 

107 If True, use mathtext parser. If "TeX", use tex for renderering. 

108 

109 usetex : bool, optional 

110 If set, forces *ismath* to True. This parameter is deprecated. 

111 

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. 

117 

118 Examples 

119 -------- 

120 Create a list of vertices and codes from a text, and create a `Path` 

121 from those:: 

122 

123 from matplotlib.path import Path 

124 from matplotlib.textpath import TextToPath 

125 from matplotlib.font_manager import FontProperties 

126 

127 fp = FontProperties(family="Humor Sans", style="italic") 

128 verts, codes = TextToPath().get_text_path(fp, "ABC") 

129 path = Path(verts, codes, closed=False) 

130 

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) 

142 

143 verts, codes = [], [] 

144 

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) 

151 

152 for verts1, codes1 in rects: 

153 verts.extend(verts1) 

154 codes.extend(codes1) 

155 

156 return verts, codes 

157 

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 """ 

163 

164 if glyph_map is None: 

165 glyph_map = OrderedDict() 

166 

167 if return_new_glyphs_only: 

168 glyph_map_new = OrderedDict() 

169 else: 

170 glyph_map_new = glyph_map 

171 

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() 

180 

181 ypositions = [0] * len(xpositions) 

182 sizes = [1.] * len(xpositions) 

183 

184 rects = [] 

185 

186 return (list(zip(glyph_ids, xpositions, ypositions, sizes)), 

187 glyph_map_new, rects) 

188 

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 """ 

194 

195 prop = prop.copy() 

196 prop.set_size(self.FONT_SCALE) 

197 

198 width, height, descent, glyphs, rects = self.mathtext_parser.parse( 

199 s, self.DPI, prop) 

200 

201 if not glyph_map: 

202 glyph_map = OrderedDict() 

203 

204 if return_new_glyphs_only: 

205 glyph_map_new = OrderedDict() 

206 else: 

207 glyph_map_new = glyph_map 

208 

209 xpositions = [] 

210 ypositions = [] 

211 glyph_ids = [] 

212 sizes = [] 

213 

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() 

221 

222 xpositions.append(ox) 

223 ypositions.append(oy) 

224 glyph_ids.append(char_id) 

225 size = fontsize / self.FONT_SCALE 

226 sizes.append(size) 

227 

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)) 

236 

237 return (list(zip(glyph_ids, xpositions, ypositions, sizes)), 

238 glyph_map_new, myrects) 

239 

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 

246 

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. 

251 

252 dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE) 

253 with dviread.Dvi(dvifile, self.DPI) as dvi: 

254 page, = dvi 

255 

256 if glyph_map is None: 

257 glyph_map = OrderedDict() 

258 

259 if return_new_glyphs_only: 

260 glyph_map_new = OrderedDict() 

261 else: 

262 glyph_map_new = glyph_map 

263 

264 glyph_ids, xpositions, ypositions, sizes = [], [], [], [] 

265 

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) 

271 

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() 

282 

283 glyph_ids.append(char_id) 

284 xpositions.append(x1) 

285 ypositions.append(y1) 

286 sizes.append(dvifont.size / self.FONT_SCALE) 

287 

288 myrects = [] 

289 

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)) 

297 

298 return (list(zip(glyph_ids, xpositions, ypositions, sizes)), 

299 glyph_map_new, myrects) 

300 

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.") 

310 

311 font = get_font(font_bunch.filename) 

312 

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 

342 

343 return font, enc 

344 

345 

346text_to_path = TextToPath() 

347 

348 

349class TextPath(Path): 

350 """ 

351 Create a path from the text. 

352 """ 

353 

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. 

361 

362 Parameters 

363 ---------- 

364 xy : tuple or array of two float values 

365 Position of the text. For no offset, use ``xy=(0, 0)``. 

366 

367 s : str 

368 The text to convert to a path. 

369 

370 size : float, optional 

371 Font size in points. Defaults to the size specified via the font 

372 properties *prop*. 

373 

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>`. 

378 

379 _interpolation_steps : integer, optional 

380 (Currently ignored) 

381 

382 usetex : bool, optional 

383 Whether to use tex rendering. Defaults to ``False``. 

384 

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:: 

389 

390 from matplotlib.textpath import TextPath 

391 from matplotlib.font_manager import FontProperties 

392 

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) 

396 

397 Also see :doc:`/gallery/text_labels_and_annotations/demo_text_path`. 

398 """ 

399 # Circular import. 

400 from matplotlib.text import Text 

401 

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.") 

406 

407 if prop is None: 

408 prop = FontProperties() 

409 if size is None: 

410 size = prop.get_size_in_points() 

411 

412 self._xy = xy 

413 self.set_size(size) 

414 

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 

422 

423 def set_size(self, size): 

424 """Set the text size.""" 

425 self._size = size 

426 self._invalid = True 

427 

428 def get_size(self): 

429 """Get the text size.""" 

430 return self._size 

431 

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 

439 

440 @property 

441 def codes(self): 

442 """ 

443 Return the codes 

444 """ 

445 return self._codes 

446 

447 def _revalidate_path(self): 

448 """ 

449 Update the path if necessary. 

450 

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 

460 

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 

467 

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) 

472 

473 if rcParams['text.usetex']: 

474 return s, 'TeX' 

475 

476 if even_dollars: 

477 return s, True 

478 else: 

479 return s.replace(r'\$', '$'), False 

480 

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)