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.

3419 lines
118 KiB

4 years ago
  1. r"""
  2. :mod:`~matplotlib.mathtext` is a module for parsing a subset of the
  3. TeX math syntax and drawing them to a matplotlib backend.
  4. For a tutorial of its usage see :doc:`/tutorials/text/mathtext`. This
  5. document is primarily concerned with implementation details.
  6. The module uses pyparsing_ to parse the TeX expression.
  7. .. _pyparsing: http://pyparsing.wikispaces.com/
  8. The Bakoma distribution of the TeX Computer Modern fonts, and STIX
  9. fonts are supported. There is experimental support for using
  10. arbitrary fonts, but results may vary without proper tweaking and
  11. metrics for those fonts.
  12. """
  13. import functools
  14. from io import StringIO
  15. import os
  16. import types
  17. import unicodedata
  18. import warnings
  19. import numpy as np
  20. from pyparsing import (
  21. Combine, Empty, FollowedBy, Forward, Group, Literal, oneOf, OneOrMore,
  22. Optional, ParseBaseException, ParseFatalException, ParserElement,
  23. QuotedString, Regex, StringEnd, Suppress, ZeroOrMore)
  24. ParserElement.enablePackrat()
  25. from matplotlib import _png, cbook, colors as mcolors, get_data_path, rcParams
  26. from matplotlib.afm import AFM
  27. from matplotlib.cbook import get_realpath_and_stat
  28. from matplotlib.ft2font import FT2Image, KERNING_DEFAULT, LOAD_NO_HINTING
  29. from matplotlib.font_manager import findfont, FontProperties, get_font
  30. from matplotlib._mathtext_data import (latex_to_bakoma, latex_to_standard,
  31. tex2uni, latex_to_cmex,
  32. stix_virtual_fonts)
  33. ####################
  34. ##############################################################################
  35. # FONTS
  36. def get_unicode_index(symbol, math=True):
  37. """get_unicode_index(symbol, [bool]) -> integer
  38. Return the integer index (from the Unicode table) of symbol. *symbol*
  39. can be a single unicode character, a TeX command (i.e. r'\\pi'), or a
  40. Type1 symbol name (i.e. 'phi').
  41. If math is False, the current symbol should be treated as a non-math symbol.
  42. """
  43. # for a non-math symbol, simply return its unicode index
  44. if not math:
  45. return ord(symbol)
  46. # From UTF #25: U+2212 minus sign is the preferred
  47. # representation of the unary and binary minus sign rather than
  48. # the ASCII-derived U+002D hyphen-minus, because minus sign is
  49. # unambiguous and because it is rendered with a more desirable
  50. # length, usually longer than a hyphen.
  51. if symbol == '-':
  52. return 0x2212
  53. try: # This will succeed if symbol is a single unicode char
  54. return ord(symbol)
  55. except TypeError:
  56. pass
  57. try: # Is symbol a TeX symbol (i.e. \alpha)
  58. return tex2uni[symbol.strip("\\")]
  59. except KeyError:
  60. raise ValueError(
  61. "'{}' is not a valid Unicode character or TeX/Type1 symbol"
  62. .format(symbol))
  63. unichr_safe = cbook.deprecated("3.0")(chr)
  64. class MathtextBackend(object):
  65. """
  66. The base class for the mathtext backend-specific code. The
  67. purpose of :class:`MathtextBackend` subclasses is to interface
  68. between mathtext and a specific matplotlib graphics backend.
  69. Subclasses need to override the following:
  70. - :meth:`render_glyph`
  71. - :meth:`render_rect_filled`
  72. - :meth:`get_results`
  73. And optionally, if you need to use a FreeType hinting style:
  74. - :meth:`get_hinting_type`
  75. """
  76. def __init__(self):
  77. self.width = 0
  78. self.height = 0
  79. self.depth = 0
  80. def set_canvas_size(self, w, h, d):
  81. 'Dimension the drawing canvas'
  82. self.width = w
  83. self.height = h
  84. self.depth = d
  85. def render_glyph(self, ox, oy, info):
  86. """
  87. Draw a glyph described by *info* to the reference point (*ox*,
  88. *oy*).
  89. """
  90. raise NotImplementedError()
  91. def render_rect_filled(self, x1, y1, x2, y2):
  92. """
  93. Draw a filled black rectangle from (*x1*, *y1*) to (*x2*, *y2*).
  94. """
  95. raise NotImplementedError()
  96. def get_results(self, box):
  97. """
  98. Return a backend-specific tuple to return to the backend after
  99. all processing is done.
  100. """
  101. raise NotImplementedError()
  102. def get_hinting_type(self):
  103. """
  104. Get the FreeType hinting type to use with this particular
  105. backend.
  106. """
  107. return LOAD_NO_HINTING
  108. class MathtextBackendAgg(MathtextBackend):
  109. """
  110. Render glyphs and rectangles to an FTImage buffer, which is later
  111. transferred to the Agg image by the Agg backend.
  112. """
  113. def __init__(self):
  114. self.ox = 0
  115. self.oy = 0
  116. self.image = None
  117. self.mode = 'bbox'
  118. self.bbox = [0, 0, 0, 0]
  119. MathtextBackend.__init__(self)
  120. def _update_bbox(self, x1, y1, x2, y2):
  121. self.bbox = [min(self.bbox[0], x1),
  122. min(self.bbox[1], y1),
  123. max(self.bbox[2], x2),
  124. max(self.bbox[3], y2)]
  125. def set_canvas_size(self, w, h, d):
  126. MathtextBackend.set_canvas_size(self, w, h, d)
  127. if self.mode != 'bbox':
  128. self.image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0)))
  129. def render_glyph(self, ox, oy, info):
  130. if self.mode == 'bbox':
  131. self._update_bbox(ox + info.metrics.xmin,
  132. oy - info.metrics.ymax,
  133. ox + info.metrics.xmax,
  134. oy - info.metrics.ymin)
  135. else:
  136. info.font.draw_glyph_to_bitmap(
  137. self.image, ox, oy - info.metrics.iceberg, info.glyph,
  138. antialiased=rcParams['text.antialiased'])
  139. def render_rect_filled(self, x1, y1, x2, y2):
  140. if self.mode == 'bbox':
  141. self._update_bbox(x1, y1, x2, y2)
  142. else:
  143. height = max(int(y2 - y1) - 1, 0)
  144. if height == 0:
  145. center = (y2 + y1) / 2.0
  146. y = int(center - (height + 1) / 2.0)
  147. else:
  148. y = int(y1)
  149. self.image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height)
  150. def get_results(self, box, used_characters):
  151. self.mode = 'bbox'
  152. orig_height = box.height
  153. orig_depth = box.depth
  154. ship(0, 0, box)
  155. bbox = self.bbox
  156. bbox = [bbox[0] - 1, bbox[1] - 1, bbox[2] + 1, bbox[3] + 1]
  157. self.mode = 'render'
  158. self.set_canvas_size(
  159. bbox[2] - bbox[0],
  160. (bbox[3] - bbox[1]) - orig_depth,
  161. (bbox[3] - bbox[1]) - orig_height)
  162. ship(-bbox[0], -bbox[1], box)
  163. result = (self.ox,
  164. self.oy,
  165. self.width,
  166. self.height + self.depth,
  167. self.depth,
  168. self.image,
  169. used_characters)
  170. self.image = None
  171. return result
  172. def get_hinting_type(self):
  173. from matplotlib.backends import backend_agg
  174. return backend_agg.get_hinting_flag()
  175. class MathtextBackendBitmap(MathtextBackendAgg):
  176. def get_results(self, box, used_characters):
  177. ox, oy, width, height, depth, image, characters = \
  178. MathtextBackendAgg.get_results(self, box, used_characters)
  179. return image, depth
  180. class MathtextBackendPs(MathtextBackend):
  181. """
  182. Store information to write a mathtext rendering to the PostScript
  183. backend.
  184. """
  185. def __init__(self):
  186. self.pswriter = StringIO()
  187. self.lastfont = None
  188. def render_glyph(self, ox, oy, info):
  189. oy = self.height - oy + info.offset
  190. postscript_name = info.postscript_name
  191. fontsize = info.fontsize
  192. symbol_name = info.symbol_name
  193. if (postscript_name, fontsize) != self.lastfont:
  194. ps = """/%(postscript_name)s findfont
  195. %(fontsize)s scalefont
  196. setfont
  197. """ % locals()
  198. self.lastfont = postscript_name, fontsize
  199. self.pswriter.write(ps)
  200. ps = """%(ox)f %(oy)f moveto
  201. /%(symbol_name)s glyphshow\n
  202. """ % locals()
  203. self.pswriter.write(ps)
  204. def render_rect_filled(self, x1, y1, x2, y2):
  205. ps = "%f %f %f %f rectfill\n" % (x1, self.height - y2, x2 - x1, y2 - y1)
  206. self.pswriter.write(ps)
  207. def get_results(self, box, used_characters):
  208. ship(0, 0, box)
  209. return (self.width,
  210. self.height + self.depth,
  211. self.depth,
  212. self.pswriter,
  213. used_characters)
  214. class MathtextBackendPdf(MathtextBackend):
  215. """
  216. Store information to write a mathtext rendering to the PDF
  217. backend.
  218. """
  219. def __init__(self):
  220. self.glyphs = []
  221. self.rects = []
  222. def render_glyph(self, ox, oy, info):
  223. filename = info.font.fname
  224. oy = self.height - oy + info.offset
  225. self.glyphs.append(
  226. (ox, oy, filename, info.fontsize,
  227. info.num, info.symbol_name))
  228. def render_rect_filled(self, x1, y1, x2, y2):
  229. self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1))
  230. def get_results(self, box, used_characters):
  231. ship(0, 0, box)
  232. return (self.width,
  233. self.height + self.depth,
  234. self.depth,
  235. self.glyphs,
  236. self.rects,
  237. used_characters)
  238. class MathtextBackendSvg(MathtextBackend):
  239. """
  240. Store information to write a mathtext rendering to the SVG
  241. backend.
  242. """
  243. def __init__(self):
  244. self.svg_glyphs = []
  245. self.svg_rects = []
  246. def render_glyph(self, ox, oy, info):
  247. oy = self.height - oy + info.offset
  248. self.svg_glyphs.append(
  249. (info.font, info.fontsize, info.num, ox, oy, info.metrics))
  250. def render_rect_filled(self, x1, y1, x2, y2):
  251. self.svg_rects.append(
  252. (x1, self.height - y1 + 1, x2 - x1, y2 - y1))
  253. def get_results(self, box, used_characters):
  254. ship(0, 0, box)
  255. svg_elements = types.SimpleNamespace(svg_glyphs=self.svg_glyphs,
  256. svg_rects=self.svg_rects)
  257. return (self.width,
  258. self.height + self.depth,
  259. self.depth,
  260. svg_elements,
  261. used_characters)
  262. class MathtextBackendPath(MathtextBackend):
  263. """
  264. Store information to write a mathtext rendering to the text path
  265. machinery.
  266. """
  267. def __init__(self):
  268. self.glyphs = []
  269. self.rects = []
  270. def render_glyph(self, ox, oy, info):
  271. oy = self.height - oy + info.offset
  272. thetext = info.num
  273. self.glyphs.append(
  274. (info.font, info.fontsize, thetext, ox, oy))
  275. def render_rect_filled(self, x1, y1, x2, y2):
  276. self.rects.append(
  277. (x1, self.height-y2 , x2 - x1, y2 - y1))
  278. def get_results(self, box, used_characters):
  279. ship(0, 0, box)
  280. return (self.width,
  281. self.height + self.depth,
  282. self.depth,
  283. self.glyphs,
  284. self.rects)
  285. class MathtextBackendCairo(MathtextBackend):
  286. """
  287. Store information to write a mathtext rendering to the Cairo
  288. backend.
  289. """
  290. def __init__(self):
  291. self.glyphs = []
  292. self.rects = []
  293. def render_glyph(self, ox, oy, info):
  294. oy = oy - info.offset - self.height
  295. thetext = chr(info.num)
  296. self.glyphs.append(
  297. (info.font, info.fontsize, thetext, ox, oy))
  298. def render_rect_filled(self, x1, y1, x2, y2):
  299. self.rects.append(
  300. (x1, y1 - self.height, x2 - x1, y2 - y1))
  301. def get_results(self, box, used_characters):
  302. ship(0, 0, box)
  303. return (self.width,
  304. self.height + self.depth,
  305. self.depth,
  306. self.glyphs,
  307. self.rects)
  308. class Fonts(object):
  309. """
  310. An abstract base class for a system of fonts to use for mathtext.
  311. The class must be able to take symbol keys and font file names and
  312. return the character metrics. It also delegates to a backend class
  313. to do the actual drawing.
  314. """
  315. def __init__(self, default_font_prop, mathtext_backend):
  316. """
  317. *default_font_prop*: A
  318. :class:`~matplotlib.font_manager.FontProperties` object to use
  319. for the default non-math font, or the base font for Unicode
  320. (generic) font rendering.
  321. *mathtext_backend*: A subclass of :class:`MathTextBackend`
  322. used to delegate the actual rendering.
  323. """
  324. self.default_font_prop = default_font_prop
  325. self.mathtext_backend = mathtext_backend
  326. self.used_characters = {}
  327. def destroy(self):
  328. """
  329. Fix any cyclical references before the object is about
  330. to be destroyed.
  331. """
  332. self.used_characters = None
  333. def get_kern(self, font1, fontclass1, sym1, fontsize1,
  334. font2, fontclass2, sym2, fontsize2, dpi):
  335. """
  336. Get the kerning distance for font between *sym1* and *sym2*.
  337. *fontX*: one of the TeX font names::
  338. tt, it, rm, cal, sf, bf or default/regular (non-math)
  339. *fontclassX*: TODO
  340. *symX*: a symbol in raw TeX form. e.g., '1', 'x' or '\\sigma'
  341. *fontsizeX*: the fontsize in points
  342. *dpi*: the current dots-per-inch
  343. """
  344. return 0.
  345. def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True):
  346. """
  347. *font*: one of the TeX font names::
  348. tt, it, rm, cal, sf, bf or default/regular (non-math)
  349. *font_class*: TODO
  350. *sym*: a symbol in raw TeX form. e.g., '1', 'x' or '\\sigma'
  351. *fontsize*: font size in points
  352. *dpi*: current dots-per-inch
  353. *math*: whether sym is a math character
  354. Returns an object with the following attributes:
  355. - *advance*: The advance distance (in points) of the glyph.
  356. - *height*: The height of the glyph in points.
  357. - *width*: The width of the glyph in points.
  358. - *xmin*, *xmax*, *ymin*, *ymax* - the ink rectangle of the glyph
  359. - *iceberg* - the distance from the baseline to the top of
  360. the glyph. This corresponds to TeX's definition of
  361. "height".
  362. """
  363. info = self._get_info(font, font_class, sym, fontsize, dpi, math)
  364. return info.metrics
  365. def set_canvas_size(self, w, h, d):
  366. """
  367. Set the size of the buffer used to render the math expression.
  368. Only really necessary for the bitmap backends.
  369. """
  370. self.width, self.height, self.depth = np.ceil([w, h, d])
  371. self.mathtext_backend.set_canvas_size(
  372. self.width, self.height, self.depth)
  373. def render_glyph(self, ox, oy, facename, font_class, sym, fontsize, dpi):
  374. """
  375. Draw a glyph at
  376. - *ox*, *oy*: position
  377. - *facename*: One of the TeX face names
  378. - *font_class*:
  379. - *sym*: TeX symbol name or single character
  380. - *fontsize*: fontsize in points
  381. - *dpi*: The dpi to draw at.
  382. """
  383. info = self._get_info(facename, font_class, sym, fontsize, dpi)
  384. realpath, stat_key = get_realpath_and_stat(info.font.fname)
  385. used_characters = self.used_characters.setdefault(
  386. stat_key, (realpath, set()))
  387. used_characters[1].add(info.num)
  388. self.mathtext_backend.render_glyph(ox, oy, info)
  389. def render_rect_filled(self, x1, y1, x2, y2):
  390. """
  391. Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*).
  392. """
  393. self.mathtext_backend.render_rect_filled(x1, y1, x2, y2)
  394. def get_xheight(self, font, fontsize, dpi):
  395. """
  396. Get the xheight for the given *font* and *fontsize*.
  397. """
  398. raise NotImplementedError()
  399. def get_underline_thickness(self, font, fontsize, dpi):
  400. """
  401. Get the line thickness that matches the given font. Used as a
  402. base unit for drawing lines such as in a fraction or radical.
  403. """
  404. raise NotImplementedError()
  405. def get_used_characters(self):
  406. """
  407. Get the set of characters that were used in the math
  408. expression. Used by backends that need to subset fonts so
  409. they know which glyphs to include.
  410. """
  411. return self.used_characters
  412. def get_results(self, box):
  413. """
  414. Get the data needed by the backend to render the math
  415. expression. The return value is backend-specific.
  416. """
  417. result = self.mathtext_backend.get_results(box, self.get_used_characters())
  418. self.destroy()
  419. return result
  420. def get_sized_alternatives_for_symbol(self, fontname, sym):
  421. """
  422. Override if your font provides multiple sizes of the same
  423. symbol. Should return a list of symbols matching *sym* in
  424. various sizes. The expression renderer will select the most
  425. appropriate size for a given situation from this list.
  426. """
  427. return [(fontname, sym)]
  428. class TruetypeFonts(Fonts):
  429. """
  430. A generic base class for all font setups that use Truetype fonts
  431. (through FT2Font).
  432. """
  433. def __init__(self, default_font_prop, mathtext_backend):
  434. Fonts.__init__(self, default_font_prop, mathtext_backend)
  435. self.glyphd = {}
  436. self._fonts = {}
  437. filename = findfont(default_font_prop)
  438. default_font = get_font(filename)
  439. self._fonts['default'] = default_font
  440. self._fonts['regular'] = default_font
  441. def destroy(self):
  442. self.glyphd = None
  443. Fonts.destroy(self)
  444. def _get_font(self, font):
  445. if font in self.fontmap:
  446. basename = self.fontmap[font]
  447. else:
  448. basename = font
  449. cached_font = self._fonts.get(basename)
  450. if cached_font is None and os.path.exists(basename):
  451. cached_font = get_font(basename)
  452. self._fonts[basename] = cached_font
  453. self._fonts[cached_font.postscript_name] = cached_font
  454. self._fonts[cached_font.postscript_name.lower()] = cached_font
  455. return cached_font
  456. def _get_offset(self, font, glyph, fontsize, dpi):
  457. if font.postscript_name == 'Cmex10':
  458. return ((glyph.height/64.0/2.0) + (fontsize/3.0 * dpi/72.0))
  459. return 0.
  460. def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True):
  461. key = fontname, font_class, sym, fontsize, dpi
  462. bunch = self.glyphd.get(key)
  463. if bunch is not None:
  464. return bunch
  465. font, num, symbol_name, fontsize, slanted = \
  466. self._get_glyph(fontname, font_class, sym, fontsize, math)
  467. font.set_size(fontsize, dpi)
  468. glyph = font.load_char(
  469. num,
  470. flags=self.mathtext_backend.get_hinting_type())
  471. xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox]
  472. offset = self._get_offset(font, glyph, fontsize, dpi)
  473. metrics = types.SimpleNamespace(
  474. advance = glyph.linearHoriAdvance/65536.0,
  475. height = glyph.height/64.0,
  476. width = glyph.width/64.0,
  477. xmin = xmin,
  478. xmax = xmax,
  479. ymin = ymin+offset,
  480. ymax = ymax+offset,
  481. # iceberg is the equivalent of TeX's "height"
  482. iceberg = glyph.horiBearingY/64.0 + offset,
  483. slanted = slanted
  484. )
  485. result = self.glyphd[key] = types.SimpleNamespace(
  486. font = font,
  487. fontsize = fontsize,
  488. postscript_name = font.postscript_name,
  489. metrics = metrics,
  490. symbol_name = symbol_name,
  491. num = num,
  492. glyph = glyph,
  493. offset = offset
  494. )
  495. return result
  496. def get_xheight(self, fontname, fontsize, dpi):
  497. font = self._get_font(fontname)
  498. font.set_size(fontsize, dpi)
  499. pclt = font.get_sfnt_table('pclt')
  500. if pclt is None:
  501. # Some fonts don't store the xHeight, so we do a poor man's xHeight
  502. metrics = self.get_metrics(fontname, rcParams['mathtext.default'], 'x', fontsize, dpi)
  503. return metrics.iceberg
  504. xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0)
  505. return xHeight
  506. def get_underline_thickness(self, font, fontsize, dpi):
  507. # This function used to grab underline thickness from the font
  508. # metrics, but that information is just too un-reliable, so it
  509. # is now hardcoded.
  510. return ((0.75 / 12.0) * fontsize * dpi) / 72.0
  511. def get_kern(self, font1, fontclass1, sym1, fontsize1,
  512. font2, fontclass2, sym2, fontsize2, dpi):
  513. if font1 == font2 and fontsize1 == fontsize2:
  514. info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
  515. info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
  516. font = info1.font
  517. return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64.0
  518. return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1,
  519. font2, fontclass2, sym2, fontsize2, dpi)
  520. class BakomaFonts(TruetypeFonts):
  521. """
  522. Use the Bakoma TrueType fonts for rendering.
  523. Symbols are strewn about a number of font files, each of which has
  524. its own proprietary 8-bit encoding.
  525. """
  526. _fontmap = { 'cal' : 'cmsy10',
  527. 'rm' : 'cmr10',
  528. 'tt' : 'cmtt10',
  529. 'it' : 'cmmi10',
  530. 'bf' : 'cmb10',
  531. 'sf' : 'cmss10',
  532. 'ex' : 'cmex10'
  533. }
  534. def __init__(self, *args, **kwargs):
  535. self._stix_fallback = StixFonts(*args, **kwargs)
  536. TruetypeFonts.__init__(self, *args, **kwargs)
  537. self.fontmap = {}
  538. for key, val in self._fontmap.items():
  539. fullpath = findfont(val)
  540. self.fontmap[key] = fullpath
  541. self.fontmap[val] = fullpath
  542. _slanted_symbols = set(r"\int \oint".split())
  543. def _get_glyph(self, fontname, font_class, sym, fontsize, math=True):
  544. symbol_name = None
  545. font = None
  546. if fontname in self.fontmap and sym in latex_to_bakoma:
  547. basename, num = latex_to_bakoma[sym]
  548. slanted = (basename == "cmmi10") or sym in self._slanted_symbols
  549. font = self._get_font(basename)
  550. elif len(sym) == 1:
  551. slanted = (fontname == "it")
  552. font = self._get_font(fontname)
  553. if font is not None:
  554. num = ord(sym)
  555. if font is not None:
  556. gid = font.get_char_index(num)
  557. if gid != 0:
  558. symbol_name = font.get_glyph_name(gid)
  559. if symbol_name is None:
  560. return self._stix_fallback._get_glyph(
  561. fontname, font_class, sym, fontsize, math)
  562. return font, num, symbol_name, fontsize, slanted
  563. # The Bakoma fonts contain many pre-sized alternatives for the
  564. # delimiters. The AutoSizedChar class will use these alternatives
  565. # and select the best (closest sized) glyph.
  566. _size_alternatives = {
  567. '(' : [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
  568. ('ex', '\xb5'), ('ex', '\xc3')],
  569. ')' : [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
  570. ('ex', '\xb6'), ('ex', '\x21')],
  571. '{' : [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'),
  572. ('ex', '\xbd'), ('ex', '\x28')],
  573. '}' : [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'),
  574. ('ex', '\xbe'), ('ex', '\x29')],
  575. # The fourth size of '[' is mysteriously missing from the BaKoMa
  576. # font, so I've omitted it for both '[' and ']'
  577. '[' : [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'),
  578. ('ex', '\x22')],
  579. ']' : [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'),
  580. ('ex', '\x23')],
  581. r'\lfloor' : [('ex', '\xa5'), ('ex', '\x6a'),
  582. ('ex', '\xb9'), ('ex', '\x24')],
  583. r'\rfloor' : [('ex', '\xa6'), ('ex', '\x6b'),
  584. ('ex', '\xba'), ('ex', '\x25')],
  585. r'\lceil' : [('ex', '\xa7'), ('ex', '\x6c'),
  586. ('ex', '\xbb'), ('ex', '\x26')],
  587. r'\rceil' : [('ex', '\xa8'), ('ex', '\x6d'),
  588. ('ex', '\xbc'), ('ex', '\x27')],
  589. r'\langle' : [('ex', '\xad'), ('ex', '\x44'),
  590. ('ex', '\xbf'), ('ex', '\x2a')],
  591. r'\rangle' : [('ex', '\xae'), ('ex', '\x45'),
  592. ('ex', '\xc0'), ('ex', '\x2b')],
  593. r'\__sqrt__' : [('ex', '\x70'), ('ex', '\x71'),
  594. ('ex', '\x72'), ('ex', '\x73')],
  595. r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'),
  596. ('ex', '\xc2'), ('ex', '\x2d')],
  597. r'/' : [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'),
  598. ('ex', '\xcb'), ('ex', '\x2c')],
  599. r'\widehat' : [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'),
  600. ('ex', '\x64')],
  601. r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'),
  602. ('ex', '\x67')],
  603. r'<' : [('cal', 'h'), ('ex', 'D')],
  604. r'>' : [('cal', 'i'), ('ex', 'E')]
  605. }
  606. for alias, target in [(r'\leftparen', '('),
  607. (r'\rightparent', ')'),
  608. (r'\leftbrace', '{'),
  609. (r'\rightbrace', '}'),
  610. (r'\leftbracket', '['),
  611. (r'\rightbracket', ']'),
  612. (r'\{', '{'),
  613. (r'\}', '}'),
  614. (r'\[', '['),
  615. (r'\]', ']')]:
  616. _size_alternatives[alias] = _size_alternatives[target]
  617. def get_sized_alternatives_for_symbol(self, fontname, sym):
  618. return self._size_alternatives.get(sym, [(fontname, sym)])
  619. class UnicodeFonts(TruetypeFonts):
  620. """
  621. An abstract base class for handling Unicode fonts.
  622. While some reasonably complete Unicode fonts (such as DejaVu) may
  623. work in some situations, the only Unicode font I'm aware of with a
  624. complete set of math symbols is STIX.
  625. This class will "fallback" on the Bakoma fonts when a required
  626. symbol can not be found in the font.
  627. """
  628. use_cmex = True
  629. def __init__(self, *args, **kwargs):
  630. # This must come first so the backend's owner is set correctly
  631. if rcParams['mathtext.fallback_to_cm']:
  632. self.cm_fallback = BakomaFonts(*args, **kwargs)
  633. else:
  634. self.cm_fallback = None
  635. TruetypeFonts.__init__(self, *args, **kwargs)
  636. self.fontmap = {}
  637. for texfont in "cal rm tt it bf sf".split():
  638. prop = rcParams['mathtext.' + texfont]
  639. font = findfont(prop)
  640. self.fontmap[texfont] = font
  641. prop = FontProperties('cmex10')
  642. font = findfont(prop)
  643. self.fontmap['ex'] = font
  644. _slanted_symbols = set(r"\int \oint".split())
  645. def _map_virtual_font(self, fontname, font_class, uniindex):
  646. return fontname, uniindex
  647. def _get_glyph(self, fontname, font_class, sym, fontsize, math=True):
  648. found_symbol = False
  649. if self.use_cmex:
  650. uniindex = latex_to_cmex.get(sym)
  651. if uniindex is not None:
  652. fontname = 'ex'
  653. found_symbol = True
  654. if not found_symbol:
  655. try:
  656. uniindex = get_unicode_index(sym, math)
  657. found_symbol = True
  658. except ValueError:
  659. uniindex = ord('?')
  660. warnings.warn(
  661. "No TeX to unicode mapping for {!a}.".format(sym),
  662. MathTextWarning)
  663. fontname, uniindex = self._map_virtual_font(
  664. fontname, font_class, uniindex)
  665. new_fontname = fontname
  666. # Only characters in the "Letter" class should be italicized in 'it'
  667. # mode. Greek capital letters should be Roman.
  668. if found_symbol:
  669. if fontname == 'it':
  670. if uniindex < 0x10000:
  671. unistring = chr(uniindex)
  672. if (not unicodedata.category(unistring)[0] == "L"
  673. or unicodedata.name(unistring).startswith("GREEK CAPITAL")):
  674. new_fontname = 'rm'
  675. slanted = (new_fontname == 'it') or sym in self._slanted_symbols
  676. found_symbol = False
  677. font = self._get_font(new_fontname)
  678. if font is not None:
  679. glyphindex = font.get_char_index(uniindex)
  680. if glyphindex != 0:
  681. found_symbol = True
  682. if not found_symbol:
  683. if self.cm_fallback:
  684. if isinstance(self.cm_fallback, BakomaFonts):
  685. warnings.warn(
  686. "Substituting with a symbol from Computer Modern.",
  687. MathTextWarning)
  688. if (fontname in ('it', 'regular') and
  689. isinstance(self.cm_fallback, StixFonts)):
  690. return self.cm_fallback._get_glyph(
  691. 'rm', font_class, sym, fontsize)
  692. else:
  693. return self.cm_fallback._get_glyph(
  694. fontname, font_class, sym, fontsize)
  695. else:
  696. if (fontname in ('it', 'regular')
  697. and isinstance(self, StixFonts)):
  698. return self._get_glyph('rm', font_class, sym, fontsize)
  699. warnings.warn(
  700. "Font {!r} does not have a glyph for {!a} [U+{:x}], "
  701. "substituting with a dummy symbol.".format(
  702. new_fontname, sym, uniindex),
  703. MathTextWarning)
  704. fontname = 'rm'
  705. new_fontname = fontname
  706. font = self._get_font(fontname)
  707. uniindex = 0xA4 # currency character, for lack of anything better
  708. glyphindex = font.get_char_index(uniindex)
  709. slanted = False
  710. symbol_name = font.get_glyph_name(glyphindex)
  711. return font, uniindex, symbol_name, fontsize, slanted
  712. def get_sized_alternatives_for_symbol(self, fontname, sym):
  713. if self.cm_fallback:
  714. return self.cm_fallback.get_sized_alternatives_for_symbol(
  715. fontname, sym)
  716. return [(fontname, sym)]
  717. class DejaVuFonts(UnicodeFonts):
  718. use_cmex = False
  719. def __init__(self, *args, **kwargs):
  720. # This must come first so the backend's owner is set correctly
  721. if isinstance(self, DejaVuSerifFonts):
  722. self.cm_fallback = StixFonts(*args, **kwargs)
  723. else:
  724. self.cm_fallback = StixSansFonts(*args, **kwargs)
  725. self.bakoma = BakomaFonts(*args, **kwargs)
  726. TruetypeFonts.__init__(self, *args, **kwargs)
  727. self.fontmap = {}
  728. # Include Stix sized alternatives for glyphs
  729. self._fontmap.update({
  730. 1 : 'STIXSizeOneSym',
  731. 2 : 'STIXSizeTwoSym',
  732. 3 : 'STIXSizeThreeSym',
  733. 4 : 'STIXSizeFourSym',
  734. 5 : 'STIXSizeFiveSym'})
  735. for key, name in self._fontmap.items():
  736. fullpath = findfont(name)
  737. self.fontmap[key] = fullpath
  738. self.fontmap[name] = fullpath
  739. def _get_glyph(self, fontname, font_class, sym, fontsize, math=True):
  740. """ Override prime symbol to use Bakoma """
  741. if sym == r'\prime':
  742. return self.bakoma._get_glyph(
  743. fontname, font_class, sym, fontsize, math)
  744. else:
  745. # check whether the glyph is available in the display font
  746. uniindex = get_unicode_index(sym)
  747. font = self._get_font('ex')
  748. if font is not None:
  749. glyphindex = font.get_char_index(uniindex)
  750. if glyphindex != 0:
  751. return super()._get_glyph(
  752. 'ex', font_class, sym, fontsize, math)
  753. # otherwise return regular glyph
  754. return super()._get_glyph(
  755. fontname, font_class, sym, fontsize, math)
  756. class DejaVuSerifFonts(DejaVuFonts):
  757. """
  758. A font handling class for the DejaVu Serif fonts
  759. If a glyph is not found it will fallback to Stix Serif
  760. """
  761. _fontmap = { 'rm' : 'DejaVu Serif',
  762. 'it' : 'DejaVu Serif:italic',
  763. 'bf' : 'DejaVu Serif:weight=bold',
  764. 'sf' : 'DejaVu Sans',
  765. 'tt' : 'DejaVu Sans Mono',
  766. 'ex' : 'DejaVu Serif Display',
  767. 0 : 'DejaVu Serif',
  768. }
  769. class DejaVuSansFonts(DejaVuFonts):
  770. """
  771. A font handling class for the DejaVu Sans fonts
  772. If a glyph is not found it will fallback to Stix Sans
  773. """
  774. _fontmap = { 'rm' : 'DejaVu Sans',
  775. 'it' : 'DejaVu Sans:italic',
  776. 'bf' : 'DejaVu Sans:weight=bold',
  777. 'sf' : 'DejaVu Sans',
  778. 'tt' : 'DejaVu Sans Mono',
  779. 'ex' : 'DejaVu Sans Display',
  780. 0 : 'DejaVu Sans',
  781. }
  782. class StixFonts(UnicodeFonts):
  783. """
  784. A font handling class for the STIX fonts.
  785. In addition to what UnicodeFonts provides, this class:
  786. - supports "virtual fonts" which are complete alpha numeric
  787. character sets with different font styles at special Unicode
  788. code points, such as "Blackboard".
  789. - handles sized alternative characters for the STIXSizeX fonts.
  790. """
  791. _fontmap = { 'rm' : 'STIXGeneral',
  792. 'it' : 'STIXGeneral:italic',
  793. 'bf' : 'STIXGeneral:weight=bold',
  794. 'nonunirm' : 'STIXNonUnicode',
  795. 'nonuniit' : 'STIXNonUnicode:italic',
  796. 'nonunibf' : 'STIXNonUnicode:weight=bold',
  797. 0 : 'STIXGeneral',
  798. 1 : 'STIXSizeOneSym',
  799. 2 : 'STIXSizeTwoSym',
  800. 3 : 'STIXSizeThreeSym',
  801. 4 : 'STIXSizeFourSym',
  802. 5 : 'STIXSizeFiveSym'
  803. }
  804. use_cmex = False
  805. cm_fallback = False
  806. _sans = False
  807. def __init__(self, *args, **kwargs):
  808. TruetypeFonts.__init__(self, *args, **kwargs)
  809. self.fontmap = {}
  810. for key, name in self._fontmap.items():
  811. fullpath = findfont(name)
  812. self.fontmap[key] = fullpath
  813. self.fontmap[name] = fullpath
  814. def _map_virtual_font(self, fontname, font_class, uniindex):
  815. # Handle these "fonts" that are actually embedded in
  816. # other fonts.
  817. mapping = stix_virtual_fonts.get(fontname)
  818. if (self._sans and mapping is None and
  819. fontname not in ('regular', 'default')):
  820. mapping = stix_virtual_fonts['sf']
  821. doing_sans_conversion = True
  822. else:
  823. doing_sans_conversion = False
  824. if mapping is not None:
  825. if isinstance(mapping, dict):
  826. try:
  827. mapping = mapping[font_class]
  828. except KeyError:
  829. mapping = mapping['rm']
  830. # Binary search for the source glyph
  831. lo = 0
  832. hi = len(mapping)
  833. while lo < hi:
  834. mid = (lo+hi)//2
  835. range = mapping[mid]
  836. if uniindex < range[0]:
  837. hi = mid
  838. elif uniindex <= range[1]:
  839. break
  840. else:
  841. lo = mid + 1
  842. if range[0] <= uniindex <= range[1]:
  843. uniindex = uniindex - range[0] + range[3]
  844. fontname = range[2]
  845. elif not doing_sans_conversion:
  846. # This will generate a dummy character
  847. uniindex = 0x1
  848. fontname = rcParams['mathtext.default']
  849. # Handle private use area glyphs
  850. if (fontname in ('it', 'rm', 'bf') and
  851. uniindex >= 0xe000 and uniindex <= 0xf8ff):
  852. fontname = 'nonuni' + fontname
  853. return fontname, uniindex
  854. _size_alternatives = {}
  855. def get_sized_alternatives_for_symbol(self, fontname, sym):
  856. fixes = {'\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']'}
  857. sym = fixes.get(sym, sym)
  858. alternatives = self._size_alternatives.get(sym)
  859. if alternatives:
  860. return alternatives
  861. alternatives = []
  862. try:
  863. uniindex = get_unicode_index(sym)
  864. except ValueError:
  865. return [(fontname, sym)]
  866. fix_ups = {
  867. ord('<'): 0x27e8,
  868. ord('>'): 0x27e9 }
  869. uniindex = fix_ups.get(uniindex, uniindex)
  870. for i in range(6):
  871. font = self._get_font(i)
  872. glyphindex = font.get_char_index(uniindex)
  873. if glyphindex != 0:
  874. alternatives.append((i, chr(uniindex)))
  875. # The largest size of the radical symbol in STIX has incorrect
  876. # metrics that cause it to be disconnected from the stem.
  877. if sym == r'\__sqrt__':
  878. alternatives = alternatives[:-1]
  879. self._size_alternatives[sym] = alternatives
  880. return alternatives
  881. class StixSansFonts(StixFonts):
  882. """
  883. A font handling class for the STIX fonts (that uses sans-serif
  884. characters by default).
  885. """
  886. _sans = True
  887. class StandardPsFonts(Fonts):
  888. """
  889. Use the standard postscript fonts for rendering to backend_ps
  890. Unlike the other font classes, BakomaFont and UnicodeFont, this
  891. one requires the Ps backend.
  892. """
  893. basepath = os.path.join( get_data_path(), 'fonts', 'afm' )
  894. fontmap = { 'cal' : 'pzcmi8a', # Zapf Chancery
  895. 'rm' : 'pncr8a', # New Century Schoolbook
  896. 'tt' : 'pcrr8a', # Courier
  897. 'it' : 'pncri8a', # New Century Schoolbook Italic
  898. 'sf' : 'phvr8a', # Helvetica
  899. 'bf' : 'pncb8a', # New Century Schoolbook Bold
  900. None : 'psyr' # Symbol
  901. }
  902. def __init__(self, default_font_prop):
  903. Fonts.__init__(self, default_font_prop, MathtextBackendPs())
  904. self.glyphd = {}
  905. self.fonts = {}
  906. filename = findfont(default_font_prop, fontext='afm',
  907. directory=self.basepath)
  908. if filename is None:
  909. filename = findfont('Helvetica', fontext='afm',
  910. directory=self.basepath)
  911. with open(filename, 'rb') as fd:
  912. default_font = AFM(fd)
  913. default_font.fname = filename
  914. self.fonts['default'] = default_font
  915. self.fonts['regular'] = default_font
  916. self.pswriter = StringIO()
  917. def _get_font(self, font):
  918. if font in self.fontmap:
  919. basename = self.fontmap[font]
  920. else:
  921. basename = font
  922. cached_font = self.fonts.get(basename)
  923. if cached_font is None:
  924. fname = os.path.join(self.basepath, basename + ".afm")
  925. with open(fname, 'rb') as fd:
  926. cached_font = AFM(fd)
  927. cached_font.fname = fname
  928. self.fonts[basename] = cached_font
  929. self.fonts[cached_font.get_fontname()] = cached_font
  930. return cached_font
  931. def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True):
  932. 'load the cmfont, metrics and glyph with caching'
  933. key = fontname, sym, fontsize, dpi
  934. tup = self.glyphd.get(key)
  935. if tup is not None:
  936. return tup
  937. # Only characters in the "Letter" class should really be italicized.
  938. # This class includes greek letters, so we're ok
  939. if (fontname == 'it' and
  940. (len(sym) > 1 or not unicodedata.category(sym).startswith("L"))):
  941. fontname = 'rm'
  942. found_symbol = False
  943. if sym in latex_to_standard:
  944. fontname, num = latex_to_standard[sym]
  945. glyph = chr(num)
  946. found_symbol = True
  947. elif len(sym) == 1:
  948. glyph = sym
  949. num = ord(glyph)
  950. found_symbol = True
  951. else:
  952. warnings.warn(
  953. "No TeX to built-in Postscript mapping for {!r}".format(sym),
  954. MathTextWarning)
  955. slanted = (fontname == 'it')
  956. font = self._get_font(fontname)
  957. if found_symbol:
  958. try:
  959. symbol_name = font.get_name_char(glyph)
  960. except KeyError:
  961. warnings.warn(
  962. "No glyph in standard Postscript font {!r} for {!r}"
  963. .format(font.get_fontname(), sym),
  964. MathTextWarning)
  965. found_symbol = False
  966. if not found_symbol:
  967. glyph = sym = '?'
  968. num = ord(glyph)
  969. symbol_name = font.get_name_char(glyph)
  970. offset = 0
  971. scale = 0.001 * fontsize
  972. xmin, ymin, xmax, ymax = [val * scale
  973. for val in font.get_bbox_char(glyph)]
  974. metrics = types.SimpleNamespace(
  975. advance = font.get_width_char(glyph) * scale,
  976. width = font.get_width_char(glyph) * scale,
  977. height = font.get_height_char(glyph) * scale,
  978. xmin = xmin,
  979. xmax = xmax,
  980. ymin = ymin+offset,
  981. ymax = ymax+offset,
  982. # iceberg is the equivalent of TeX's "height"
  983. iceberg = ymax + offset,
  984. slanted = slanted
  985. )
  986. self.glyphd[key] = types.SimpleNamespace(
  987. font = font,
  988. fontsize = fontsize,
  989. postscript_name = font.get_fontname(),
  990. metrics = metrics,
  991. symbol_name = symbol_name,
  992. num = num,
  993. glyph = glyph,
  994. offset = offset
  995. )
  996. return self.glyphd[key]
  997. def get_kern(self, font1, fontclass1, sym1, fontsize1,
  998. font2, fontclass2, sym2, fontsize2, dpi):
  999. if font1 == font2 and fontsize1 == fontsize2:
  1000. info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
  1001. info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
  1002. font = info1.font
  1003. return (font.get_kern_dist(info1.glyph, info2.glyph)
  1004. * 0.001 * fontsize1)
  1005. return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1,
  1006. font2, fontclass2, sym2, fontsize2, dpi)
  1007. def get_xheight(self, font, fontsize, dpi):
  1008. font = self._get_font(font)
  1009. return font.get_xheight() * 0.001 * fontsize
  1010. def get_underline_thickness(self, font, fontsize, dpi):
  1011. font = self._get_font(font)
  1012. return font.get_underline_thickness() * 0.001 * fontsize
  1013. ##############################################################################
  1014. # TeX-LIKE BOX MODEL
  1015. # The following is based directly on the document 'woven' from the
  1016. # TeX82 source code. This information is also available in printed
  1017. # form:
  1018. #
  1019. # Knuth, Donald E.. 1986. Computers and Typesetting, Volume B:
  1020. # TeX: The Program. Addison-Wesley Professional.
  1021. #
  1022. # The most relevant "chapters" are:
  1023. # Data structures for boxes and their friends
  1024. # Shipping pages out (Ship class)
  1025. # Packaging (hpack and vpack)
  1026. # Data structures for math mode
  1027. # Subroutines for math mode
  1028. # Typesetting math formulas
  1029. #
  1030. # Many of the docstrings below refer to a numbered "node" in that
  1031. # book, e.g., node123
  1032. #
  1033. # Note that (as TeX) y increases downward, unlike many other parts of
  1034. # matplotlib.
  1035. # How much text shrinks when going to the next-smallest level. GROW_FACTOR
  1036. # must be the inverse of SHRINK_FACTOR.
  1037. SHRINK_FACTOR = 0.7
  1038. GROW_FACTOR = 1.0 / SHRINK_FACTOR
  1039. # The number of different sizes of chars to use, beyond which they will not
  1040. # get any smaller
  1041. NUM_SIZE_LEVELS = 6
  1042. class FontConstantsBase(object):
  1043. """
  1044. A set of constants that controls how certain things, such as sub-
  1045. and superscripts are laid out. These are all metrics that can't
  1046. be reliably retrieved from the font metrics in the font itself.
  1047. """
  1048. # Percentage of x-height of additional horiz. space after sub/superscripts
  1049. script_space = 0.05
  1050. # Percentage of x-height that sub/superscripts drop below the baseline
  1051. subdrop = 0.4
  1052. # Percentage of x-height that superscripts are raised from the baseline
  1053. sup1 = 0.7
  1054. # Percentage of x-height that subscripts drop below the baseline
  1055. sub1 = 0.3
  1056. # Percentage of x-height that subscripts drop below the baseline when a
  1057. # superscript is present
  1058. sub2 = 0.5
  1059. # Percentage of x-height that sub/supercripts are offset relative to the
  1060. # nucleus edge for non-slanted nuclei
  1061. delta = 0.025
  1062. # Additional percentage of last character height above 2/3 of the
  1063. # x-height that supercripts are offset relative to the subscript
  1064. # for slanted nuclei
  1065. delta_slanted = 0.2
  1066. # Percentage of x-height that supercripts and subscripts are offset for
  1067. # integrals
  1068. delta_integral = 0.1
  1069. class ComputerModernFontConstants(FontConstantsBase):
  1070. script_space = 0.075
  1071. subdrop = 0.2
  1072. sup1 = 0.45
  1073. sub1 = 0.2
  1074. sub2 = 0.3
  1075. delta = 0.075
  1076. delta_slanted = 0.3
  1077. delta_integral = 0.3
  1078. class STIXFontConstants(FontConstantsBase):
  1079. script_space = 0.1
  1080. sup1 = 0.8
  1081. sub2 = 0.6
  1082. delta = 0.05
  1083. delta_slanted = 0.3
  1084. delta_integral = 0.3
  1085. class STIXSansFontConstants(FontConstantsBase):
  1086. script_space = 0.05
  1087. sup1 = 0.8
  1088. delta_slanted = 0.6
  1089. delta_integral = 0.3
  1090. class DejaVuSerifFontConstants(FontConstantsBase):
  1091. pass
  1092. class DejaVuSansFontConstants(FontConstantsBase):
  1093. pass
  1094. # Maps font family names to the FontConstantBase subclass to use
  1095. _font_constant_mapping = {
  1096. 'DejaVu Sans': DejaVuSansFontConstants,
  1097. 'DejaVu Sans Mono': DejaVuSansFontConstants,
  1098. 'DejaVu Serif': DejaVuSerifFontConstants,
  1099. 'cmb10': ComputerModernFontConstants,
  1100. 'cmex10': ComputerModernFontConstants,
  1101. 'cmmi10': ComputerModernFontConstants,
  1102. 'cmr10': ComputerModernFontConstants,
  1103. 'cmss10': ComputerModernFontConstants,
  1104. 'cmsy10': ComputerModernFontConstants,
  1105. 'cmtt10': ComputerModernFontConstants,
  1106. 'STIXGeneral': STIXFontConstants,
  1107. 'STIXNonUnicode': STIXFontConstants,
  1108. 'STIXSizeFiveSym': STIXFontConstants,
  1109. 'STIXSizeFourSym': STIXFontConstants,
  1110. 'STIXSizeThreeSym': STIXFontConstants,
  1111. 'STIXSizeTwoSym': STIXFontConstants,
  1112. 'STIXSizeOneSym': STIXFontConstants,
  1113. # Map the fonts we used to ship, just for good measure
  1114. 'Bitstream Vera Sans': DejaVuSansFontConstants,
  1115. 'Bitstream Vera': DejaVuSansFontConstants,
  1116. }
  1117. def _get_font_constant_set(state):
  1118. constants = _font_constant_mapping.get(
  1119. state.font_output._get_font(state.font).family_name,
  1120. FontConstantsBase)
  1121. # STIX sans isn't really its own fonts, just different code points
  1122. # in the STIX fonts, so we have to detect this one separately.
  1123. if (constants is STIXFontConstants and
  1124. isinstance(state.font_output, StixSansFonts)):
  1125. return STIXSansFontConstants
  1126. return constants
  1127. class MathTextWarning(Warning):
  1128. pass
  1129. class Node(object):
  1130. """
  1131. A node in the TeX box model
  1132. """
  1133. def __init__(self):
  1134. self.size = 0
  1135. def __repr__(self):
  1136. return self.__class__.__name__
  1137. def get_kerning(self, next):
  1138. return 0.0
  1139. def shrink(self):
  1140. """
  1141. Shrinks one level smaller. There are only three levels of
  1142. sizes, after which things will no longer get smaller.
  1143. """
  1144. self.size += 1
  1145. def grow(self):
  1146. """
  1147. Grows one level larger. There is no limit to how big
  1148. something can get.
  1149. """
  1150. self.size -= 1
  1151. def render(self, x, y):
  1152. pass
  1153. class Box(Node):
  1154. """
  1155. Represents any node with a physical location.
  1156. """
  1157. def __init__(self, width, height, depth):
  1158. Node.__init__(self)
  1159. self.width = width
  1160. self.height = height
  1161. self.depth = depth
  1162. def shrink(self):
  1163. Node.shrink(self)
  1164. if self.size < NUM_SIZE_LEVELS:
  1165. self.width *= SHRINK_FACTOR
  1166. self.height *= SHRINK_FACTOR
  1167. self.depth *= SHRINK_FACTOR
  1168. def grow(self):
  1169. Node.grow(self)
  1170. self.width *= GROW_FACTOR
  1171. self.height *= GROW_FACTOR
  1172. self.depth *= GROW_FACTOR
  1173. def render(self, x1, y1, x2, y2):
  1174. pass
  1175. class Vbox(Box):
  1176. """
  1177. A box with only height (zero width).
  1178. """
  1179. def __init__(self, height, depth):
  1180. Box.__init__(self, 0., height, depth)
  1181. class Hbox(Box):
  1182. """
  1183. A box with only width (zero height and depth).
  1184. """
  1185. def __init__(self, width):
  1186. Box.__init__(self, width, 0., 0.)
  1187. class Char(Node):
  1188. """
  1189. Represents a single character. Unlike TeX, the font information
  1190. and metrics are stored with each :class:`Char` to make it easier
  1191. to lookup the font metrics when needed. Note that TeX boxes have
  1192. a width, height, and depth, unlike Type1 and Truetype which use a
  1193. full bounding box and an advance in the x-direction. The metrics
  1194. must be converted to the TeX way, and the advance (if different
  1195. from width) must be converted into a :class:`Kern` node when the
  1196. :class:`Char` is added to its parent :class:`Hlist`.
  1197. """
  1198. def __init__(self, c, state, math=True):
  1199. Node.__init__(self)
  1200. self.c = c
  1201. self.font_output = state.font_output
  1202. self.font = state.font
  1203. self.font_class = state.font_class
  1204. self.fontsize = state.fontsize
  1205. self.dpi = state.dpi
  1206. self.math = math
  1207. # The real width, height and depth will be set during the
  1208. # pack phase, after we know the real fontsize
  1209. self._update_metrics()
  1210. def __repr__(self):
  1211. return '`%s`' % self.c
  1212. def _update_metrics(self):
  1213. metrics = self._metrics = self.font_output.get_metrics(
  1214. self.font, self.font_class, self.c, self.fontsize, self.dpi, self.math)
  1215. if self.c == ' ':
  1216. self.width = metrics.advance
  1217. else:
  1218. self.width = metrics.width
  1219. self.height = metrics.iceberg
  1220. self.depth = -(metrics.iceberg - metrics.height)
  1221. def is_slanted(self):
  1222. return self._metrics.slanted
  1223. def get_kerning(self, next):
  1224. """
  1225. Return the amount of kerning between this and the given
  1226. character. Called when characters are strung together into
  1227. :class:`Hlist` to create :class:`Kern` nodes.
  1228. """
  1229. advance = self._metrics.advance - self.width
  1230. kern = 0.
  1231. if isinstance(next, Char):
  1232. kern = self.font_output.get_kern(
  1233. self.font, self.font_class, self.c, self.fontsize,
  1234. next.font, next.font_class, next.c, next.fontsize,
  1235. self.dpi)
  1236. return advance + kern
  1237. def render(self, x, y):
  1238. """
  1239. Render the character to the canvas
  1240. """
  1241. self.font_output.render_glyph(
  1242. x, y,
  1243. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  1244. def shrink(self):
  1245. Node.shrink(self)
  1246. if self.size < NUM_SIZE_LEVELS:
  1247. self.fontsize *= SHRINK_FACTOR
  1248. self.width *= SHRINK_FACTOR
  1249. self.height *= SHRINK_FACTOR
  1250. self.depth *= SHRINK_FACTOR
  1251. def grow(self):
  1252. Node.grow(self)
  1253. self.fontsize *= GROW_FACTOR
  1254. self.width *= GROW_FACTOR
  1255. self.height *= GROW_FACTOR
  1256. self.depth *= GROW_FACTOR
  1257. class Accent(Char):
  1258. """
  1259. The font metrics need to be dealt with differently for accents,
  1260. since they are already offset correctly from the baseline in
  1261. TrueType fonts.
  1262. """
  1263. def _update_metrics(self):
  1264. metrics = self._metrics = self.font_output.get_metrics(
  1265. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  1266. self.width = metrics.xmax - metrics.xmin
  1267. self.height = metrics.ymax - metrics.ymin
  1268. self.depth = 0
  1269. def shrink(self):
  1270. Char.shrink(self)
  1271. self._update_metrics()
  1272. def grow(self):
  1273. Char.grow(self)
  1274. self._update_metrics()
  1275. def render(self, x, y):
  1276. """
  1277. Render the character to the canvas.
  1278. """
  1279. self.font_output.render_glyph(
  1280. x - self._metrics.xmin, y + self._metrics.ymin,
  1281. self.font, self.font_class, self.c, self.fontsize, self.dpi)
  1282. class List(Box):
  1283. """
  1284. A list of nodes (either horizontal or vertical).
  1285. """
  1286. def __init__(self, elements):
  1287. Box.__init__(self, 0., 0., 0.)
  1288. self.shift_amount = 0. # An arbitrary offset
  1289. self.children = elements # The child nodes of this list
  1290. # The following parameters are set in the vpack and hpack functions
  1291. self.glue_set = 0. # The glue setting of this list
  1292. self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching
  1293. self.glue_order = 0 # The order of infinity (0 - 3) for the glue
  1294. def __repr__(self):
  1295. return '[%s <%.02f %.02f %.02f %.02f> %s]' % (
  1296. super().__repr__(),
  1297. self.width, self.height,
  1298. self.depth, self.shift_amount,
  1299. ' '.join([repr(x) for x in self.children]))
  1300. @staticmethod
  1301. def _determine_order(totals):
  1302. """
  1303. Determine the highest order of glue used by the members of this list.
  1304. Helper function used by vpack and hpack.
  1305. """
  1306. for i in range(len(totals))[::-1]:
  1307. if totals[i] != 0:
  1308. return i
  1309. return 0
  1310. def _set_glue(self, x, sign, totals, error_type):
  1311. o = self._determine_order(totals)
  1312. self.glue_order = o
  1313. self.glue_sign = sign
  1314. if totals[o] != 0.:
  1315. self.glue_set = x / totals[o]
  1316. else:
  1317. self.glue_sign = 0
  1318. self.glue_ratio = 0.
  1319. if o == 0:
  1320. if len(self.children):
  1321. warnings.warn(
  1322. "%s %s: %r" % (error_type, self.__class__.__name__, self),
  1323. MathTextWarning)
  1324. def shrink(self):
  1325. for child in self.children:
  1326. child.shrink()
  1327. Box.shrink(self)
  1328. if self.size < NUM_SIZE_LEVELS:
  1329. self.shift_amount *= SHRINK_FACTOR
  1330. self.glue_set *= SHRINK_FACTOR
  1331. def grow(self):
  1332. for child in self.children:
  1333. child.grow()
  1334. Box.grow(self)
  1335. self.shift_amount *= GROW_FACTOR
  1336. self.glue_set *= GROW_FACTOR
  1337. class Hlist(List):
  1338. """
  1339. A horizontal list of boxes.
  1340. """
  1341. def __init__(self, elements, w=0., m='additional', do_kern=True):
  1342. List.__init__(self, elements)
  1343. if do_kern:
  1344. self.kern()
  1345. self.hpack()
  1346. def kern(self):
  1347. """
  1348. Insert :class:`Kern` nodes between :class:`Char` nodes to set
  1349. kerning. The :class:`Char` nodes themselves determine the
  1350. amount of kerning they need (in :meth:`~Char.get_kerning`),
  1351. and this function just creates the linked list in the correct
  1352. way.
  1353. """
  1354. new_children = []
  1355. num_children = len(self.children)
  1356. if num_children:
  1357. for i in range(num_children):
  1358. elem = self.children[i]
  1359. if i < num_children - 1:
  1360. next = self.children[i + 1]
  1361. else:
  1362. next = None
  1363. new_children.append(elem)
  1364. kerning_distance = elem.get_kerning(next)
  1365. if kerning_distance != 0.:
  1366. kern = Kern(kerning_distance)
  1367. new_children.append(kern)
  1368. self.children = new_children
  1369. # This is a failed experiment to fake cross-font kerning.
  1370. # def get_kerning(self, next):
  1371. # if len(self.children) >= 2 and isinstance(self.children[-2], Char):
  1372. # if isinstance(next, Char):
  1373. # print "CASE A"
  1374. # return self.children[-2].get_kerning(next)
  1375. # elif isinstance(next, Hlist) and len(next.children) and isinstance(next.children[0], Char):
  1376. # print "CASE B"
  1377. # result = self.children[-2].get_kerning(next.children[0])
  1378. # print result
  1379. # return result
  1380. # return 0.0
  1381. def hpack(self, w=0., m='additional'):
  1382. """
  1383. The main duty of :meth:`hpack` is to compute the dimensions of
  1384. the resulting boxes, and to adjust the glue if one of those
  1385. dimensions is pre-specified. The computed sizes normally
  1386. enclose all of the material inside the new box; but some items
  1387. may stick out if negative glue is used, if the box is
  1388. overfull, or if a ``\\vbox`` includes other boxes that have
  1389. been shifted left.
  1390. - *w*: specifies a width
  1391. - *m*: is either 'exactly' or 'additional'.
  1392. Thus, ``hpack(w, 'exactly')`` produces a box whose width is
  1393. exactly *w*, while ``hpack(w, 'additional')`` yields a box
  1394. whose width is the natural width plus *w*. The default values
  1395. produce a box with the natural width.
  1396. """
  1397. # I don't know why these get reset in TeX. Shift_amount is pretty
  1398. # much useless if we do.
  1399. #self.shift_amount = 0.
  1400. h = 0.
  1401. d = 0.
  1402. x = 0.
  1403. total_stretch = [0.] * 4
  1404. total_shrink = [0.] * 4
  1405. for p in self.children:
  1406. if isinstance(p, Char):
  1407. x += p.width
  1408. h = max(h, p.height)
  1409. d = max(d, p.depth)
  1410. elif isinstance(p, Box):
  1411. x += p.width
  1412. if not np.isinf(p.height) and not np.isinf(p.depth):
  1413. s = getattr(p, 'shift_amount', 0.)
  1414. h = max(h, p.height - s)
  1415. d = max(d, p.depth + s)
  1416. elif isinstance(p, Glue):
  1417. glue_spec = p.glue_spec
  1418. x += glue_spec.width
  1419. total_stretch[glue_spec.stretch_order] += glue_spec.stretch
  1420. total_shrink[glue_spec.shrink_order] += glue_spec.shrink
  1421. elif isinstance(p, Kern):
  1422. x += p.width
  1423. self.height = h
  1424. self.depth = d
  1425. if m == 'additional':
  1426. w += x
  1427. self.width = w
  1428. x = w - x
  1429. if x == 0.:
  1430. self.glue_sign = 0
  1431. self.glue_order = 0
  1432. self.glue_ratio = 0.
  1433. return
  1434. if x > 0.:
  1435. self._set_glue(x, 1, total_stretch, "Overfull")
  1436. else:
  1437. self._set_glue(x, -1, total_shrink, "Underfull")
  1438. class Vlist(List):
  1439. """
  1440. A vertical list of boxes.
  1441. """
  1442. def __init__(self, elements, h=0., m='additional'):
  1443. List.__init__(self, elements)
  1444. self.vpack()
  1445. def vpack(self, h=0., m='additional', l=np.inf):
  1446. """
  1447. The main duty of :meth:`vpack` is to compute the dimensions of
  1448. the resulting boxes, and to adjust the glue if one of those
  1449. dimensions is pre-specified.
  1450. - *h*: specifies a height
  1451. - *m*: is either 'exactly' or 'additional'.
  1452. - *l*: a maximum height
  1453. Thus, ``vpack(h, 'exactly')`` produces a box whose height is
  1454. exactly *h*, while ``vpack(h, 'additional')`` yields a box
  1455. whose height is the natural height plus *h*. The default
  1456. values produce a box with the natural width.
  1457. """
  1458. # I don't know why these get reset in TeX. Shift_amount is pretty
  1459. # much useless if we do.
  1460. # self.shift_amount = 0.
  1461. w = 0.
  1462. d = 0.
  1463. x = 0.
  1464. total_stretch = [0.] * 4
  1465. total_shrink = [0.] * 4
  1466. for p in self.children:
  1467. if isinstance(p, Box):
  1468. x += d + p.height
  1469. d = p.depth
  1470. if not np.isinf(p.width):
  1471. s = getattr(p, 'shift_amount', 0.)
  1472. w = max(w, p.width + s)
  1473. elif isinstance(p, Glue):
  1474. x += d
  1475. d = 0.
  1476. glue_spec = p.glue_spec
  1477. x += glue_spec.width
  1478. total_stretch[glue_spec.stretch_order] += glue_spec.stretch
  1479. total_shrink[glue_spec.shrink_order] += glue_spec.shrink
  1480. elif isinstance(p, Kern):
  1481. x += d + p.width
  1482. d = 0.
  1483. elif isinstance(p, Char):
  1484. raise RuntimeError("Internal mathtext error: Char node found in Vlist.")
  1485. self.width = w
  1486. if d > l:
  1487. x += d - l
  1488. self.depth = l
  1489. else:
  1490. self.depth = d
  1491. if m == 'additional':
  1492. h += x
  1493. self.height = h
  1494. x = h - x
  1495. if x == 0:
  1496. self.glue_sign = 0
  1497. self.glue_order = 0
  1498. self.glue_ratio = 0.
  1499. return
  1500. if x > 0.:
  1501. self._set_glue(x, 1, total_stretch, "Overfull")
  1502. else:
  1503. self._set_glue(x, -1, total_shrink, "Underfull")
  1504. class Rule(Box):
  1505. """
  1506. A :class:`Rule` node stands for a solid black rectangle; it has
  1507. *width*, *depth*, and *height* fields just as in an
  1508. :class:`Hlist`. However, if any of these dimensions is inf, the
  1509. actual value will be determined by running the rule up to the
  1510. boundary of the innermost enclosing box. This is called a "running
  1511. dimension." The width is never running in an :class:`Hlist`; the
  1512. height and depth are never running in a :class:`Vlist`.
  1513. """
  1514. def __init__(self, width, height, depth, state):
  1515. Box.__init__(self, width, height, depth)
  1516. self.font_output = state.font_output
  1517. def render(self, x, y, w, h):
  1518. self.font_output.render_rect_filled(x, y, x + w, y + h)
  1519. class Hrule(Rule):
  1520. """
  1521. Convenience class to create a horizontal rule.
  1522. """
  1523. def __init__(self, state, thickness=None):
  1524. if thickness is None:
  1525. thickness = state.font_output.get_underline_thickness(
  1526. state.font, state.fontsize, state.dpi)
  1527. height = depth = thickness * 0.5
  1528. Rule.__init__(self, np.inf, height, depth, state)
  1529. class Vrule(Rule):
  1530. """
  1531. Convenience class to create a vertical rule.
  1532. """
  1533. def __init__(self, state):
  1534. thickness = state.font_output.get_underline_thickness(
  1535. state.font, state.fontsize, state.dpi)
  1536. Rule.__init__(self, thickness, np.inf, np.inf, state)
  1537. class Glue(Node):
  1538. """
  1539. Most of the information in this object is stored in the underlying
  1540. :class:`GlueSpec` class, which is shared between multiple glue objects.
  1541. (This is a memory optimization which probably doesn't matter anymore, but
  1542. it's easier to stick to what TeX does.)
  1543. """
  1544. def __init__(self, glue_type, copy=False):
  1545. Node.__init__(self)
  1546. self.glue_subtype = 'normal'
  1547. if isinstance(glue_type, str):
  1548. glue_spec = GlueSpec.factory(glue_type)
  1549. elif isinstance(glue_type, GlueSpec):
  1550. glue_spec = glue_type
  1551. else:
  1552. raise ValueError("glue_type must be a glue spec name or instance")
  1553. if copy:
  1554. glue_spec = glue_spec.copy()
  1555. self.glue_spec = glue_spec
  1556. def shrink(self):
  1557. Node.shrink(self)
  1558. if self.size < NUM_SIZE_LEVELS:
  1559. if self.glue_spec.width != 0.:
  1560. self.glue_spec = self.glue_spec.copy()
  1561. self.glue_spec.width *= SHRINK_FACTOR
  1562. def grow(self):
  1563. Node.grow(self)
  1564. if self.glue_spec.width != 0.:
  1565. self.glue_spec = self.glue_spec.copy()
  1566. self.glue_spec.width *= GROW_FACTOR
  1567. class GlueSpec(object):
  1568. """
  1569. See :class:`Glue`.
  1570. """
  1571. def __init__(self, width=0., stretch=0., stretch_order=0, shrink=0., shrink_order=0):
  1572. self.width = width
  1573. self.stretch = stretch
  1574. self.stretch_order = stretch_order
  1575. self.shrink = shrink
  1576. self.shrink_order = shrink_order
  1577. def copy(self):
  1578. return GlueSpec(
  1579. self.width,
  1580. self.stretch,
  1581. self.stretch_order,
  1582. self.shrink,
  1583. self.shrink_order)
  1584. def factory(cls, glue_type):
  1585. return cls._types[glue_type]
  1586. factory = classmethod(factory)
  1587. GlueSpec._types = {
  1588. 'fil': GlueSpec(0., 1., 1, 0., 0),
  1589. 'fill': GlueSpec(0., 1., 2, 0., 0),
  1590. 'filll': GlueSpec(0., 1., 3, 0., 0),
  1591. 'neg_fil': GlueSpec(0., 0., 0, 1., 1),
  1592. 'neg_fill': GlueSpec(0., 0., 0, 1., 2),
  1593. 'neg_filll': GlueSpec(0., 0., 0, 1., 3),
  1594. 'empty': GlueSpec(0., 0., 0, 0., 0),
  1595. 'ss': GlueSpec(0., 1., 1, -1., 1)
  1596. }
  1597. # Some convenient ways to get common kinds of glue
  1598. class Fil(Glue):
  1599. def __init__(self):
  1600. Glue.__init__(self, 'fil')
  1601. class Fill(Glue):
  1602. def __init__(self):
  1603. Glue.__init__(self, 'fill')
  1604. class Filll(Glue):
  1605. def __init__(self):
  1606. Glue.__init__(self, 'filll')
  1607. class NegFil(Glue):
  1608. def __init__(self):
  1609. Glue.__init__(self, 'neg_fil')
  1610. class NegFill(Glue):
  1611. def __init__(self):
  1612. Glue.__init__(self, 'neg_fill')
  1613. class NegFilll(Glue):
  1614. def __init__(self):
  1615. Glue.__init__(self, 'neg_filll')
  1616. class SsGlue(Glue):
  1617. def __init__(self):
  1618. Glue.__init__(self, 'ss')
  1619. class HCentered(Hlist):
  1620. """
  1621. A convenience class to create an :class:`Hlist` whose contents are
  1622. centered within its enclosing box.
  1623. """
  1624. def __init__(self, elements):
  1625. Hlist.__init__(self, [SsGlue()] + elements + [SsGlue()],
  1626. do_kern=False)
  1627. class VCentered(Hlist):
  1628. """
  1629. A convenience class to create a :class:`Vlist` whose contents are
  1630. centered within its enclosing box.
  1631. """
  1632. def __init__(self, elements):
  1633. Vlist.__init__(self, [SsGlue()] + elements + [SsGlue()])
  1634. class Kern(Node):
  1635. """
  1636. A :class:`Kern` node has a width field to specify a (normally
  1637. negative) amount of spacing. This spacing correction appears in
  1638. horizontal lists between letters like A and V when the font
  1639. designer said that it looks better to move them closer together or
  1640. further apart. A kern node can also appear in a vertical list,
  1641. when its *width* denotes additional spacing in the vertical
  1642. direction.
  1643. """
  1644. height = 0
  1645. depth = 0
  1646. def __init__(self, width):
  1647. Node.__init__(self)
  1648. self.width = width
  1649. def __repr__(self):
  1650. return "k%.02f" % self.width
  1651. def shrink(self):
  1652. Node.shrink(self)
  1653. if self.size < NUM_SIZE_LEVELS:
  1654. self.width *= SHRINK_FACTOR
  1655. def grow(self):
  1656. Node.grow(self)
  1657. self.width *= GROW_FACTOR
  1658. class SubSuperCluster(Hlist):
  1659. """
  1660. :class:`SubSuperCluster` is a sort of hack to get around that fact
  1661. that this code do a two-pass parse like TeX. This lets us store
  1662. enough information in the hlist itself, namely the nucleus, sub-
  1663. and super-script, such that if another script follows that needs
  1664. to be attached, it can be reconfigured on the fly.
  1665. """
  1666. def __init__(self):
  1667. self.nucleus = None
  1668. self.sub = None
  1669. self.super = None
  1670. Hlist.__init__(self, [])
  1671. class AutoHeightChar(Hlist):
  1672. """
  1673. :class:`AutoHeightChar` will create a character as close to the
  1674. given height and depth as possible. When using a font with
  1675. multiple height versions of some characters (such as the BaKoMa
  1676. fonts), the correct glyph will be selected, otherwise this will
  1677. always just return a scaled version of the glyph.
  1678. """
  1679. def __init__(self, c, height, depth, state, always=False, factor=None):
  1680. alternatives = state.font_output.get_sized_alternatives_for_symbol(
  1681. state.font, c)
  1682. xHeight = state.font_output.get_xheight(
  1683. state.font, state.fontsize, state.dpi)
  1684. state = state.copy()
  1685. target_total = height + depth
  1686. for fontname, sym in alternatives:
  1687. state.font = fontname
  1688. char = Char(sym, state)
  1689. # Ensure that size 0 is chosen when the text is regular sized but
  1690. # with descender glyphs by subtracting 0.2 * xHeight
  1691. if char.height + char.depth >= target_total - 0.2 * xHeight:
  1692. break
  1693. shift = 0
  1694. if state.font != 0:
  1695. if factor is None:
  1696. factor = (target_total) / (char.height + char.depth)
  1697. state.fontsize *= factor
  1698. char = Char(sym, state)
  1699. shift = (depth - char.depth)
  1700. Hlist.__init__(self, [char])
  1701. self.shift_amount = shift
  1702. class AutoWidthChar(Hlist):
  1703. """
  1704. :class:`AutoWidthChar` will create a character as close to the
  1705. given width as possible. When using a font with multiple width
  1706. versions of some characters (such as the BaKoMa fonts), the
  1707. correct glyph will be selected, otherwise this will always just
  1708. return a scaled version of the glyph.
  1709. """
  1710. def __init__(self, c, width, state, always=False, char_class=Char):
  1711. alternatives = state.font_output.get_sized_alternatives_for_symbol(
  1712. state.font, c)
  1713. state = state.copy()
  1714. for fontname, sym in alternatives:
  1715. state.font = fontname
  1716. char = char_class(sym, state)
  1717. if char.width >= width:
  1718. break
  1719. factor = width / char.width
  1720. state.fontsize *= factor
  1721. char = char_class(sym, state)
  1722. Hlist.__init__(self, [char])
  1723. self.width = char.width
  1724. class Ship(object):
  1725. """
  1726. Once the boxes have been set up, this sends them to output. Since
  1727. boxes can be inside of boxes inside of boxes, the main work of
  1728. :class:`Ship` is done by two mutually recursive routines,
  1729. :meth:`hlist_out` and :meth:`vlist_out`, which traverse the
  1730. :class:`Hlist` nodes and :class:`Vlist` nodes inside of horizontal
  1731. and vertical boxes. The global variables used in TeX to store
  1732. state as it processes have become member variables here.
  1733. """
  1734. def __call__(self, ox, oy, box):
  1735. self.max_push = 0 # Deepest nesting of push commands so far
  1736. self.cur_s = 0
  1737. self.cur_v = 0.
  1738. self.cur_h = 0.
  1739. self.off_h = ox
  1740. self.off_v = oy + box.height
  1741. self.hlist_out(box)
  1742. def clamp(value):
  1743. if value < -1000000000.:
  1744. return -1000000000.
  1745. if value > 1000000000.:
  1746. return 1000000000.
  1747. return value
  1748. clamp = staticmethod(clamp)
  1749. def hlist_out(self, box):
  1750. cur_g = 0
  1751. cur_glue = 0.
  1752. glue_order = box.glue_order
  1753. glue_sign = box.glue_sign
  1754. base_line = self.cur_v
  1755. left_edge = self.cur_h
  1756. self.cur_s += 1
  1757. self.max_push = max(self.cur_s, self.max_push)
  1758. clamp = self.clamp
  1759. for p in box.children:
  1760. if isinstance(p, Char):
  1761. p.render(self.cur_h + self.off_h, self.cur_v + self.off_v)
  1762. self.cur_h += p.width
  1763. elif isinstance(p, Kern):
  1764. self.cur_h += p.width
  1765. elif isinstance(p, List):
  1766. # node623
  1767. if len(p.children) == 0:
  1768. self.cur_h += p.width
  1769. else:
  1770. edge = self.cur_h
  1771. self.cur_v = base_line + p.shift_amount
  1772. if isinstance(p, Hlist):
  1773. self.hlist_out(p)
  1774. else:
  1775. # p.vpack(box.height + box.depth, 'exactly')
  1776. self.vlist_out(p)
  1777. self.cur_h = edge + p.width
  1778. self.cur_v = base_line
  1779. elif isinstance(p, Box):
  1780. # node624
  1781. rule_height = p.height
  1782. rule_depth = p.depth
  1783. rule_width = p.width
  1784. if np.isinf(rule_height):
  1785. rule_height = box.height
  1786. if np.isinf(rule_depth):
  1787. rule_depth = box.depth
  1788. if rule_height > 0 and rule_width > 0:
  1789. self.cur_v = base_line + rule_depth
  1790. p.render(self.cur_h + self.off_h,
  1791. self.cur_v + self.off_v,
  1792. rule_width, rule_height)
  1793. self.cur_v = base_line
  1794. self.cur_h += rule_width
  1795. elif isinstance(p, Glue):
  1796. # node625
  1797. glue_spec = p.glue_spec
  1798. rule_width = glue_spec.width - cur_g
  1799. if glue_sign != 0: # normal
  1800. if glue_sign == 1: # stretching
  1801. if glue_spec.stretch_order == glue_order:
  1802. cur_glue += glue_spec.stretch
  1803. cur_g = np.round(clamp(float(box.glue_set) * cur_glue))
  1804. elif glue_spec.shrink_order == glue_order:
  1805. cur_glue += glue_spec.shrink
  1806. cur_g = np.round(clamp(float(box.glue_set) * cur_glue))
  1807. rule_width += cur_g
  1808. self.cur_h += rule_width
  1809. self.cur_s -= 1
  1810. def vlist_out(self, box):
  1811. cur_g = 0
  1812. cur_glue = 0.
  1813. glue_order = box.glue_order
  1814. glue_sign = box.glue_sign
  1815. self.cur_s += 1
  1816. self.max_push = max(self.max_push, self.cur_s)
  1817. left_edge = self.cur_h
  1818. self.cur_v -= box.height
  1819. top_edge = self.cur_v
  1820. clamp = self.clamp
  1821. for p in box.children:
  1822. if isinstance(p, Kern):
  1823. self.cur_v += p.width
  1824. elif isinstance(p, List):
  1825. if len(p.children) == 0:
  1826. self.cur_v += p.height + p.depth
  1827. else:
  1828. self.cur_v += p.height
  1829. self.cur_h = left_edge + p.shift_amount
  1830. save_v = self.cur_v
  1831. p.width = box.width
  1832. if isinstance(p, Hlist):
  1833. self.hlist_out(p)
  1834. else:
  1835. self.vlist_out(p)
  1836. self.cur_v = save_v + p.depth
  1837. self.cur_h = left_edge
  1838. elif isinstance(p, Box):
  1839. rule_height = p.height
  1840. rule_depth = p.depth
  1841. rule_width = p.width
  1842. if np.isinf(rule_width):
  1843. rule_width = box.width
  1844. rule_height += rule_depth
  1845. if rule_height > 0 and rule_depth > 0:
  1846. self.cur_v += rule_height
  1847. p.render(self.cur_h + self.off_h,
  1848. self.cur_v + self.off_v,
  1849. rule_width, rule_height)
  1850. elif isinstance(p, Glue):
  1851. glue_spec = p.glue_spec
  1852. rule_height = glue_spec.width - cur_g
  1853. if glue_sign != 0: # normal
  1854. if glue_sign == 1: # stretching
  1855. if glue_spec.stretch_order == glue_order:
  1856. cur_glue += glue_spec.stretch
  1857. cur_g = np.round(clamp(float(box.glue_set) * cur_glue))
  1858. elif glue_spec.shrink_order == glue_order: # shrinking
  1859. cur_glue += glue_spec.shrink
  1860. cur_g = np.round(clamp(float(box.glue_set) * cur_glue))
  1861. rule_height += cur_g
  1862. self.cur_v += rule_height
  1863. elif isinstance(p, Char):
  1864. raise RuntimeError("Internal mathtext error: Char node found in vlist")
  1865. self.cur_s -= 1
  1866. ship = Ship()
  1867. ##############################################################################
  1868. # PARSER
  1869. def Error(msg):
  1870. """
  1871. Helper class to raise parser errors.
  1872. """
  1873. def raise_error(s, loc, toks):
  1874. raise ParseFatalException(s, loc, msg)
  1875. empty = Empty()
  1876. empty.setParseAction(raise_error)
  1877. return empty
  1878. class Parser(object):
  1879. """
  1880. This is the pyparsing-based parser for math expressions. It
  1881. actually parses full strings *containing* math expressions, in
  1882. that raw text may also appear outside of pairs of ``$``.
  1883. The grammar is based directly on that in TeX, though it cuts a few
  1884. corners.
  1885. """
  1886. _math_style_dict = dict(displaystyle=0, textstyle=1,
  1887. scriptstyle=2, scriptscriptstyle=3)
  1888. _binary_operators = set('''
  1889. + * -
  1890. \\pm \\sqcap \\rhd
  1891. \\mp \\sqcup \\unlhd
  1892. \\times \\vee \\unrhd
  1893. \\div \\wedge \\oplus
  1894. \\ast \\setminus \\ominus
  1895. \\star \\wr \\otimes
  1896. \\circ \\diamond \\oslash
  1897. \\bullet \\bigtriangleup \\odot
  1898. \\cdot \\bigtriangledown \\bigcirc
  1899. \\cap \\triangleleft \\dagger
  1900. \\cup \\triangleright \\ddagger
  1901. \\uplus \\lhd \\amalg'''.split())
  1902. _relation_symbols = set('''
  1903. = < > :
  1904. \\leq \\geq \\equiv \\models
  1905. \\prec \\succ \\sim \\perp
  1906. \\preceq \\succeq \\simeq \\mid
  1907. \\ll \\gg \\asymp \\parallel
  1908. \\subset \\supset \\approx \\bowtie
  1909. \\subseteq \\supseteq \\cong \\Join
  1910. \\sqsubset \\sqsupset \\neq \\smile
  1911. \\sqsubseteq \\sqsupseteq \\doteq \\frown
  1912. \\in \\ni \\propto \\vdash
  1913. \\dashv \\dots \\dotplus \\doteqdot'''.split())
  1914. _arrow_symbols = set('''
  1915. \\leftarrow \\longleftarrow \\uparrow
  1916. \\Leftarrow \\Longleftarrow \\Uparrow
  1917. \\rightarrow \\longrightarrow \\downarrow
  1918. \\Rightarrow \\Longrightarrow \\Downarrow
  1919. \\leftrightarrow \\longleftrightarrow \\updownarrow
  1920. \\Leftrightarrow \\Longleftrightarrow \\Updownarrow
  1921. \\mapsto \\longmapsto \\nearrow
  1922. \\hookleftarrow \\hookrightarrow \\searrow
  1923. \\leftharpoonup \\rightharpoonup \\swarrow
  1924. \\leftharpoondown \\rightharpoondown \\nwarrow
  1925. \\rightleftharpoons \\leadsto'''.split())
  1926. _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols
  1927. _punctuation_symbols = set(r', ; . ! \ldotp \cdotp'.split())
  1928. _overunder_symbols = set(r'''
  1929. \sum \prod \coprod \bigcap \bigcup \bigsqcup \bigvee
  1930. \bigwedge \bigodot \bigotimes \bigoplus \biguplus
  1931. '''.split())
  1932. _overunder_functions = set(
  1933. r"lim liminf limsup sup max min".split())
  1934. _dropsub_symbols = set(r'''\int \oint'''.split())
  1935. _fontnames = set("rm cal it tt sf bf default bb frak circled scr regular".split())
  1936. _function_names = set("""
  1937. arccos csc ker min arcsin deg lg Pr arctan det lim sec arg dim
  1938. liminf sin cos exp limsup sinh cosh gcd ln sup cot hom log tan
  1939. coth inf max tanh""".split())
  1940. _ambi_delim = set("""
  1941. | \\| / \\backslash \\uparrow \\downarrow \\updownarrow \\Uparrow
  1942. \\Downarrow \\Updownarrow . \\vert \\Vert \\\\|""".split())
  1943. _left_delim = set(r"( [ \{ < \lfloor \langle \lceil".split())
  1944. _right_delim = set(r") ] \} > \rfloor \rangle \rceil".split())
  1945. def __init__(self):
  1946. p = types.SimpleNamespace()
  1947. # All forward declarations are here
  1948. p.accent = Forward()
  1949. p.ambi_delim = Forward()
  1950. p.apostrophe = Forward()
  1951. p.auto_delim = Forward()
  1952. p.binom = Forward()
  1953. p.bslash = Forward()
  1954. p.c_over_c = Forward()
  1955. p.customspace = Forward()
  1956. p.end_group = Forward()
  1957. p.float_literal = Forward()
  1958. p.font = Forward()
  1959. p.frac = Forward()
  1960. p.dfrac = Forward()
  1961. p.function = Forward()
  1962. p.genfrac = Forward()
  1963. p.group = Forward()
  1964. p.int_literal = Forward()
  1965. p.latexfont = Forward()
  1966. p.lbracket = Forward()
  1967. p.left_delim = Forward()
  1968. p.lbrace = Forward()
  1969. p.main = Forward()
  1970. p.math = Forward()
  1971. p.math_string = Forward()
  1972. p.non_math = Forward()
  1973. p.operatorname = Forward()
  1974. p.overline = Forward()
  1975. p.placeable = Forward()
  1976. p.rbrace = Forward()
  1977. p.rbracket = Forward()
  1978. p.required_group = Forward()
  1979. p.right_delim = Forward()
  1980. p.right_delim_safe = Forward()
  1981. p.simple = Forward()
  1982. p.simple_group = Forward()
  1983. p.single_symbol = Forward()
  1984. p.snowflake = Forward()
  1985. p.space = Forward()
  1986. p.sqrt = Forward()
  1987. p.stackrel = Forward()
  1988. p.start_group = Forward()
  1989. p.subsuper = Forward()
  1990. p.subsuperop = Forward()
  1991. p.symbol = Forward()
  1992. p.symbol_name = Forward()
  1993. p.token = Forward()
  1994. p.unknown_symbol = Forward()
  1995. # Set names on everything -- very useful for debugging
  1996. for key, val in vars(p).items():
  1997. if not key.startswith('_'):
  1998. val.setName(key)
  1999. p.float_literal <<= Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)")
  2000. p.int_literal <<= Regex("[-+]?[0-9]+")
  2001. p.lbrace <<= Literal('{').suppress()
  2002. p.rbrace <<= Literal('}').suppress()
  2003. p.lbracket <<= Literal('[').suppress()
  2004. p.rbracket <<= Literal(']').suppress()
  2005. p.bslash <<= Literal('\\')
  2006. p.space <<= oneOf(list(self._space_widths))
  2007. p.customspace <<= (Suppress(Literal(r'\hspace'))
  2008. - ((p.lbrace + p.float_literal + p.rbrace)
  2009. | Error(r"Expected \hspace{n}")))
  2010. unicode_range = "\U00000080-\U0001ffff"
  2011. p.single_symbol <<= Regex(r"([a-zA-Z0-9 +\-*/<>=:,.;!\?&'@()\[\]|%s])|(\\[%%${}\[\]_|])" %
  2012. unicode_range)
  2013. p.snowflake <<= Suppress(p.bslash) + oneOf(self._snowflake)
  2014. p.symbol_name <<= (Combine(p.bslash + oneOf(list(tex2uni))) +
  2015. FollowedBy(Regex("[^A-Za-z]").leaveWhitespace() | StringEnd()))
  2016. p.symbol <<= (p.single_symbol | p.symbol_name).leaveWhitespace()
  2017. p.apostrophe <<= Regex("'+")
  2018. p.c_over_c <<= Suppress(p.bslash) + oneOf(list(self._char_over_chars))
  2019. p.accent <<= Group(
  2020. Suppress(p.bslash)
  2021. + oneOf([*self._accent_map, *self._wide_accents])
  2022. - p.placeable
  2023. )
  2024. p.function <<= Suppress(p.bslash) + oneOf(list(self._function_names))
  2025. p.start_group <<= Optional(p.latexfont) + p.lbrace
  2026. p.end_group <<= p.rbrace.copy()
  2027. p.simple_group <<= Group(p.lbrace + ZeroOrMore(p.token) + p.rbrace)
  2028. p.required_group<<= Group(p.lbrace + OneOrMore(p.token) + p.rbrace)
  2029. p.group <<= Group(p.start_group + ZeroOrMore(p.token) + p.end_group)
  2030. p.font <<= Suppress(p.bslash) + oneOf(list(self._fontnames))
  2031. p.latexfont <<= Suppress(p.bslash) + oneOf(['math' + x for x in self._fontnames])
  2032. p.frac <<= Group(
  2033. Suppress(Literal(r"\frac"))
  2034. - ((p.required_group + p.required_group) | Error(r"Expected \frac{num}{den}"))
  2035. )
  2036. p.dfrac <<= Group(
  2037. Suppress(Literal(r"\dfrac"))
  2038. - ((p.required_group + p.required_group) | Error(r"Expected \dfrac{num}{den}"))
  2039. )
  2040. p.stackrel <<= Group(
  2041. Suppress(Literal(r"\stackrel"))
  2042. - ((p.required_group + p.required_group) | Error(r"Expected \stackrel{num}{den}"))
  2043. )
  2044. p.binom <<= Group(
  2045. Suppress(Literal(r"\binom"))
  2046. - ((p.required_group + p.required_group) | Error(r"Expected \binom{num}{den}"))
  2047. )
  2048. p.ambi_delim <<= oneOf(list(self._ambi_delim))
  2049. p.left_delim <<= oneOf(list(self._left_delim))
  2050. p.right_delim <<= oneOf(list(self._right_delim))
  2051. p.right_delim_safe <<= oneOf([*(self._right_delim - {'}'}), r'\}'])
  2052. p.genfrac <<= Group(
  2053. Suppress(Literal(r"\genfrac"))
  2054. - (((p.lbrace + Optional(p.ambi_delim | p.left_delim, default='') + p.rbrace)
  2055. + (p.lbrace + Optional(p.ambi_delim | p.right_delim_safe, default='') + p.rbrace)
  2056. + (p.lbrace + p.float_literal + p.rbrace)
  2057. + p.simple_group + p.required_group + p.required_group)
  2058. | Error(r"Expected \genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}"))
  2059. )
  2060. p.sqrt <<= Group(
  2061. Suppress(Literal(r"\sqrt"))
  2062. - ((Optional(p.lbracket + p.int_literal + p.rbracket, default=None)
  2063. + p.required_group)
  2064. | Error("Expected \\sqrt{value}"))
  2065. )
  2066. p.overline <<= Group(
  2067. Suppress(Literal(r"\overline"))
  2068. - (p.required_group | Error("Expected \\overline{value}"))
  2069. )
  2070. p.unknown_symbol<<= Combine(p.bslash + Regex("[A-Za-z]*"))
  2071. p.operatorname <<= Group(
  2072. Suppress(Literal(r"\operatorname"))
  2073. - ((p.lbrace + ZeroOrMore(p.simple | p.unknown_symbol) + p.rbrace)
  2074. | Error("Expected \\operatorname{value}"))
  2075. )
  2076. p.placeable <<= ( p.snowflake # this needs to be before accent so named symbols
  2077. # that are prefixed with an accent name work
  2078. | p.accent # Must be before symbol as all accents are symbols
  2079. | p.symbol # Must be third to catch all named symbols and single chars not in a group
  2080. | p.c_over_c
  2081. | p.function
  2082. | p.group
  2083. | p.frac
  2084. | p.dfrac
  2085. | p.stackrel
  2086. | p.binom
  2087. | p.genfrac
  2088. | p.sqrt
  2089. | p.overline
  2090. | p.operatorname
  2091. )
  2092. p.simple <<= ( p.space
  2093. | p.customspace
  2094. | p.font
  2095. | p.subsuper
  2096. )
  2097. p.subsuperop <<= oneOf(["_", "^"])
  2098. p.subsuper <<= Group(
  2099. (Optional(p.placeable) + OneOrMore(p.subsuperop - p.placeable) + Optional(p.apostrophe))
  2100. | (p.placeable + Optional(p.apostrophe))
  2101. | p.apostrophe
  2102. )
  2103. p.token <<= ( p.simple
  2104. | p.auto_delim
  2105. | p.unknown_symbol # Must be last
  2106. )
  2107. p.auto_delim <<= (Suppress(Literal(r"\left"))
  2108. - ((p.left_delim | p.ambi_delim) | Error("Expected a delimiter"))
  2109. + Group(ZeroOrMore(p.simple | p.auto_delim))
  2110. + Suppress(Literal(r"\right"))
  2111. - ((p.right_delim | p.ambi_delim) | Error("Expected a delimiter"))
  2112. )
  2113. p.math <<= OneOrMore(p.token)
  2114. p.math_string <<= QuotedString('$', '\\', unquoteResults=False)
  2115. p.non_math <<= Regex(r"(?:(?:\\[$])|[^$])*").leaveWhitespace()
  2116. p.main <<= (p.non_math + ZeroOrMore(p.math_string + p.non_math)) + StringEnd()
  2117. # Set actions
  2118. for key, val in vars(p).items():
  2119. if not key.startswith('_'):
  2120. if hasattr(self, key):
  2121. val.setParseAction(getattr(self, key))
  2122. self._expression = p.main
  2123. self._math_expression = p.math
  2124. def parse(self, s, fonts_object, fontsize, dpi):
  2125. """
  2126. Parse expression *s* using the given *fonts_object* for
  2127. output, at the given *fontsize* and *dpi*.
  2128. Returns the parse tree of :class:`Node` instances.
  2129. """
  2130. self._state_stack = [self.State(fonts_object, 'default', 'rm', fontsize, dpi)]
  2131. self._em_width_cache = {}
  2132. try:
  2133. result = self._expression.parseString(s)
  2134. except ParseBaseException as err:
  2135. raise ValueError("\n".join(["",
  2136. err.line,
  2137. " " * (err.column - 1) + "^",
  2138. str(err)]))
  2139. self._state_stack = None
  2140. self._em_width_cache = {}
  2141. self._expression.resetCache()
  2142. return result[0]
  2143. # The state of the parser is maintained in a stack. Upon
  2144. # entering and leaving a group { } or math/non-math, the stack
  2145. # is pushed and popped accordingly. The current state always
  2146. # exists in the top element of the stack.
  2147. class State(object):
  2148. """
  2149. Stores the state of the parser.
  2150. States are pushed and popped from a stack as necessary, and
  2151. the "current" state is always at the top of the stack.
  2152. """
  2153. def __init__(self, font_output, font, font_class, fontsize, dpi):
  2154. self.font_output = font_output
  2155. self._font = font
  2156. self.font_class = font_class
  2157. self.fontsize = fontsize
  2158. self.dpi = dpi
  2159. def copy(self):
  2160. return Parser.State(
  2161. self.font_output,
  2162. self.font,
  2163. self.font_class,
  2164. self.fontsize,
  2165. self.dpi)
  2166. def _get_font(self):
  2167. return self._font
  2168. def _set_font(self, name):
  2169. if name in ('rm', 'it', 'bf'):
  2170. self.font_class = name
  2171. self._font = name
  2172. font = property(_get_font, _set_font)
  2173. def get_state(self):
  2174. """
  2175. Get the current :class:`State` of the parser.
  2176. """
  2177. return self._state_stack[-1]
  2178. def pop_state(self):
  2179. """
  2180. Pop a :class:`State` off of the stack.
  2181. """
  2182. self._state_stack.pop()
  2183. def push_state(self):
  2184. """
  2185. Push a new :class:`State` onto the stack which is just a copy
  2186. of the current state.
  2187. """
  2188. self._state_stack.append(self.get_state().copy())
  2189. def main(self, s, loc, toks):
  2190. return [Hlist(toks)]
  2191. def math_string(self, s, loc, toks):
  2192. return self._math_expression.parseString(toks[0][1:-1])
  2193. def math(self, s, loc, toks):
  2194. hlist = Hlist(toks)
  2195. self.pop_state()
  2196. return [hlist]
  2197. def non_math(self, s, loc, toks):
  2198. s = toks[0].replace(r'\$', '$')
  2199. symbols = [Char(c, self.get_state(), math=False) for c in s]
  2200. hlist = Hlist(symbols)
  2201. # We're going into math now, so set font to 'it'
  2202. self.push_state()
  2203. self.get_state().font = rcParams['mathtext.default']
  2204. return [hlist]
  2205. def _make_space(self, percentage):
  2206. # All spaces are relative to em width
  2207. state = self.get_state()
  2208. key = (state.font, state.fontsize, state.dpi)
  2209. width = self._em_width_cache.get(key)
  2210. if width is None:
  2211. metrics = state.font_output.get_metrics(
  2212. state.font, rcParams['mathtext.default'], 'm', state.fontsize, state.dpi)
  2213. width = metrics.advance
  2214. self._em_width_cache[key] = width
  2215. return Kern(width * percentage)
  2216. _space_widths = { r'\,' : 0.16667, # 3/18 em = 3 mu
  2217. r'\thinspace' : 0.16667, # 3/18 em = 3 mu
  2218. r'\/' : 0.16667, # 3/18 em = 3 mu
  2219. r'\>' : 0.22222, # 4/18 em = 4 mu
  2220. r'\:' : 0.22222, # 4/18 em = 4 mu
  2221. r'\;' : 0.27778, # 5/18 em = 5 mu
  2222. r'\ ' : 0.33333, # 6/18 em = 6 mu
  2223. r'\enspace' : 0.5, # 9/18 em = 9 mu
  2224. r'\quad' : 1, # 1 em = 18 mu
  2225. r'\qquad' : 2, # 2 em = 36 mu
  2226. r'\!' : -0.16667, # -3/18 em = -3 mu
  2227. }
  2228. def space(self, s, loc, toks):
  2229. assert len(toks)==1
  2230. num = self._space_widths[toks[0]]
  2231. box = self._make_space(num)
  2232. return [box]
  2233. def customspace(self, s, loc, toks):
  2234. return [self._make_space(float(toks[0]))]
  2235. def symbol(self, s, loc, toks):
  2236. c = toks[0]
  2237. try:
  2238. char = Char(c, self.get_state())
  2239. except ValueError:
  2240. raise ParseFatalException(s, loc, "Unknown symbol: %s" % c)
  2241. if c in self._spaced_symbols:
  2242. # iterate until we find previous character, needed for cases
  2243. # such as ${ -2}$, $ -2$, or $ -2$.
  2244. prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
  2245. # Binary operators at start of string should not be spaced
  2246. if (c in self._binary_operators and
  2247. (len(s[:loc].split()) == 0 or prev_char == '{' or
  2248. prev_char in self._left_delim)):
  2249. return [char]
  2250. else:
  2251. return [Hlist([self._make_space(0.2),
  2252. char,
  2253. self._make_space(0.2)] ,
  2254. do_kern = True)]
  2255. elif c in self._punctuation_symbols:
  2256. # Do not space commas between brackets
  2257. if c == ',':
  2258. prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
  2259. next_char = next((c for c in s[loc + 1:] if c != ' '), '')
  2260. if prev_char == '{' and next_char == '}':
  2261. return [char]
  2262. # Do not space dots as decimal separators
  2263. if c == '.' and s[loc - 1].isdigit() and s[loc + 1].isdigit():
  2264. return [char]
  2265. else:
  2266. return [Hlist([char,
  2267. self._make_space(0.2)],
  2268. do_kern = True)]
  2269. return [char]
  2270. snowflake = symbol
  2271. def unknown_symbol(self, s, loc, toks):
  2272. c = toks[0]
  2273. raise ParseFatalException(s, loc, "Unknown symbol: %s" % c)
  2274. _char_over_chars = {
  2275. # The first 2 entries in the tuple are (font, char, sizescale) for
  2276. # the two symbols under and over. The third element is the space
  2277. # (in multiples of underline height)
  2278. r'AA': (('it', 'A', 1.0), (None, '\\circ', 0.5), 0.0),
  2279. }
  2280. def c_over_c(self, s, loc, toks):
  2281. sym = toks[0]
  2282. state = self.get_state()
  2283. thickness = state.font_output.get_underline_thickness(
  2284. state.font, state.fontsize, state.dpi)
  2285. under_desc, over_desc, space = \
  2286. self._char_over_chars.get(sym, (None, None, 0.0))
  2287. if under_desc is None:
  2288. raise ParseFatalException("Error parsing symbol")
  2289. over_state = state.copy()
  2290. if over_desc[0] is not None:
  2291. over_state.font = over_desc[0]
  2292. over_state.fontsize *= over_desc[2]
  2293. over = Accent(over_desc[1], over_state)
  2294. under_state = state.copy()
  2295. if under_desc[0] is not None:
  2296. under_state.font = under_desc[0]
  2297. under_state.fontsize *= under_desc[2]
  2298. under = Char(under_desc[1], under_state)
  2299. width = max(over.width, under.width)
  2300. over_centered = HCentered([over])
  2301. over_centered.hpack(width, 'exactly')
  2302. under_centered = HCentered([under])
  2303. under_centered.hpack(width, 'exactly')
  2304. return Vlist([
  2305. over_centered,
  2306. Vbox(0., thickness * space),
  2307. under_centered
  2308. ])
  2309. _accent_map = {
  2310. r'hat' : r'\circumflexaccent',
  2311. r'breve' : r'\combiningbreve',
  2312. r'bar' : r'\combiningoverline',
  2313. r'grave' : r'\combininggraveaccent',
  2314. r'acute' : r'\combiningacuteaccent',
  2315. r'tilde' : r'\combiningtilde',
  2316. r'dot' : r'\combiningdotabove',
  2317. r'ddot' : r'\combiningdiaeresis',
  2318. r'vec' : r'\combiningrightarrowabove',
  2319. r'"' : r'\combiningdiaeresis',
  2320. r"`" : r'\combininggraveaccent',
  2321. r"'" : r'\combiningacuteaccent',
  2322. r'~' : r'\combiningtilde',
  2323. r'.' : r'\combiningdotabove',
  2324. r'^' : r'\circumflexaccent',
  2325. r'overrightarrow' : r'\rightarrow',
  2326. r'overleftarrow' : r'\leftarrow',
  2327. r'mathring' : r'\circ'
  2328. }
  2329. _wide_accents = set(r"widehat widetilde widebar".split())
  2330. # make a lambda and call it to get the namespace right
  2331. _snowflake = (lambda am: [p for p in tex2uni if
  2332. any(p.startswith(a) and a != p for a in am)]
  2333. ) (set(_accent_map))
  2334. def accent(self, s, loc, toks):
  2335. assert len(toks)==1
  2336. state = self.get_state()
  2337. thickness = state.font_output.get_underline_thickness(
  2338. state.font, state.fontsize, state.dpi)
  2339. if len(toks[0]) != 2:
  2340. raise ParseFatalException("Error parsing accent")
  2341. accent, sym = toks[0]
  2342. if accent in self._wide_accents:
  2343. accent_box = AutoWidthChar(
  2344. '\\' + accent, sym.width, state, char_class=Accent)
  2345. else:
  2346. accent_box = Accent(self._accent_map[accent], state)
  2347. if accent == 'mathring':
  2348. accent_box.shrink()
  2349. accent_box.shrink()
  2350. centered = HCentered([Hbox(sym.width / 4.0), accent_box])
  2351. centered.hpack(sym.width, 'exactly')
  2352. return Vlist([
  2353. centered,
  2354. Vbox(0., thickness * 2.0),
  2355. Hlist([sym])
  2356. ])
  2357. def function(self, s, loc, toks):
  2358. self.push_state()
  2359. state = self.get_state()
  2360. state.font = 'rm'
  2361. hlist = Hlist([Char(c, state) for c in toks[0]])
  2362. self.pop_state()
  2363. hlist.function_name = toks[0]
  2364. return hlist
  2365. def operatorname(self, s, loc, toks):
  2366. self.push_state()
  2367. state = self.get_state()
  2368. state.font = 'rm'
  2369. # Change the font of Chars, but leave Kerns alone
  2370. for c in toks[0]:
  2371. if isinstance(c, Char):
  2372. c.font = 'rm'
  2373. c._update_metrics()
  2374. self.pop_state()
  2375. return Hlist(toks[0])
  2376. def start_group(self, s, loc, toks):
  2377. self.push_state()
  2378. # Deal with LaTeX-style font tokens
  2379. if len(toks):
  2380. self.get_state().font = toks[0][4:]
  2381. return []
  2382. def group(self, s, loc, toks):
  2383. grp = Hlist(toks[0])
  2384. return [grp]
  2385. required_group = simple_group = group
  2386. def end_group(self, s, loc, toks):
  2387. self.pop_state()
  2388. return []
  2389. def font(self, s, loc, toks):
  2390. assert len(toks)==1
  2391. name = toks[0]
  2392. self.get_state().font = name
  2393. return []
  2394. def is_overunder(self, nucleus):
  2395. if isinstance(nucleus, Char):
  2396. return nucleus.c in self._overunder_symbols
  2397. elif isinstance(nucleus, Hlist) and hasattr(nucleus, 'function_name'):
  2398. return nucleus.function_name in self._overunder_functions
  2399. return False
  2400. def is_dropsub(self, nucleus):
  2401. if isinstance(nucleus, Char):
  2402. return nucleus.c in self._dropsub_symbols
  2403. return False
  2404. def is_slanted(self, nucleus):
  2405. if isinstance(nucleus, Char):
  2406. return nucleus.is_slanted()
  2407. return False
  2408. def is_between_brackets(self, s, loc):
  2409. return False
  2410. def subsuper(self, s, loc, toks):
  2411. assert len(toks)==1
  2412. nucleus = None
  2413. sub = None
  2414. super = None
  2415. # Pick all of the apostrophes out, including first apostrophes that have
  2416. # been parsed as characters
  2417. napostrophes = 0
  2418. new_toks = []
  2419. for tok in toks[0]:
  2420. if isinstance(tok, str) and tok not in ('^', '_'):
  2421. napostrophes += len(tok)
  2422. elif isinstance(tok, Char) and tok.c == "'":
  2423. napostrophes += 1
  2424. else:
  2425. new_toks.append(tok)
  2426. toks = new_toks
  2427. if len(toks) == 0:
  2428. assert napostrophes
  2429. nucleus = Hbox(0.0)
  2430. elif len(toks) == 1:
  2431. if not napostrophes:
  2432. return toks[0] # .asList()
  2433. else:
  2434. nucleus = toks[0]
  2435. elif len(toks) in (2, 3):
  2436. # single subscript or superscript
  2437. nucleus = toks[0] if len(toks) == 3 else Hbox(0.0)
  2438. op, next = toks[-2:]
  2439. if op == '_':
  2440. sub = next
  2441. else:
  2442. super = next
  2443. elif len(toks) in (4, 5):
  2444. # subscript and superscript
  2445. nucleus = toks[0] if len(toks) == 5 else Hbox(0.0)
  2446. op1, next1, op2, next2 = toks[-4:]
  2447. if op1 == op2:
  2448. if op1 == '_':
  2449. raise ParseFatalException("Double subscript")
  2450. else:
  2451. raise ParseFatalException("Double superscript")
  2452. if op1 == '_':
  2453. sub = next1
  2454. super = next2
  2455. else:
  2456. super = next1
  2457. sub = next2
  2458. else:
  2459. raise ParseFatalException(
  2460. "Subscript/superscript sequence is too long. "
  2461. "Use braces { } to remove ambiguity.")
  2462. state = self.get_state()
  2463. rule_thickness = state.font_output.get_underline_thickness(
  2464. state.font, state.fontsize, state.dpi)
  2465. xHeight = state.font_output.get_xheight(
  2466. state.font, state.fontsize, state.dpi)
  2467. if napostrophes:
  2468. if super is None:
  2469. super = Hlist([])
  2470. for i in range(napostrophes):
  2471. super.children.extend(self.symbol(s, loc, ['\\prime']))
  2472. # kern() and hpack() needed to get the metrics right after extending
  2473. super.kern()
  2474. super.hpack()
  2475. # Handle over/under symbols, such as sum or integral
  2476. if self.is_overunder(nucleus):
  2477. vlist = []
  2478. shift = 0.
  2479. width = nucleus.width
  2480. if super is not None:
  2481. super.shrink()
  2482. width = max(width, super.width)
  2483. if sub is not None:
  2484. sub.shrink()
  2485. width = max(width, sub.width)
  2486. if super is not None:
  2487. hlist = HCentered([super])
  2488. hlist.hpack(width, 'exactly')
  2489. vlist.extend([hlist, Kern(rule_thickness * 3.0)])
  2490. hlist = HCentered([nucleus])
  2491. hlist.hpack(width, 'exactly')
  2492. vlist.append(hlist)
  2493. if sub is not None:
  2494. hlist = HCentered([sub])
  2495. hlist.hpack(width, 'exactly')
  2496. vlist.extend([Kern(rule_thickness * 3.0), hlist])
  2497. shift = hlist.height
  2498. vlist = Vlist(vlist)
  2499. vlist.shift_amount = shift + nucleus.depth
  2500. result = Hlist([vlist])
  2501. return [result]
  2502. # We remove kerning on the last character for consistency (otherwise it
  2503. # will compute kerning based on non-shrinked characters and may put them
  2504. # too close together when superscripted)
  2505. # We change the width of the last character to match the advance to
  2506. # consider some fonts with weird metrics: e.g. stix's f has a width of
  2507. # 7.75 and a kerning of -4.0 for an advance of 3.72, and we want to put
  2508. # the superscript at the advance
  2509. last_char = nucleus
  2510. if isinstance(nucleus, Hlist):
  2511. new_children = nucleus.children
  2512. if len(new_children):
  2513. # remove last kern
  2514. if (isinstance(new_children[-1],Kern) and
  2515. hasattr(new_children[-2], '_metrics')):
  2516. new_children = new_children[:-1]
  2517. last_char = new_children[-1]
  2518. if hasattr(last_char, '_metrics'):
  2519. last_char.width = last_char._metrics.advance
  2520. # create new Hlist without kerning
  2521. nucleus = Hlist(new_children, do_kern=False)
  2522. else:
  2523. if isinstance(nucleus, Char):
  2524. last_char.width = last_char._metrics.advance
  2525. nucleus = Hlist([nucleus])
  2526. # Handle regular sub/superscripts
  2527. constants = _get_font_constant_set(state)
  2528. lc_height = last_char.height
  2529. lc_baseline = 0
  2530. if self.is_dropsub(last_char):
  2531. lc_baseline = last_char.depth
  2532. # Compute kerning for sub and super
  2533. superkern = constants.delta * xHeight
  2534. subkern = constants.delta * xHeight
  2535. if self.is_slanted(last_char):
  2536. superkern += constants.delta * xHeight
  2537. superkern += (constants.delta_slanted *
  2538. (lc_height - xHeight * 2. / 3.))
  2539. if self.is_dropsub(last_char):
  2540. subkern = (3 * constants.delta -
  2541. constants.delta_integral) * lc_height
  2542. superkern = (3 * constants.delta +
  2543. constants.delta_integral) * lc_height
  2544. else:
  2545. subkern = 0
  2546. if super is None:
  2547. # node757
  2548. x = Hlist([Kern(subkern), sub])
  2549. x.shrink()
  2550. if self.is_dropsub(last_char):
  2551. shift_down = lc_baseline + constants.subdrop * xHeight
  2552. else:
  2553. shift_down = constants.sub1 * xHeight
  2554. x.shift_amount = shift_down
  2555. else:
  2556. x = Hlist([Kern(superkern), super])
  2557. x.shrink()
  2558. if self.is_dropsub(last_char):
  2559. shift_up = lc_height - constants.subdrop * xHeight
  2560. else:
  2561. shift_up = constants.sup1 * xHeight
  2562. if sub is None:
  2563. x.shift_amount = -shift_up
  2564. else: # Both sub and superscript
  2565. y = Hlist([Kern(subkern),sub])
  2566. y.shrink()
  2567. if self.is_dropsub(last_char):
  2568. shift_down = lc_baseline + constants.subdrop * xHeight
  2569. else:
  2570. shift_down = constants.sub2 * xHeight
  2571. # If sub and superscript collide, move super up
  2572. clr = (2.0 * rule_thickness -
  2573. ((shift_up - x.depth) - (y.height - shift_down)))
  2574. if clr > 0.:
  2575. shift_up += clr
  2576. x = Vlist([x,
  2577. Kern((shift_up - x.depth) - (y.height - shift_down)),
  2578. y])
  2579. x.shift_amount = shift_down
  2580. if not self.is_dropsub(last_char):
  2581. x.width += constants.script_space * xHeight
  2582. result = Hlist([nucleus, x])
  2583. return [result]
  2584. def _genfrac(self, ldelim, rdelim, rule, style, num, den):
  2585. state = self.get_state()
  2586. thickness = state.font_output.get_underline_thickness(
  2587. state.font, state.fontsize, state.dpi)
  2588. rule = float(rule)
  2589. # If style != displaystyle == 0, shrink the num and den
  2590. if style != self._math_style_dict['displaystyle']:
  2591. num.shrink()
  2592. den.shrink()
  2593. cnum = HCentered([num])
  2594. cden = HCentered([den])
  2595. width = max(num.width, den.width)
  2596. cnum.hpack(width, 'exactly')
  2597. cden.hpack(width, 'exactly')
  2598. vlist = Vlist([cnum, # numerator
  2599. Vbox(0, thickness * 2.0), # space
  2600. Hrule(state, rule), # rule
  2601. Vbox(0, thickness * 2.0), # space
  2602. cden # denominator
  2603. ])
  2604. # Shift so the fraction line sits in the middle of the
  2605. # equals sign
  2606. metrics = state.font_output.get_metrics(
  2607. state.font, rcParams['mathtext.default'],
  2608. '=', state.fontsize, state.dpi)
  2609. shift = (cden.height -
  2610. ((metrics.ymax + metrics.ymin) / 2 -
  2611. thickness * 3.0))
  2612. vlist.shift_amount = shift
  2613. result = [Hlist([vlist, Hbox(thickness * 2.)])]
  2614. if ldelim or rdelim:
  2615. if ldelim == '':
  2616. ldelim = '.'
  2617. if rdelim == '':
  2618. rdelim = '.'
  2619. return self._auto_sized_delimiter(ldelim, result, rdelim)
  2620. return result
  2621. def genfrac(self, s, loc, toks):
  2622. assert len(toks) == 1
  2623. assert len(toks[0]) == 6
  2624. return self._genfrac(*tuple(toks[0]))
  2625. def frac(self, s, loc, toks):
  2626. assert len(toks) == 1
  2627. assert len(toks[0]) == 2
  2628. state = self.get_state()
  2629. thickness = state.font_output.get_underline_thickness(
  2630. state.font, state.fontsize, state.dpi)
  2631. num, den = toks[0]
  2632. return self._genfrac('', '', thickness,
  2633. self._math_style_dict['textstyle'], num, den)
  2634. def dfrac(self, s, loc, toks):
  2635. assert len(toks) == 1
  2636. assert len(toks[0]) == 2
  2637. state = self.get_state()
  2638. thickness = state.font_output.get_underline_thickness(
  2639. state.font, state.fontsize, state.dpi)
  2640. num, den = toks[0]
  2641. return self._genfrac('', '', thickness,
  2642. self._math_style_dict['displaystyle'], num, den)
  2643. def stackrel(self, s, loc, toks):
  2644. assert len(toks) == 1
  2645. assert len(toks[0]) == 2
  2646. num, den = toks[0]
  2647. return self._genfrac('', '', 0.0,
  2648. self._math_style_dict['textstyle'], num, den)
  2649. def binom(self, s, loc, toks):
  2650. assert len(toks) == 1
  2651. assert len(toks[0]) == 2
  2652. num, den = toks[0]
  2653. return self._genfrac('(', ')', 0.0,
  2654. self._math_style_dict['textstyle'], num, den)
  2655. def sqrt(self, s, loc, toks):
  2656. root, body = toks[0]
  2657. state = self.get_state()
  2658. thickness = state.font_output.get_underline_thickness(
  2659. state.font, state.fontsize, state.dpi)
  2660. # Determine the height of the body, and add a little extra to
  2661. # the height so it doesn't seem cramped
  2662. height = body.height - body.shift_amount + thickness * 5.0
  2663. depth = body.depth + body.shift_amount
  2664. check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True)
  2665. height = check.height - check.shift_amount
  2666. depth = check.depth + check.shift_amount
  2667. # Put a little extra space to the left and right of the body
  2668. padded_body = Hlist([Hbox(thickness * 2.0),
  2669. body,
  2670. Hbox(thickness * 2.0)])
  2671. rightside = Vlist([Hrule(state),
  2672. Fill(),
  2673. padded_body])
  2674. # Stretch the glue between the hrule and the body
  2675. rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
  2676. 'exactly', depth)
  2677. # Add the root and shift it upward so it is above the tick.
  2678. # The value of 0.6 is a hard-coded hack ;)
  2679. if root is None:
  2680. root = Box(check.width * 0.5, 0., 0.)
  2681. else:
  2682. root = Hlist([Char(x, state) for x in root])
  2683. root.shrink()
  2684. root.shrink()
  2685. root_vlist = Vlist([Hlist([root])])
  2686. root_vlist.shift_amount = -height * 0.6
  2687. hlist = Hlist([root_vlist, # Root
  2688. # Negative kerning to put root over tick
  2689. Kern(-check.width * 0.5),
  2690. check, # Check
  2691. rightside]) # Body
  2692. return [hlist]
  2693. def overline(self, s, loc, toks):
  2694. assert len(toks)==1
  2695. assert len(toks[0])==1
  2696. body = toks[0][0]
  2697. state = self.get_state()
  2698. thickness = state.font_output.get_underline_thickness(
  2699. state.font, state.fontsize, state.dpi)
  2700. height = body.height - body.shift_amount + thickness * 3.0
  2701. depth = body.depth + body.shift_amount
  2702. # Place overline above body
  2703. rightside = Vlist([Hrule(state),
  2704. Fill(),
  2705. Hlist([body])])
  2706. # Stretch the glue between the hrule and the body
  2707. rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
  2708. 'exactly', depth)
  2709. hlist = Hlist([rightside])
  2710. return [hlist]
  2711. def _auto_sized_delimiter(self, front, middle, back):
  2712. state = self.get_state()
  2713. if len(middle):
  2714. height = max(x.height for x in middle)
  2715. depth = max(x.depth for x in middle)
  2716. factor = None
  2717. else:
  2718. height = 0
  2719. depth = 0
  2720. factor = 1.0
  2721. parts = []
  2722. # \left. and \right. aren't supposed to produce any symbols
  2723. if front != '.':
  2724. parts.append(AutoHeightChar(front, height, depth, state, factor=factor))
  2725. parts.extend(middle)
  2726. if back != '.':
  2727. parts.append(AutoHeightChar(back, height, depth, state, factor=factor))
  2728. hlist = Hlist(parts)
  2729. return hlist
  2730. def auto_delim(self, s, loc, toks):
  2731. front, middle, back = toks
  2732. return self._auto_sized_delimiter(front, middle.asList(), back)
  2733. ###
  2734. ##############################################################################
  2735. # MAIN
  2736. class MathTextParser(object):
  2737. _parser = None
  2738. _backend_mapping = {
  2739. 'bitmap': MathtextBackendBitmap,
  2740. 'agg' : MathtextBackendAgg,
  2741. 'ps' : MathtextBackendPs,
  2742. 'pdf' : MathtextBackendPdf,
  2743. 'svg' : MathtextBackendSvg,
  2744. 'path' : MathtextBackendPath,
  2745. 'cairo' : MathtextBackendCairo,
  2746. 'macosx': MathtextBackendAgg,
  2747. }
  2748. _font_type_mapping = {
  2749. 'cm' : BakomaFonts,
  2750. 'dejavuserif' : DejaVuSerifFonts,
  2751. 'dejavusans' : DejaVuSansFonts,
  2752. 'stix' : StixFonts,
  2753. 'stixsans' : StixSansFonts,
  2754. 'custom' : UnicodeFonts
  2755. }
  2756. def __init__(self, output):
  2757. """
  2758. Create a MathTextParser for the given backend *output*.
  2759. """
  2760. self._output = output.lower()
  2761. @functools.lru_cache(50)
  2762. def parse(self, s, dpi = 72, prop = None):
  2763. """
  2764. Parse the given math expression *s* at the given *dpi*. If
  2765. *prop* is provided, it is a
  2766. :class:`~matplotlib.font_manager.FontProperties` object
  2767. specifying the "default" font to use in the math expression,
  2768. used for all non-math text.
  2769. The results are cached, so multiple calls to :meth:`parse`
  2770. with the same expression should be fast.
  2771. """
  2772. if prop is None:
  2773. prop = FontProperties()
  2774. if self._output == 'ps' and rcParams['ps.useafm']:
  2775. font_output = StandardPsFonts(prop)
  2776. else:
  2777. backend = self._backend_mapping[self._output]()
  2778. fontset = rcParams['mathtext.fontset']
  2779. fontset_class = self._font_type_mapping.get(fontset.lower())
  2780. if fontset_class is not None:
  2781. font_output = fontset_class(prop, backend)
  2782. else:
  2783. raise ValueError(
  2784. "mathtext.fontset must be either 'cm', 'dejavuserif', "
  2785. "'dejavusans', 'stix', 'stixsans', or 'custom'")
  2786. fontsize = prop.get_size_in_points()
  2787. # This is a class variable so we don't rebuild the parser
  2788. # with each request.
  2789. if self._parser is None:
  2790. self.__class__._parser = Parser()
  2791. box = self._parser.parse(s, font_output, fontsize, dpi)
  2792. font_output.set_canvas_size(box.width, box.height, box.depth)
  2793. return font_output.get_results(box)
  2794. def to_mask(self, texstr, dpi=120, fontsize=14):
  2795. """
  2796. *texstr*
  2797. A valid mathtext string, e.g., r'IQ: $\\sigma_i=15$'
  2798. *dpi*
  2799. The dots-per-inch to render the text
  2800. *fontsize*
  2801. The font size in points
  2802. Returns a tuple (*array*, *depth*)
  2803. - *array* is an NxM uint8 alpha ubyte mask array of
  2804. rasterized tex.
  2805. - depth is the offset of the baseline from the bottom of the
  2806. image in pixels.
  2807. """
  2808. assert self._output == "bitmap"
  2809. prop = FontProperties(size=fontsize)
  2810. ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop)
  2811. x = ftimage.as_array()
  2812. return x, depth
  2813. def to_rgba(self, texstr, color='black', dpi=120, fontsize=14):
  2814. """
  2815. *texstr*
  2816. A valid mathtext string, e.g., r'IQ: $\\sigma_i=15$'
  2817. *color*
  2818. Any matplotlib color argument
  2819. *dpi*
  2820. The dots-per-inch to render the text
  2821. *fontsize*
  2822. The font size in points
  2823. Returns a tuple (*array*, *depth*)
  2824. - *array* is an NxM uint8 alpha ubyte mask array of
  2825. rasterized tex.
  2826. - depth is the offset of the baseline from the bottom of the
  2827. image in pixels.
  2828. """
  2829. x, depth = self.to_mask(texstr, dpi=dpi, fontsize=fontsize)
  2830. r, g, b, a = mcolors.to_rgba(color)
  2831. RGBA = np.zeros((x.shape[0], x.shape[1], 4), dtype=np.uint8)
  2832. RGBA[:, :, 0] = 255 * r
  2833. RGBA[:, :, 1] = 255 * g
  2834. RGBA[:, :, 2] = 255 * b
  2835. RGBA[:, :, 3] = x
  2836. return RGBA, depth
  2837. def to_png(self, filename, texstr, color='black', dpi=120, fontsize=14):
  2838. """
  2839. Writes a tex expression to a PNG file.
  2840. Returns the offset of the baseline from the bottom of the
  2841. image in pixels.
  2842. *filename*
  2843. A writable filename or fileobject
  2844. *texstr*
  2845. A valid mathtext string, e.g., r'IQ: $\\sigma_i=15$'
  2846. *color*
  2847. A valid matplotlib color argument
  2848. *dpi*
  2849. The dots-per-inch to render the text
  2850. *fontsize*
  2851. The font size in points
  2852. Returns the offset of the baseline from the bottom of the
  2853. image in pixels.
  2854. """
  2855. rgba, depth = self.to_rgba(texstr, color=color, dpi=dpi, fontsize=fontsize)
  2856. _png.write_png(rgba, filename)
  2857. return depth
  2858. def get_depth(self, texstr, dpi=120, fontsize=14):
  2859. """
  2860. Returns the offset of the baseline from the bottom of the
  2861. image in pixels.
  2862. *texstr*
  2863. A valid mathtext string, e.g., r'IQ: $\\sigma_i=15$'
  2864. *dpi*
  2865. The dots-per-inch to render the text
  2866. *fontsize*
  2867. The font size in points
  2868. """
  2869. assert self._output=="bitmap"
  2870. prop = FontProperties(size=fontsize)
  2871. ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop)
  2872. return depth
  2873. def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None):
  2874. """
  2875. Given a math expression, renders it in a closely-clipped bounding
  2876. box to an image file.
  2877. *s*
  2878. A math expression. The math portion should be enclosed in
  2879. dollar signs.
  2880. *filename_or_obj*
  2881. A filepath or writable file-like object to write the image data
  2882. to.
  2883. *prop*
  2884. If provided, a FontProperties() object describing the size and
  2885. style of the text.
  2886. *dpi*
  2887. Override the output dpi, otherwise use the default associated
  2888. with the output format.
  2889. *format*
  2890. The output format, e.g., 'svg', 'pdf', 'ps' or 'png'. If not
  2891. provided, will be deduced from the filename.
  2892. """
  2893. from matplotlib import figure
  2894. # backend_agg supports all of the core output formats
  2895. from matplotlib.backends import backend_agg
  2896. if prop is None:
  2897. prop = FontProperties()
  2898. parser = MathTextParser('path')
  2899. width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
  2900. fig = figure.Figure(figsize=(width / 72.0, height / 72.0))
  2901. fig.text(0, depth/height, s, fontproperties=prop)
  2902. backend_agg.FigureCanvasAgg(fig)
  2903. fig.savefig(filename_or_obj, dpi=dpi, format=format)
  2904. return depth