You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

563 lines
18 KiB

4 years ago
  1. from collections import OrderedDict
  2. import functools
  3. import urllib.parse
  4. import warnings
  5. import numpy as np
  6. from matplotlib import cbook, dviread, font_manager, rcParams
  7. from matplotlib.font_manager import FontProperties, get_font
  8. from matplotlib.ft2font import (
  9. KERNING_DEFAULT, LOAD_NO_HINTING, LOAD_TARGET_LIGHT)
  10. from matplotlib.mathtext import MathTextParser
  11. from matplotlib.path import Path
  12. from matplotlib.transforms import Affine2D
  13. @functools.lru_cache(1)
  14. def _get_adobe_standard_encoding():
  15. enc_name = dviread.find_tex_file('8a.enc')
  16. enc = dviread.Encoding(enc_name)
  17. return {c: i for i, c in enumerate(enc.encoding)}
  18. class TextToPath(object):
  19. """
  20. A class that convert a given text to a path using ttf fonts.
  21. """
  22. FONT_SCALE = 100.
  23. DPI = 72
  24. def __init__(self):
  25. self.mathtext_parser = MathTextParser('path')
  26. self._texmanager = None
  27. @property
  28. @cbook.deprecated("3.0")
  29. def tex_font_map(self):
  30. return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
  31. def _get_font(self, prop):
  32. """
  33. find a ttf font.
  34. """
  35. fname = font_manager.findfont(prop)
  36. font = get_font(fname)
  37. font.set_size(self.FONT_SCALE, self.DPI)
  38. return font
  39. def _get_hinting_flag(self):
  40. return LOAD_NO_HINTING
  41. def _get_char_id(self, font, ccode):
  42. """
  43. Return a unique id for the given font and character-code set.
  44. """
  45. 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
  53. def glyph_to_path(self, font, currx=0.):
  54. """
  55. convert the ft2font glyph to vertices and codes.
  56. """
  57. verts, codes = font.get_path()
  58. if currx != 0.0:
  59. verts[:, 0] += currx
  60. return verts, codes
  61. def get_text_width_height_descent(self, s, prop, ismath):
  62. if rcParams['text.usetex']:
  63. texmanager = self.get_texmanager()
  64. fontsize = prop.get_size_in_points()
  65. w, h, d = texmanager.get_text_width_height_descent(s, fontsize,
  66. renderer=None)
  67. return w, h, d
  68. fontsize = prop.get_size_in_points()
  69. scale = fontsize / self.FONT_SCALE
  70. if ismath:
  71. prop = prop.copy()
  72. prop.set_size(self.FONT_SCALE)
  73. width, height, descent, trash, used_characters = \
  74. self.mathtext_parser.parse(s, 72, prop)
  75. return width * scale, height * scale, descent * scale
  76. font = self._get_font(prop)
  77. font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
  78. w, h = font.get_width_height()
  79. w /= 64.0 # convert from subpixels
  80. h /= 64.0
  81. d = font.get_descent()
  82. d /= 64.0
  83. return w * scale, h * scale, d * scale
  84. def get_text_path(self, prop, s, ismath=False, usetex=False):
  85. """
  86. Convert text *s* to path (a tuple of vertices and codes for
  87. matplotlib.path.Path).
  88. Parameters
  89. ----------
  90. prop : `matplotlib.font_manager.FontProperties` instance
  91. The font properties for the text.
  92. s : str
  93. The text to be converted.
  94. usetex : bool, optional
  95. Whether to use tex rendering. Defaults to ``False``.
  96. ismath : bool, optional
  97. If True, use mathtext parser. Effective only if
  98. ``usetex == False``.
  99. Returns
  100. -------
  101. verts, codes : tuple of lists
  102. *verts* is a list of numpy arrays containing the x and y
  103. coordinates of the vertices. *codes* is a list of path codes.
  104. Examples
  105. --------
  106. Create a list of vertices and codes from a text, and create a `Path`
  107. from those::
  108. from matplotlib.path import Path
  109. from matplotlib.textpath import TextToPath
  110. from matplotlib.font_manager import FontProperties
  111. fp = FontProperties(family="Humor Sans", style="italic")
  112. verts, codes = TextToPath().get_text_path(fp, "ABC")
  113. path = Path(verts, codes, closed=False)
  114. Also see `TextPath` for a more direct way to create a path from a text.
  115. """
  116. if not usetex:
  117. if not ismath:
  118. font = self._get_font(prop)
  119. glyph_info, glyph_map, rects = self.get_glyphs_with_font(
  120. font, s)
  121. else:
  122. glyph_info, glyph_map, rects = self.get_glyphs_mathtext(
  123. prop, s)
  124. else:
  125. glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)
  126. verts, codes = [], []
  127. for glyph_id, xposition, yposition, scale in glyph_info:
  128. verts1, codes1 = glyph_map[glyph_id]
  129. if len(verts1):
  130. verts1 = np.array(verts1) * scale + [xposition, yposition]
  131. verts.extend(verts1)
  132. codes.extend(codes1)
  133. for verts1, codes1 in rects:
  134. verts.extend(verts1)
  135. codes.extend(codes1)
  136. return verts, codes
  137. def get_glyphs_with_font(self, font, s, glyph_map=None,
  138. return_new_glyphs_only=False):
  139. """
  140. Convert string *s* to vertices and codes using the provided ttf font.
  141. """
  142. # Mostly copied from backend_svg.py.
  143. lastgind = None
  144. currx = 0
  145. xpositions = []
  146. glyph_ids = []
  147. if glyph_map is None:
  148. glyph_map = OrderedDict()
  149. if return_new_glyphs_only:
  150. glyph_map_new = OrderedDict()
  151. else:
  152. glyph_map_new = glyph_map
  153. # I'm not sure if I get kernings right. Needs to be verified. -JJL
  154. for c in s:
  155. ccode = ord(c)
  156. gind = font.get_char_index(ccode)
  157. if gind is None:
  158. ccode = ord('?')
  159. gind = 0
  160. if lastgind is not None:
  161. kern = font.get_kerning(lastgind, gind, KERNING_DEFAULT)
  162. else:
  163. kern = 0
  164. glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
  165. horiz_advance = glyph.linearHoriAdvance / 65536
  166. char_id = self._get_char_id(font, ccode)
  167. if char_id not in glyph_map:
  168. glyph_map_new[char_id] = self.glyph_to_path(font)
  169. currx += kern / 64
  170. xpositions.append(currx)
  171. glyph_ids.append(char_id)
  172. currx += horiz_advance
  173. lastgind = gind
  174. ypositions = [0] * len(xpositions)
  175. sizes = [1.] * len(xpositions)
  176. rects = []
  177. return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
  178. glyph_map_new, rects)
  179. def get_glyphs_mathtext(self, prop, s, glyph_map=None,
  180. return_new_glyphs_only=False):
  181. """
  182. convert the string *s* to vertices and codes by parsing it with
  183. mathtext.
  184. """
  185. prop = prop.copy()
  186. prop.set_size(self.FONT_SCALE)
  187. width, height, descent, glyphs, rects = self.mathtext_parser.parse(
  188. s, self.DPI, prop)
  189. if not glyph_map:
  190. glyph_map = OrderedDict()
  191. if return_new_glyphs_only:
  192. glyph_map_new = OrderedDict()
  193. else:
  194. glyph_map_new = glyph_map
  195. xpositions = []
  196. ypositions = []
  197. glyph_ids = []
  198. sizes = []
  199. currx, curry = 0, 0
  200. for font, fontsize, ccode, ox, oy in glyphs:
  201. char_id = self._get_char_id(font, ccode)
  202. if char_id not in glyph_map:
  203. font.clear()
  204. font.set_size(self.FONT_SCALE, self.DPI)
  205. glyph = font.load_char(ccode, flags=LOAD_NO_HINTING)
  206. glyph_map_new[char_id] = self.glyph_to_path(font)
  207. xpositions.append(ox)
  208. ypositions.append(oy)
  209. glyph_ids.append(char_id)
  210. size = fontsize / self.FONT_SCALE
  211. sizes.append(size)
  212. myrects = []
  213. for ox, oy, w, h in rects:
  214. vert1 = [(ox, oy), (ox, oy + h), (ox + w, oy + h),
  215. (ox + w, oy), (ox, oy), (0, 0)]
  216. code1 = [Path.MOVETO,
  217. Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
  218. Path.CLOSEPOLY]
  219. myrects.append((vert1, code1))
  220. return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
  221. glyph_map_new, myrects)
  222. def get_texmanager(self):
  223. """
  224. return the :class:`matplotlib.texmanager.TexManager` instance
  225. """
  226. if self._texmanager is None:
  227. from matplotlib.texmanager import TexManager
  228. self._texmanager = TexManager()
  229. return self._texmanager
  230. def get_glyphs_tex(self, prop, s, glyph_map=None,
  231. return_new_glyphs_only=False):
  232. """
  233. convert the string *s* to vertices and codes using matplotlib's usetex
  234. mode.
  235. """
  236. # codes are modstly borrowed from pdf backend.
  237. texmanager = self.get_texmanager()
  238. fontsize = prop.get_size_in_points()
  239. if hasattr(texmanager, "get_dvi"):
  240. dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE)
  241. dvi = dviread.DviFromFileLike(dvifilelike, self.DPI)
  242. else:
  243. dvifile = texmanager.make_dvi(s, self.FONT_SCALE)
  244. dvi = dviread.Dvi(dvifile, self.DPI)
  245. with dvi:
  246. page = next(iter(dvi))
  247. if glyph_map is None:
  248. glyph_map = OrderedDict()
  249. if return_new_glyphs_only:
  250. glyph_map_new = OrderedDict()
  251. else:
  252. glyph_map_new = glyph_map
  253. glyph_ids, xpositions, ypositions, sizes = [], [], [], []
  254. # Gather font information and do some setup for combining
  255. # characters into strings.
  256. # oldfont, seq = None, []
  257. for x1, y1, dvifont, glyph, width in page.text:
  258. font, enc = self._get_ps_font_and_encoding(dvifont.texname)
  259. char_id = self._get_char_id_ps(font, glyph)
  260. if char_id not in glyph_map:
  261. font.clear()
  262. font.set_size(self.FONT_SCALE, self.DPI)
  263. if enc:
  264. charcode = enc.get(glyph, None)
  265. else:
  266. charcode = glyph
  267. ft2font_flag = LOAD_TARGET_LIGHT
  268. if charcode is not None:
  269. glyph0 = font.load_char(charcode, flags=ft2font_flag)
  270. else:
  271. warnings.warn("The glyph (%d) of font (%s) cannot be "
  272. "converted with the encoding. Glyph may "
  273. "be wrong" % (glyph, font.fname))
  274. glyph0 = font.load_char(glyph, flags=ft2font_flag)
  275. glyph_map_new[char_id] = self.glyph_to_path(font)
  276. glyph_ids.append(char_id)
  277. xpositions.append(x1)
  278. ypositions.append(y1)
  279. sizes.append(dvifont.size / self.FONT_SCALE)
  280. myrects = []
  281. for ox, oy, h, w in page.boxes:
  282. vert1 = [(ox, oy), (ox + w, oy), (ox + w, oy + h),
  283. (ox, oy + h), (ox, oy), (0, 0)]
  284. code1 = [Path.MOVETO,
  285. Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO,
  286. Path.CLOSEPOLY]
  287. myrects.append((vert1, code1))
  288. return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
  289. glyph_map_new, myrects)
  290. @staticmethod
  291. @functools.lru_cache(50)
  292. def _get_ps_font_and_encoding(texname):
  293. tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
  294. font_bunch = tex_font_map[texname]
  295. if font_bunch.filename is None:
  296. raise ValueError(
  297. ("No usable font file found for %s (%s). "
  298. "The font may lack a Type-1 version.")
  299. % (font_bunch.psname, texname))
  300. font = get_font(font_bunch.filename)
  301. for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451),
  302. ("ADOBE_STANDARD", 1094995778)]:
  303. try:
  304. font.select_charmap(charmap_code)
  305. except (ValueError, RuntimeError):
  306. pass
  307. else:
  308. break
  309. else:
  310. charmap_name = ""
  311. warnings.warn("No supported encoding in font (%s)." %
  312. font_bunch.filename)
  313. if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
  314. enc0 = dviread.Encoding(font_bunch.encoding)
  315. enc = {i: _get_adobe_standard_encoding().get(c, None)
  316. for i, c in enumerate(enc0.encoding)}
  317. else:
  318. enc = {}
  319. return font, enc
  320. text_to_path = TextToPath()
  321. class TextPath(Path):
  322. """
  323. Create a path from the text.
  324. """
  325. def __init__(self, xy, s, size=None, prop=None,
  326. _interpolation_steps=1, usetex=False,
  327. *kl, **kwargs):
  328. r"""
  329. Create a path from the text. Note that it simply is a path,
  330. not an artist. You need to use the `~.PathPatch` (or other artists)
  331. to draw this path onto the canvas.
  332. Parameters
  333. ----------
  334. xy : tuple or array of two float values
  335. Position of the text. For no offset, use ``xy=(0, 0)``.
  336. s : str
  337. The text to convert to a path.
  338. size : float, optional
  339. Font size in points. Defaults to the size specified via the font
  340. properties *prop*.
  341. prop : `matplotlib.font_manager.FontProperties`, optional
  342. Font property. If not provided, will use a default
  343. ``FontProperties`` with parameters from the
  344. :ref:`rcParams <matplotlib-rcparams>`.
  345. _interpolation_steps : integer, optional
  346. (Currently ignored)
  347. usetex : bool, optional
  348. Whether to use tex rendering. Defaults to ``False``.
  349. Examples
  350. --------
  351. The following creates a path from the string "ABC" with Helvetica
  352. font face; and another path from the latex fraction 1/2::
  353. from matplotlib.textpath import TextPath
  354. from matplotlib.font_manager import FontProperties
  355. fp = FontProperties(family="Helvetica", style="italic")
  356. path1 = TextPath((12,12), "ABC", size=12, prop=fp)
  357. path2 = TextPath((0,0), r"$\frac{1}{2}$", size=12, usetex=True)
  358. Also see :doc:`/gallery/text_labels_and_annotations/demo_text_path`.
  359. """
  360. if prop is None:
  361. prop = FontProperties()
  362. if size is None:
  363. size = prop.get_size_in_points()
  364. self._xy = xy
  365. self.set_size(size)
  366. self._cached_vertices = None
  367. self._vertices, self._codes = self.text_get_vertices_codes(
  368. prop, s,
  369. usetex=usetex)
  370. self._should_simplify = False
  371. self._simplify_threshold = rcParams['path.simplify_threshold']
  372. self._has_nonfinite = False
  373. self._interpolation_steps = _interpolation_steps
  374. def set_size(self, size):
  375. """
  376. set the size of the text
  377. """
  378. self._size = size
  379. self._invalid = True
  380. def get_size(self):
  381. """
  382. get the size of the text
  383. """
  384. return self._size
  385. def _get_vertices(self):
  386. """
  387. Return the cached path after updating it if necessary.
  388. """
  389. self._revalidate_path()
  390. return self._cached_vertices
  391. def _get_codes(self):
  392. """
  393. Return the codes
  394. """
  395. return self._codes
  396. vertices = property(_get_vertices)
  397. codes = property(_get_codes)
  398. def _revalidate_path(self):
  399. """
  400. update the path if necessary.
  401. The path for the text is initially create with the font size
  402. of FONT_SCALE, and this path is rescaled to other size when
  403. necessary.
  404. """
  405. if self._invalid or self._cached_vertices is None:
  406. tr = Affine2D().scale(
  407. self._size / text_to_path.FONT_SCALE,
  408. self._size / text_to_path.FONT_SCALE).translate(*self._xy)
  409. self._cached_vertices = tr.transform(self._vertices)
  410. self._invalid = False
  411. def is_math_text(self, s):
  412. """
  413. Returns True if the given string *s* contains any mathtext.
  414. """
  415. # copied from Text.is_math_text -JJL
  416. # Did we find an even number of non-escaped dollar signs?
  417. # If so, treat is as math text.
  418. dollar_count = s.count(r'$') - s.count(r'\$')
  419. even_dollars = (dollar_count > 0 and dollar_count % 2 == 0)
  420. if rcParams['text.usetex']:
  421. return s, 'TeX'
  422. if even_dollars:
  423. return s, True
  424. else:
  425. return s.replace(r'\$', '$'), False
  426. def text_get_vertices_codes(self, prop, s, usetex):
  427. """
  428. convert the string *s* to vertices and codes using the
  429. provided font property *prop*. Mostly copied from
  430. backend_svg.py.
  431. """
  432. if usetex:
  433. verts, codes = text_to_path.get_text_path(prop, s, usetex=True)
  434. else:
  435. clean_line, ismath = self.is_math_text(s)
  436. verts, codes = text_to_path.get_text_path(prop, clean_line,
  437. ismath=ismath)
  438. return verts, codes