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

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