Coverage for /usr/lib/python3/dist-packages/matplotlib/_mathtext.py: 55%
1258 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2Implementation details for :mod:`.mathtext`.
3"""
5import copy
6from collections import namedtuple
7import enum
8import functools
9import logging
10import os
11import re
12import types
13import unicodedata
15import numpy as np
16from pyparsing import (
17 Empty, Forward, Literal, NotAny, oneOf, OneOrMore, Optional,
18 ParseBaseException, ParseExpression, ParseFatalException, ParserElement,
19 ParseResults, QuotedString, Regex, StringEnd, ZeroOrMore, pyparsing_common)
21import matplotlib as mpl
22from . import _api, cbook
23from ._mathtext_data import (
24 latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni)
25from .font_manager import FontProperties, findfont, get_font
26from .ft2font import FT2Image, KERNING_DEFAULT
29ParserElement.enablePackrat()
30_log = logging.getLogger("matplotlib.mathtext")
33##############################################################################
34# FONTS
37@_api.delete_parameter("3.6", "math")
38def get_unicode_index(symbol, math=False): # Publicly exported.
39 r"""
40 Return the integer index (from the Unicode table) of *symbol*.
42 Parameters
43 ----------
44 symbol : str
45 A single (Unicode) character, a TeX command (e.g. r'\pi') or a Type1
46 symbol name (e.g. 'phi').
47 math : bool, default: False
48 If True (deprecated), replace ASCII hyphen-minus by Unicode minus.
49 """
50 # From UTF #25: U+2212 minus sign is the preferred
51 # representation of the unary and binary minus sign rather than
52 # the ASCII-derived U+002D hyphen-minus, because minus sign is
53 # unambiguous and because it is rendered with a more desirable
54 # length, usually longer than a hyphen.
55 # Remove this block when the 'math' parameter is deleted.
56 if math and symbol == '-':
57 return 0x2212
58 try: # This will succeed if symbol is a single Unicode char
59 return ord(symbol)
60 except TypeError:
61 pass
62 try: # Is symbol a TeX symbol (i.e. \alpha)
63 return tex2uni[symbol.strip("\\")]
64 except KeyError as err:
65 raise ValueError(
66 "'{}' is not a valid Unicode character or TeX/Type1 symbol"
67 .format(symbol)) from err
70VectorParse = namedtuple("VectorParse", "width height depth glyphs rects",
71 module="matplotlib.mathtext")
72VectorParse.__doc__ = r"""
73The namedtuple type returned by ``MathTextParser("path").parse(...)``.
75This tuple contains the global metrics (*width*, *height*, *depth*), a list of
76*glyphs* (including their positions) and of *rect*\angles.
77"""
80RasterParse = namedtuple("RasterParse", "ox oy width height depth image",
81 module="matplotlib.mathtext")
82RasterParse.__doc__ = r"""
83The namedtuple type returned by ``MathTextParser("agg").parse(...)``.
85This tuple contains the global metrics (*width*, *height*, *depth*), and a
86raster *image*. The offsets *ox*, *oy* are always zero.
87"""
90class Output:
91 r"""
92 Result of `ship`\ping a box: lists of positioned glyphs and rectangles.
94 This class is not exposed to end users, but converted to a `VectorParse` or
95 a `RasterParse` by `.MathTextParser.parse`.
96 """
98 def __init__(self, box):
99 self.box = box
100 self.glyphs = [] # (ox, oy, info)
101 self.rects = [] # (x1, y1, x2, y2)
103 def to_vector(self):
104 w, h, d = map(
105 np.ceil, [self.box.width, self.box.height, self.box.depth])
106 gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset)
107 for ox, oy, info in self.glyphs]
108 rs = [(x1, h - y2, x2 - x1, y2 - y1)
109 for x1, y1, x2, y2 in self.rects]
110 return VectorParse(w, h + d, d, gs, rs)
112 def to_raster(self):
113 # Metrics y's and mathtext y's are oriented in opposite directions,
114 # hence the switch between ymin and ymax.
115 xmin = min([*[ox + info.metrics.xmin for ox, oy, info in self.glyphs],
116 *[x1 for x1, y1, x2, y2 in self.rects], 0]) - 1
117 ymin = min([*[oy - info.metrics.ymax for ox, oy, info in self.glyphs],
118 *[y1 for x1, y1, x2, y2 in self.rects], 0]) - 1
119 xmax = max([*[ox + info.metrics.xmax for ox, oy, info in self.glyphs],
120 *[x2 for x1, y1, x2, y2 in self.rects], 0]) + 1
121 ymax = max([*[oy - info.metrics.ymin for ox, oy, info in self.glyphs],
122 *[y2 for x1, y1, x2, y2 in self.rects], 0]) + 1
123 w = xmax - xmin
124 h = ymax - ymin - self.box.depth
125 d = ymax - ymin - self.box.height
126 image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0)))
128 # Ideally, we could just use self.glyphs and self.rects here, shifting
129 # their coordinates by (-xmin, -ymin), but this yields slightly
130 # different results due to floating point slop; shipping twice is the
131 # old approach and keeps baseline images backcompat.
132 shifted = ship(self.box, (-xmin, -ymin))
134 for ox, oy, info in shifted.glyphs:
135 info.font.draw_glyph_to_bitmap(
136 image, ox, oy - info.metrics.iceberg, info.glyph,
137 antialiased=mpl.rcParams['text.antialiased'])
138 for x1, y1, x2, y2 in shifted.rects:
139 height = max(int(y2 - y1) - 1, 0)
140 if height == 0:
141 center = (y2 + y1) / 2
142 y = int(center - (height + 1) / 2)
143 else:
144 y = int(y1)
145 image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height)
146 return RasterParse(0, 0, w, h + d, d, image)
149class Fonts:
150 """
151 An abstract base class for a system of fonts to use for mathtext.
153 The class must be able to take symbol keys and font file names and
154 return the character metrics. It also delegates to a backend class
155 to do the actual drawing.
156 """
158 def __init__(self, default_font_prop, load_glyph_flags):
159 """
160 Parameters
161 ----------
162 default_font_prop : `~.font_manager.FontProperties`
163 The default non-math font, or the base font for Unicode (generic)
164 font rendering.
165 load_glyph_flags : int
166 Flags passed to the glyph loader (e.g. ``FT_Load_Glyph`` and
167 ``FT_Load_Char`` for FreeType-based fonts).
168 """
169 self.default_font_prop = default_font_prop
170 self.load_glyph_flags = load_glyph_flags
172 def get_kern(self, font1, fontclass1, sym1, fontsize1,
173 font2, fontclass2, sym2, fontsize2, dpi):
174 """
175 Get the kerning distance for font between *sym1* and *sym2*.
177 See `~.Fonts.get_metrics` for a detailed description of the parameters.
178 """
179 return 0.
181 def get_metrics(self, font, font_class, sym, fontsize, dpi):
182 r"""
183 Parameters
184 ----------
185 font : str
186 One of the TeX font names: "tt", "it", "rm", "cal", "sf", "bf",
187 "default", "regular", "bb", "frak", "scr". "default" and "regular"
188 are synonyms and use the non-math font.
189 font_class : str
190 One of the TeX font names (as for *font*), but **not** "bb",
191 "frak", or "scr". This is used to combine two font classes. The
192 only supported combination currently is ``get_metrics("frak", "bf",
193 ...)``.
194 sym : str
195 A symbol in raw TeX form, e.g., "1", "x", or "\sigma".
196 fontsize : float
197 Font size in points.
198 dpi : float
199 Rendering dots-per-inch.
201 Returns
202 -------
203 object
205 The returned object has the following attributes (all floats,
206 except *slanted*):
208 - *advance*: The advance distance (in points) of the glyph.
209 - *height*: The height of the glyph in points.
210 - *width*: The width of the glyph in points.
211 - *xmin*, *xmax*, *ymin*, *ymax*: The ink rectangle of the glyph
212 - *iceberg*: The distance from the baseline to the top of the
213 glyph. (This corresponds to TeX's definition of "height".)
214 - *slanted*: Whether the glyph should be considered as "slanted"
215 (currently used for kerning sub/superscripts).
216 """
217 info = self._get_info(font, font_class, sym, fontsize, dpi)
218 return info.metrics
220 def render_glyph(
221 self, output, ox, oy, font, font_class, sym, fontsize, dpi):
222 """
223 At position (*ox*, *oy*), draw the glyph specified by the remaining
224 parameters (see `get_metrics` for their detailed description).
225 """
226 info = self._get_info(font, font_class, sym, fontsize, dpi)
227 output.glyphs.append((ox, oy, info))
229 def render_rect_filled(self, output, x1, y1, x2, y2):
230 """
231 Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*).
232 """
233 output.rects.append((x1, y1, x2, y2))
235 def get_xheight(self, font, fontsize, dpi):
236 """
237 Get the xheight for the given *font* and *fontsize*.
238 """
239 raise NotImplementedError()
241 def get_underline_thickness(self, font, fontsize, dpi):
242 """
243 Get the line thickness that matches the given font. Used as a
244 base unit for drawing lines such as in a fraction or radical.
245 """
246 raise NotImplementedError()
248 def get_used_characters(self):
249 """
250 Get the set of characters that were used in the math
251 expression. Used by backends that need to subset fonts so
252 they know which glyphs to include.
253 """
254 return self.used_characters
256 def get_sized_alternatives_for_symbol(self, fontname, sym):
257 """
258 Override if your font provides multiple sizes of the same
259 symbol. Should return a list of symbols matching *sym* in
260 various sizes. The expression renderer will select the most
261 appropriate size for a given situation from this list.
262 """
263 return [(fontname, sym)]
266class TruetypeFonts(Fonts):
267 """
268 A generic base class for all font setups that use Truetype fonts
269 (through FT2Font).
270 """
272 def __init__(self, *args, **kwargs):
273 super().__init__(*args, **kwargs)
274 # Per-instance cache.
275 self._get_info = functools.lru_cache(None)(self._get_info)
276 self._fonts = {}
278 filename = findfont(self.default_font_prop)
279 default_font = get_font(filename)
280 self._fonts['default'] = default_font
281 self._fonts['regular'] = default_font
283 def _get_font(self, font):
284 if font in self.fontmap:
285 basename = self.fontmap[font]
286 else:
287 basename = font
288 cached_font = self._fonts.get(basename)
289 if cached_font is None and os.path.exists(basename):
290 cached_font = get_font(basename)
291 self._fonts[basename] = cached_font
292 self._fonts[cached_font.postscript_name] = cached_font
293 self._fonts[cached_font.postscript_name.lower()] = cached_font
294 return cached_font
296 def _get_offset(self, font, glyph, fontsize, dpi):
297 if font.postscript_name == 'Cmex10':
298 return (glyph.height / 64 / 2) + (fontsize/3 * dpi/72)
299 return 0.
301 # The return value of _get_info is cached per-instance.
302 def _get_info(self, fontname, font_class, sym, fontsize, dpi):
303 font, num, slanted = self._get_glyph(fontname, font_class, sym)
304 font.set_size(fontsize, dpi)
305 glyph = font.load_char(num, flags=self.load_glyph_flags)
307 xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox]
308 offset = self._get_offset(font, glyph, fontsize, dpi)
309 metrics = types.SimpleNamespace(
310 advance = glyph.linearHoriAdvance/65536.0,
311 height = glyph.height/64.0,
312 width = glyph.width/64.0,
313 xmin = xmin,
314 xmax = xmax,
315 ymin = ymin+offset,
316 ymax = ymax+offset,
317 # iceberg is the equivalent of TeX's "height"
318 iceberg = glyph.horiBearingY/64.0 + offset,
319 slanted = slanted
320 )
322 return types.SimpleNamespace(
323 font = font,
324 fontsize = fontsize,
325 postscript_name = font.postscript_name,
326 metrics = metrics,
327 num = num,
328 glyph = glyph,
329 offset = offset
330 )
332 def get_xheight(self, fontname, fontsize, dpi):
333 font = self._get_font(fontname)
334 font.set_size(fontsize, dpi)
335 pclt = font.get_sfnt_table('pclt')
336 if pclt is None:
337 # Some fonts don't store the xHeight, so we do a poor man's xHeight
338 metrics = self.get_metrics(
339 fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi)
340 return metrics.iceberg
341 xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0)
342 return xHeight
344 def get_underline_thickness(self, font, fontsize, dpi):
345 # This function used to grab underline thickness from the font
346 # metrics, but that information is just too un-reliable, so it
347 # is now hardcoded.
348 return ((0.75 / 12.0) * fontsize * dpi) / 72.0
350 def get_kern(self, font1, fontclass1, sym1, fontsize1,
351 font2, fontclass2, sym2, fontsize2, dpi):
352 if font1 == font2 and fontsize1 == fontsize2:
353 info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
354 info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
355 font = info1.font
356 return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64
357 return super().get_kern(font1, fontclass1, sym1, fontsize1,
358 font2, fontclass2, sym2, fontsize2, dpi)
361class BakomaFonts(TruetypeFonts):
362 """
363 Use the Bakoma TrueType fonts for rendering.
365 Symbols are strewn about a number of font files, each of which has
366 its own proprietary 8-bit encoding.
367 """
368 _fontmap = {
369 'cal': 'cmsy10',
370 'rm': 'cmr10',
371 'tt': 'cmtt10',
372 'it': 'cmmi10',
373 'bf': 'cmb10',
374 'sf': 'cmss10',
375 'ex': 'cmex10',
376 }
378 def __init__(self, *args, **kwargs):
379 self._stix_fallback = StixFonts(*args, **kwargs)
381 super().__init__(*args, **kwargs)
382 self.fontmap = {}
383 for key, val in self._fontmap.items():
384 fullpath = findfont(val)
385 self.fontmap[key] = fullpath
386 self.fontmap[val] = fullpath
388 _slanted_symbols = set(r"\int \oint".split())
390 def _get_glyph(self, fontname, font_class, sym):
391 font = None
392 if fontname in self.fontmap and sym in latex_to_bakoma:
393 basename, num = latex_to_bakoma[sym]
394 slanted = (basename == "cmmi10") or sym in self._slanted_symbols
395 font = self._get_font(basename)
396 elif len(sym) == 1:
397 slanted = (fontname == "it")
398 font = self._get_font(fontname)
399 if font is not None:
400 num = ord(sym)
401 if font is not None and font.get_char_index(num) != 0:
402 return font, num, slanted
403 else:
404 return self._stix_fallback._get_glyph(fontname, font_class, sym)
406 # The Bakoma fonts contain many pre-sized alternatives for the
407 # delimiters. The AutoSizedChar class will use these alternatives
408 # and select the best (closest sized) glyph.
409 _size_alternatives = {
410 '(': [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
411 ('ex', '\xb5'), ('ex', '\xc3')],
412 ')': [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
413 ('ex', '\xb6'), ('ex', '\x21')],
414 '{': [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'),
415 ('ex', '\xbd'), ('ex', '\x28')],
416 '}': [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'),
417 ('ex', '\xbe'), ('ex', '\x29')],
418 # The fourth size of '[' is mysteriously missing from the BaKoMa
419 # font, so I've omitted it for both '[' and ']'
420 '[': [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'),
421 ('ex', '\x22')],
422 ']': [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'),
423 ('ex', '\x23')],
424 r'\lfloor': [('ex', '\xa5'), ('ex', '\x6a'),
425 ('ex', '\xb9'), ('ex', '\x24')],
426 r'\rfloor': [('ex', '\xa6'), ('ex', '\x6b'),
427 ('ex', '\xba'), ('ex', '\x25')],
428 r'\lceil': [('ex', '\xa7'), ('ex', '\x6c'),
429 ('ex', '\xbb'), ('ex', '\x26')],
430 r'\rceil': [('ex', '\xa8'), ('ex', '\x6d'),
431 ('ex', '\xbc'), ('ex', '\x27')],
432 r'\langle': [('ex', '\xad'), ('ex', '\x44'),
433 ('ex', '\xbf'), ('ex', '\x2a')],
434 r'\rangle': [('ex', '\xae'), ('ex', '\x45'),
435 ('ex', '\xc0'), ('ex', '\x2b')],
436 r'\__sqrt__': [('ex', '\x70'), ('ex', '\x71'),
437 ('ex', '\x72'), ('ex', '\x73')],
438 r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'),
439 ('ex', '\xc2'), ('ex', '\x2d')],
440 r'/': [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'),
441 ('ex', '\xcb'), ('ex', '\x2c')],
442 r'\widehat': [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'),
443 ('ex', '\x64')],
444 r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'),
445 ('ex', '\x67')],
446 r'<': [('cal', 'h'), ('ex', 'D')],
447 r'>': [('cal', 'i'), ('ex', 'E')]
448 }
450 for alias, target in [(r'\leftparen', '('),
451 (r'\rightparent', ')'),
452 (r'\leftbrace', '{'),
453 (r'\rightbrace', '}'),
454 (r'\leftbracket', '['),
455 (r'\rightbracket', ']'),
456 (r'\{', '{'),
457 (r'\}', '}'),
458 (r'\[', '['),
459 (r'\]', ']')]:
460 _size_alternatives[alias] = _size_alternatives[target]
462 def get_sized_alternatives_for_symbol(self, fontname, sym):
463 return self._size_alternatives.get(sym, [(fontname, sym)])
466class UnicodeFonts(TruetypeFonts):
467 """
468 An abstract base class for handling Unicode fonts.
470 While some reasonably complete Unicode fonts (such as DejaVu) may
471 work in some situations, the only Unicode font I'm aware of with a
472 complete set of math symbols is STIX.
474 This class will "fallback" on the Bakoma fonts when a required
475 symbol can not be found in the font.
476 """
478 def __init__(self, *args, **kwargs):
479 # This must come first so the backend's owner is set correctly
480 fallback_rc = mpl.rcParams['mathtext.fallback']
481 font_cls = {'stix': StixFonts,
482 'stixsans': StixSansFonts,
483 'cm': BakomaFonts
484 }.get(fallback_rc)
485 self._fallback_font = font_cls(*args, **kwargs) if font_cls else None
487 super().__init__(*args, **kwargs)
488 self.fontmap = {}
489 for texfont in "cal rm tt it bf sf".split():
490 prop = mpl.rcParams['mathtext.' + texfont]
491 font = findfont(prop)
492 self.fontmap[texfont] = font
493 prop = FontProperties('cmex10')
494 font = findfont(prop)
495 self.fontmap['ex'] = font
497 # include STIX sized alternatives for glyphs if fallback is STIX
498 if isinstance(self._fallback_font, StixFonts):
499 stixsizedaltfonts = {
500 0: 'STIXGeneral',
501 1: 'STIXSizeOneSym',
502 2: 'STIXSizeTwoSym',
503 3: 'STIXSizeThreeSym',
504 4: 'STIXSizeFourSym',
505 5: 'STIXSizeFiveSym'}
507 for size, name in stixsizedaltfonts.items():
508 fullpath = findfont(name)
509 self.fontmap[size] = fullpath
510 self.fontmap[name] = fullpath
512 _slanted_symbols = set(r"\int \oint".split())
514 def _map_virtual_font(self, fontname, font_class, uniindex):
515 return fontname, uniindex
517 def _get_glyph(self, fontname, font_class, sym):
518 try:
519 uniindex = get_unicode_index(sym)
520 found_symbol = True
521 except ValueError:
522 uniindex = ord('?')
523 found_symbol = False
524 _log.warning("No TeX to Unicode mapping for {!a}.".format(sym))
526 fontname, uniindex = self._map_virtual_font(
527 fontname, font_class, uniindex)
529 new_fontname = fontname
531 # Only characters in the "Letter" class should be italicized in 'it'
532 # mode. Greek capital letters should be Roman.
533 if found_symbol:
534 if fontname == 'it' and uniindex < 0x10000:
535 char = chr(uniindex)
536 if (unicodedata.category(char)[0] != "L"
537 or unicodedata.name(char).startswith("GREEK CAPITAL")):
538 new_fontname = 'rm'
540 slanted = (new_fontname == 'it') or sym in self._slanted_symbols
541 found_symbol = False
542 font = self._get_font(new_fontname)
543 if font is not None:
544 if font.family_name == "cmr10" and uniindex == 0x2212:
545 # minus sign exists in cmsy10 (not cmr10)
546 font = get_font(
547 cbook._get_data_path("fonts/ttf/cmsy10.ttf"))
548 uniindex = 0xa1
549 glyphindex = font.get_char_index(uniindex)
550 if glyphindex != 0:
551 found_symbol = True
553 if not found_symbol:
554 if self._fallback_font:
555 if (fontname in ('it', 'regular')
556 and isinstance(self._fallback_font, StixFonts)):
557 fontname = 'rm'
559 g = self._fallback_font._get_glyph(fontname, font_class, sym)
560 family = g[0].family_name
561 if family in list(BakomaFonts._fontmap.values()):
562 family = "Computer Modern"
563 _log.info("Substituting symbol %s from %s", sym, family)
564 return g
566 else:
567 if (fontname in ('it', 'regular')
568 and isinstance(self, StixFonts)):
569 return self._get_glyph('rm', font_class, sym)
570 _log.warning("Font {!r} does not have a glyph for {!a} "
571 "[U+{:x}], substituting with a dummy "
572 "symbol.".format(new_fontname, sym, uniindex))
573 font = self._get_font('rm')
574 uniindex = 0xA4 # currency char, for lack of anything better
575 slanted = False
577 return font, uniindex, slanted
579 def get_sized_alternatives_for_symbol(self, fontname, sym):
580 if self._fallback_font:
581 return self._fallback_font.get_sized_alternatives_for_symbol(
582 fontname, sym)
583 return [(fontname, sym)]
586class DejaVuFonts(UnicodeFonts):
588 def __init__(self, *args, **kwargs):
589 # This must come first so the backend's owner is set correctly
590 if isinstance(self, DejaVuSerifFonts):
591 self._fallback_font = StixFonts(*args, **kwargs)
592 else:
593 self._fallback_font = StixSansFonts(*args, **kwargs)
594 self.bakoma = BakomaFonts(*args, **kwargs)
595 TruetypeFonts.__init__(self, *args, **kwargs)
596 self.fontmap = {}
597 # Include Stix sized alternatives for glyphs
598 self._fontmap.update({
599 1: 'STIXSizeOneSym',
600 2: 'STIXSizeTwoSym',
601 3: 'STIXSizeThreeSym',
602 4: 'STIXSizeFourSym',
603 5: 'STIXSizeFiveSym',
604 })
605 for key, name in self._fontmap.items():
606 fullpath = findfont(name)
607 self.fontmap[key] = fullpath
608 self.fontmap[name] = fullpath
610 def _get_glyph(self, fontname, font_class, sym):
611 # Override prime symbol to use Bakoma.
612 if sym == r'\prime':
613 return self.bakoma._get_glyph(fontname, font_class, sym)
614 else:
615 # check whether the glyph is available in the display font
616 uniindex = get_unicode_index(sym)
617 font = self._get_font('ex')
618 if font is not None:
619 glyphindex = font.get_char_index(uniindex)
620 if glyphindex != 0:
621 return super()._get_glyph('ex', font_class, sym)
622 # otherwise return regular glyph
623 return super()._get_glyph(fontname, font_class, sym)
626class DejaVuSerifFonts(DejaVuFonts):
627 """
628 A font handling class for the DejaVu Serif fonts
630 If a glyph is not found it will fallback to Stix Serif
631 """
632 _fontmap = {
633 'rm': 'DejaVu Serif',
634 'it': 'DejaVu Serif:italic',
635 'bf': 'DejaVu Serif:weight=bold',
636 'sf': 'DejaVu Sans',
637 'tt': 'DejaVu Sans Mono',
638 'ex': 'DejaVu Serif Display',
639 0: 'DejaVu Serif',
640 }
643class DejaVuSansFonts(DejaVuFonts):
644 """
645 A font handling class for the DejaVu Sans fonts
647 If a glyph is not found it will fallback to Stix Sans
648 """
649 _fontmap = {
650 'rm': 'DejaVu Sans',
651 'it': 'DejaVu Sans:italic',
652 'bf': 'DejaVu Sans:weight=bold',
653 'sf': 'DejaVu Sans',
654 'tt': 'DejaVu Sans Mono',
655 'ex': 'DejaVu Sans Display',
656 0: 'DejaVu Sans',
657 }
660class StixFonts(UnicodeFonts):
661 """
662 A font handling class for the STIX fonts.
664 In addition to what UnicodeFonts provides, this class:
666 - supports "virtual fonts" which are complete alpha numeric
667 character sets with different font styles at special Unicode
668 code points, such as "Blackboard".
670 - handles sized alternative characters for the STIXSizeX fonts.
671 """
672 _fontmap = {
673 'rm': 'STIXGeneral',
674 'it': 'STIXGeneral:italic',
675 'bf': 'STIXGeneral:weight=bold',
676 'nonunirm': 'STIXNonUnicode',
677 'nonuniit': 'STIXNonUnicode:italic',
678 'nonunibf': 'STIXNonUnicode:weight=bold',
679 0: 'STIXGeneral',
680 1: 'STIXSizeOneSym',
681 2: 'STIXSizeTwoSym',
682 3: 'STIXSizeThreeSym',
683 4: 'STIXSizeFourSym',
684 5: 'STIXSizeFiveSym',
685 }
686 _fallback_font = False
687 _sans = False
689 def __init__(self, *args, **kwargs):
690 TruetypeFonts.__init__(self, *args, **kwargs)
691 self.fontmap = {}
692 for key, name in self._fontmap.items():
693 fullpath = findfont(name)
694 self.fontmap[key] = fullpath
695 self.fontmap[name] = fullpath
697 def _map_virtual_font(self, fontname, font_class, uniindex):
698 # Handle these "fonts" that are actually embedded in
699 # other fonts.
700 mapping = stix_virtual_fonts.get(fontname)
701 if (self._sans and mapping is None
702 and fontname not in ('regular', 'default')):
703 mapping = stix_virtual_fonts['sf']
704 doing_sans_conversion = True
705 else:
706 doing_sans_conversion = False
708 if mapping is not None:
709 if isinstance(mapping, dict):
710 try:
711 mapping = mapping[font_class]
712 except KeyError:
713 mapping = mapping['rm']
715 # Binary search for the source glyph
716 lo = 0
717 hi = len(mapping)
718 while lo < hi:
719 mid = (lo+hi)//2
720 range = mapping[mid]
721 if uniindex < range[0]:
722 hi = mid
723 elif uniindex <= range[1]:
724 break
725 else:
726 lo = mid + 1
728 if range[0] <= uniindex <= range[1]:
729 uniindex = uniindex - range[0] + range[3]
730 fontname = range[2]
731 elif not doing_sans_conversion:
732 # This will generate a dummy character
733 uniindex = 0x1
734 fontname = mpl.rcParams['mathtext.default']
736 # Fix some incorrect glyphs.
737 if fontname in ('rm', 'it'):
738 uniindex = stix_glyph_fixes.get(uniindex, uniindex)
740 # Handle private use area glyphs
741 if fontname in ('it', 'rm', 'bf') and 0xe000 <= uniindex <= 0xf8ff:
742 fontname = 'nonuni' + fontname
744 return fontname, uniindex
746 @functools.lru_cache()
747 def get_sized_alternatives_for_symbol(self, fontname, sym):
748 fixes = {
749 '\\{': '{', '\\}': '}', '\\[': '[', '\\]': ']',
750 '<': '\N{MATHEMATICAL LEFT ANGLE BRACKET}',
751 '>': '\N{MATHEMATICAL RIGHT ANGLE BRACKET}',
752 }
753 sym = fixes.get(sym, sym)
754 try:
755 uniindex = get_unicode_index(sym)
756 except ValueError:
757 return [(fontname, sym)]
758 alternatives = [(i, chr(uniindex)) for i in range(6)
759 if self._get_font(i).get_char_index(uniindex) != 0]
760 # The largest size of the radical symbol in STIX has incorrect
761 # metrics that cause it to be disconnected from the stem.
762 if sym == r'\__sqrt__':
763 alternatives = alternatives[:-1]
764 return alternatives
767class StixSansFonts(StixFonts):
768 """
769 A font handling class for the STIX fonts (that uses sans-serif
770 characters by default).
771 """
772 _sans = True
775##############################################################################
776# TeX-LIKE BOX MODEL
778# The following is based directly on the document 'woven' from the
779# TeX82 source code. This information is also available in printed
780# form:
781#
782# Knuth, Donald E.. 1986. Computers and Typesetting, Volume B:
783# TeX: The Program. Addison-Wesley Professional.
784#
785# The most relevant "chapters" are:
786# Data structures for boxes and their friends
787# Shipping pages out (ship())
788# Packaging (hpack() and vpack())
789# Data structures for math mode
790# Subroutines for math mode
791# Typesetting math formulas
792#
793# Many of the docstrings below refer to a numbered "node" in that
794# book, e.g., node123
795#
796# Note that (as TeX) y increases downward, unlike many other parts of
797# matplotlib.
799# How much text shrinks when going to the next-smallest level.
800SHRINK_FACTOR = 0.7
801# The number of different sizes of chars to use, beyond which they will not
802# get any smaller
803NUM_SIZE_LEVELS = 6
806class FontConstantsBase:
807 """
808 A set of constants that controls how certain things, such as sub-
809 and superscripts are laid out. These are all metrics that can't
810 be reliably retrieved from the font metrics in the font itself.
811 """
812 # Percentage of x-height of additional horiz. space after sub/superscripts
813 script_space = 0.05
815 # Percentage of x-height that sub/superscripts drop below the baseline
816 subdrop = 0.4
818 # Percentage of x-height that superscripts are raised from the baseline
819 sup1 = 0.7
821 # Percentage of x-height that subscripts drop below the baseline
822 sub1 = 0.3
824 # Percentage of x-height that subscripts drop below the baseline when a
825 # superscript is present
826 sub2 = 0.5
828 # Percentage of x-height that sub/superscripts are offset relative to the
829 # nucleus edge for non-slanted nuclei
830 delta = 0.025
832 # Additional percentage of last character height above 2/3 of the
833 # x-height that superscripts are offset relative to the subscript
834 # for slanted nuclei
835 delta_slanted = 0.2
837 # Percentage of x-height that superscripts and subscripts are offset for
838 # integrals
839 delta_integral = 0.1
842class ComputerModernFontConstants(FontConstantsBase):
843 script_space = 0.075
844 subdrop = 0.2
845 sup1 = 0.45
846 sub1 = 0.2
847 sub2 = 0.3
848 delta = 0.075
849 delta_slanted = 0.3
850 delta_integral = 0.3
853class STIXFontConstants(FontConstantsBase):
854 script_space = 0.1
855 sup1 = 0.8
856 sub2 = 0.6
857 delta = 0.05
858 delta_slanted = 0.3
859 delta_integral = 0.3
862class STIXSansFontConstants(FontConstantsBase):
863 script_space = 0.05
864 sup1 = 0.8
865 delta_slanted = 0.6
866 delta_integral = 0.3
869class DejaVuSerifFontConstants(FontConstantsBase):
870 pass
873class DejaVuSansFontConstants(FontConstantsBase):
874 pass
877# Maps font family names to the FontConstantBase subclass to use
878_font_constant_mapping = {
879 'DejaVu Sans': DejaVuSansFontConstants,
880 'DejaVu Sans Mono': DejaVuSansFontConstants,
881 'DejaVu Serif': DejaVuSerifFontConstants,
882 'cmb10': ComputerModernFontConstants,
883 'cmex10': ComputerModernFontConstants,
884 'cmmi10': ComputerModernFontConstants,
885 'cmr10': ComputerModernFontConstants,
886 'cmss10': ComputerModernFontConstants,
887 'cmsy10': ComputerModernFontConstants,
888 'cmtt10': ComputerModernFontConstants,
889 'STIXGeneral': STIXFontConstants,
890 'STIXNonUnicode': STIXFontConstants,
891 'STIXSizeFiveSym': STIXFontConstants,
892 'STIXSizeFourSym': STIXFontConstants,
893 'STIXSizeThreeSym': STIXFontConstants,
894 'STIXSizeTwoSym': STIXFontConstants,
895 'STIXSizeOneSym': STIXFontConstants,
896 # Map the fonts we used to ship, just for good measure
897 'Bitstream Vera Sans': DejaVuSansFontConstants,
898 'Bitstream Vera': DejaVuSansFontConstants,
899 }
902def _get_font_constant_set(state):
903 constants = _font_constant_mapping.get(
904 state.fontset._get_font(state.font).family_name, FontConstantsBase)
905 # STIX sans isn't really its own fonts, just different code points
906 # in the STIX fonts, so we have to detect this one separately.
907 if (constants is STIXFontConstants and
908 isinstance(state.fontset, StixSansFonts)):
909 return STIXSansFontConstants
910 return constants
913class Node:
914 """A node in the TeX box model."""
916 def __init__(self):
917 self.size = 0
919 def __repr__(self):
920 return type(self).__name__
922 def get_kerning(self, next):
923 return 0.0
925 def shrink(self):
926 """
927 Shrinks one level smaller. There are only three levels of
928 sizes, after which things will no longer get smaller.
929 """
930 self.size += 1
932 def render(self, output, x, y):
933 """Render this node."""
936class Box(Node):
937 """A node with a physical location."""
939 def __init__(self, width, height, depth):
940 super().__init__()
941 self.width = width
942 self.height = height
943 self.depth = depth
945 def shrink(self):
946 super().shrink()
947 if self.size < NUM_SIZE_LEVELS:
948 self.width *= SHRINK_FACTOR
949 self.height *= SHRINK_FACTOR
950 self.depth *= SHRINK_FACTOR
952 def render(self, output, x1, y1, x2, y2):
953 pass
956class Vbox(Box):
957 """A box with only height (zero width)."""
959 def __init__(self, height, depth):
960 super().__init__(0., height, depth)
963class Hbox(Box):
964 """A box with only width (zero height and depth)."""
966 def __init__(self, width):
967 super().__init__(width, 0., 0.)
970class Char(Node):
971 """
972 A single character.
974 Unlike TeX, the font information and metrics are stored with each `Char`
975 to make it easier to lookup the font metrics when needed. Note that TeX
976 boxes have a width, height, and depth, unlike Type1 and TrueType which use
977 a full bounding box and an advance in the x-direction. The metrics must
978 be converted to the TeX model, and the advance (if different from width)
979 must be converted into a `Kern` node when the `Char` is added to its parent
980 `Hlist`.
981 """
983 def __init__(self, c, state):
984 super().__init__()
985 self.c = c
986 self.fontset = state.fontset
987 self.font = state.font
988 self.font_class = state.font_class
989 self.fontsize = state.fontsize
990 self.dpi = state.dpi
991 # The real width, height and depth will be set during the
992 # pack phase, after we know the real fontsize
993 self._update_metrics()
995 def __repr__(self):
996 return '`%s`' % self.c
998 def _update_metrics(self):
999 metrics = self._metrics = self.fontset.get_metrics(
1000 self.font, self.font_class, self.c, self.fontsize, self.dpi)
1001 if self.c == ' ':
1002 self.width = metrics.advance
1003 else:
1004 self.width = metrics.width
1005 self.height = metrics.iceberg
1006 self.depth = -(metrics.iceberg - metrics.height)
1008 def is_slanted(self):
1009 return self._metrics.slanted
1011 def get_kerning(self, next):
1012 """
1013 Return the amount of kerning between this and the given character.
1015 This method is called when characters are strung together into `Hlist`
1016 to create `Kern` nodes.
1017 """
1018 advance = self._metrics.advance - self.width
1019 kern = 0.
1020 if isinstance(next, Char):
1021 kern = self.fontset.get_kern(
1022 self.font, self.font_class, self.c, self.fontsize,
1023 next.font, next.font_class, next.c, next.fontsize,
1024 self.dpi)
1025 return advance + kern
1027 def render(self, output, x, y):
1028 self.fontset.render_glyph(
1029 output, x, y,
1030 self.font, self.font_class, self.c, self.fontsize, self.dpi)
1032 def shrink(self):
1033 super().shrink()
1034 if self.size < NUM_SIZE_LEVELS:
1035 self.fontsize *= SHRINK_FACTOR
1036 self.width *= SHRINK_FACTOR
1037 self.height *= SHRINK_FACTOR
1038 self.depth *= SHRINK_FACTOR
1041class Accent(Char):
1042 """
1043 The font metrics need to be dealt with differently for accents,
1044 since they are already offset correctly from the baseline in
1045 TrueType fonts.
1046 """
1047 def _update_metrics(self):
1048 metrics = self._metrics = self.fontset.get_metrics(
1049 self.font, self.font_class, self.c, self.fontsize, self.dpi)
1050 self.width = metrics.xmax - metrics.xmin
1051 self.height = metrics.ymax - metrics.ymin
1052 self.depth = 0
1054 def shrink(self):
1055 super().shrink()
1056 self._update_metrics()
1058 def render(self, output, x, y):
1059 self.fontset.render_glyph(
1060 output, x - self._metrics.xmin, y + self._metrics.ymin,
1061 self.font, self.font_class, self.c, self.fontsize, self.dpi)
1064class List(Box):
1065 """A list of nodes (either horizontal or vertical)."""
1067 def __init__(self, elements):
1068 super().__init__(0., 0., 0.)
1069 self.shift_amount = 0. # An arbitrary offset
1070 self.children = elements # The child nodes of this list
1071 # The following parameters are set in the vpack and hpack functions
1072 self.glue_set = 0. # The glue setting of this list
1073 self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching
1074 self.glue_order = 0 # The order of infinity (0 - 3) for the glue
1076 def __repr__(self):
1077 return '%s<w=%.02f h=%.02f d=%.02f s=%.02f>[%s]' % (
1078 super().__repr__(),
1079 self.width, self.height,
1080 self.depth, self.shift_amount,
1081 ', '.join([repr(x) for x in self.children]))
1083 def _set_glue(self, x, sign, totals, error_type):
1084 self.glue_order = o = next(
1085 # Highest order of glue used by the members of this list.
1086 (i for i in range(len(totals))[::-1] if totals[i] != 0), 0)
1087 self.glue_sign = sign
1088 if totals[o] != 0.:
1089 self.glue_set = x / totals[o]
1090 else:
1091 self.glue_sign = 0
1092 self.glue_ratio = 0.
1093 if o == 0:
1094 if len(self.children):
1095 _log.warning("%s %s: %r",
1096 error_type, type(self).__name__, self)
1098 def shrink(self):
1099 for child in self.children:
1100 child.shrink()
1101 super().shrink()
1102 if self.size < NUM_SIZE_LEVELS:
1103 self.shift_amount *= SHRINK_FACTOR
1104 self.glue_set *= SHRINK_FACTOR
1107class Hlist(List):
1108 """A horizontal list of boxes."""
1110 def __init__(self, elements, w=0., m='additional', do_kern=True):
1111 super().__init__(elements)
1112 if do_kern:
1113 self.kern()
1114 self.hpack(w=w, m=m)
1116 def kern(self):
1117 """
1118 Insert `Kern` nodes between `Char` nodes to set kerning.
1120 The `Char` nodes themselves determine the amount of kerning they need
1121 (in `~Char.get_kerning`), and this function just creates the correct
1122 linked list.
1123 """
1124 new_children = []
1125 num_children = len(self.children)
1126 if num_children:
1127 for i in range(num_children):
1128 elem = self.children[i]
1129 if i < num_children - 1:
1130 next = self.children[i + 1]
1131 else:
1132 next = None
1134 new_children.append(elem)
1135 kerning_distance = elem.get_kerning(next)
1136 if kerning_distance != 0.:
1137 kern = Kern(kerning_distance)
1138 new_children.append(kern)
1139 self.children = new_children
1141 # This is a failed experiment to fake cross-font kerning.
1142# def get_kerning(self, next):
1143# if len(self.children) >= 2 and isinstance(self.children[-2], Char):
1144# if isinstance(next, Char):
1145# print "CASE A"
1146# return self.children[-2].get_kerning(next)
1147# elif (isinstance(next, Hlist) and len(next.children)
1148# and isinstance(next.children[0], Char)):
1149# print "CASE B"
1150# result = self.children[-2].get_kerning(next.children[0])
1151# print result
1152# return result
1153# return 0.0
1155 def hpack(self, w=0., m='additional'):
1156 r"""
1157 Compute the dimensions of the resulting boxes, and adjust the glue if
1158 one of those dimensions is pre-specified. The computed sizes normally
1159 enclose all of the material inside the new box; but some items may
1160 stick out if negative glue is used, if the box is overfull, or if a
1161 ``\vbox`` includes other boxes that have been shifted left.
1163 Parameters
1164 ----------
1165 w : float, default: 0
1166 A width.
1167 m : {'exactly', 'additional'}, default: 'additional'
1168 Whether to produce a box whose width is 'exactly' *w*; or a box
1169 with the natural width of the contents, plus *w* ('additional').
1171 Notes
1172 -----
1173 The defaults produce a box with the natural width of the contents.
1174 """
1175 # I don't know why these get reset in TeX. Shift_amount is pretty
1176 # much useless if we do.
1177 # self.shift_amount = 0.
1178 h = 0.
1179 d = 0.
1180 x = 0.
1181 total_stretch = [0.] * 4
1182 total_shrink = [0.] * 4
1183 for p in self.children:
1184 if isinstance(p, Char):
1185 x += p.width
1186 h = max(h, p.height)
1187 d = max(d, p.depth)
1188 elif isinstance(p, Box):
1189 x += p.width
1190 if not np.isinf(p.height) and not np.isinf(p.depth):
1191 s = getattr(p, 'shift_amount', 0.)
1192 h = max(h, p.height - s)
1193 d = max(d, p.depth + s)
1194 elif isinstance(p, Glue):
1195 glue_spec = p.glue_spec
1196 x += glue_spec.width
1197 total_stretch[glue_spec.stretch_order] += glue_spec.stretch
1198 total_shrink[glue_spec.shrink_order] += glue_spec.shrink
1199 elif isinstance(p, Kern):
1200 x += p.width
1201 self.height = h
1202 self.depth = d
1204 if m == 'additional':
1205 w += x
1206 self.width = w
1207 x = w - x
1209 if x == 0.:
1210 self.glue_sign = 0
1211 self.glue_order = 0
1212 self.glue_ratio = 0.
1213 return
1214 if x > 0.:
1215 self._set_glue(x, 1, total_stretch, "Overful")
1216 else:
1217 self._set_glue(x, -1, total_shrink, "Underful")
1220class Vlist(List):
1221 """A vertical list of boxes."""
1223 def __init__(self, elements, h=0., m='additional'):
1224 super().__init__(elements)
1225 self.vpack(h=h, m=m)
1227 def vpack(self, h=0., m='additional', l=np.inf):
1228 """
1229 Compute the dimensions of the resulting boxes, and to adjust the glue
1230 if one of those dimensions is pre-specified.
1232 Parameters
1233 ----------
1234 h : float, default: 0
1235 A height.
1236 m : {'exactly', 'additional'}, default: 'additional'
1237 Whether to produce a box whose height is 'exactly' *h*; or a box
1238 with the natural height of the contents, plus *h* ('additional').
1239 l : float, default: np.inf
1240 The maximum height.
1242 Notes
1243 -----
1244 The defaults produce a box with the natural height of the contents.
1245 """
1246 # I don't know why these get reset in TeX. Shift_amount is pretty
1247 # much useless if we do.
1248 # self.shift_amount = 0.
1249 w = 0.
1250 d = 0.
1251 x = 0.
1252 total_stretch = [0.] * 4
1253 total_shrink = [0.] * 4
1254 for p in self.children:
1255 if isinstance(p, Box):
1256 x += d + p.height
1257 d = p.depth
1258 if not np.isinf(p.width):
1259 s = getattr(p, 'shift_amount', 0.)
1260 w = max(w, p.width + s)
1261 elif isinstance(p, Glue):
1262 x += d
1263 d = 0.
1264 glue_spec = p.glue_spec
1265 x += glue_spec.width
1266 total_stretch[glue_spec.stretch_order] += glue_spec.stretch
1267 total_shrink[glue_spec.shrink_order] += glue_spec.shrink
1268 elif isinstance(p, Kern):
1269 x += d + p.width
1270 d = 0.
1271 elif isinstance(p, Char):
1272 raise RuntimeError(
1273 "Internal mathtext error: Char node found in Vlist")
1275 self.width = w
1276 if d > l:
1277 x += d - l
1278 self.depth = l
1279 else:
1280 self.depth = d
1282 if m == 'additional':
1283 h += x
1284 self.height = h
1285 x = h - x
1287 if x == 0:
1288 self.glue_sign = 0
1289 self.glue_order = 0
1290 self.glue_ratio = 0.
1291 return
1293 if x > 0.:
1294 self._set_glue(x, 1, total_stretch, "Overful")
1295 else:
1296 self._set_glue(x, -1, total_shrink, "Underful")
1299class Rule(Box):
1300 """
1301 A solid black rectangle.
1303 It has *width*, *depth*, and *height* fields just as in an `Hlist`.
1304 However, if any of these dimensions is inf, the actual value will be
1305 determined by running the rule up to the boundary of the innermost
1306 enclosing box. This is called a "running dimension". The width is never
1307 running in an `Hlist`; the height and depth are never running in a `Vlist`.
1308 """
1310 def __init__(self, width, height, depth, state):
1311 super().__init__(width, height, depth)
1312 self.fontset = state.fontset
1314 def render(self, output, x, y, w, h):
1315 self.fontset.render_rect_filled(output, x, y, x + w, y + h)
1318class Hrule(Rule):
1319 """Convenience class to create a horizontal rule."""
1321 def __init__(self, state, thickness=None):
1322 if thickness is None:
1323 thickness = state.get_current_underline_thickness()
1324 height = depth = thickness * 0.5
1325 super().__init__(np.inf, height, depth, state)
1328class Vrule(Rule):
1329 """Convenience class to create a vertical rule."""
1331 def __init__(self, state):
1332 thickness = state.get_current_underline_thickness()
1333 super().__init__(thickness, np.inf, np.inf, state)
1336_GlueSpec = namedtuple(
1337 "_GlueSpec", "width stretch stretch_order shrink shrink_order")
1338_GlueSpec._named = {
1339 'fil': _GlueSpec(0., 1., 1, 0., 0),
1340 'fill': _GlueSpec(0., 1., 2, 0., 0),
1341 'filll': _GlueSpec(0., 1., 3, 0., 0),
1342 'neg_fil': _GlueSpec(0., 0., 0, 1., 1),
1343 'neg_fill': _GlueSpec(0., 0., 0, 1., 2),
1344 'neg_filll': _GlueSpec(0., 0., 0, 1., 3),
1345 'empty': _GlueSpec(0., 0., 0, 0., 0),
1346 'ss': _GlueSpec(0., 1., 1, -1., 1),
1347}
1350class Glue(Node):
1351 """
1352 Most of the information in this object is stored in the underlying
1353 ``_GlueSpec`` class, which is shared between multiple glue objects.
1354 (This is a memory optimization which probably doesn't matter anymore, but
1355 it's easier to stick to what TeX does.)
1356 """
1358 def __init__(self, glue_type):
1359 super().__init__()
1360 if isinstance(glue_type, str):
1361 glue_spec = _GlueSpec._named[glue_type]
1362 elif isinstance(glue_type, _GlueSpec):
1363 glue_spec = glue_type
1364 else:
1365 raise ValueError("glue_type must be a glue spec name or instance")
1366 self.glue_spec = glue_spec
1368 def shrink(self):
1369 super().shrink()
1370 if self.size < NUM_SIZE_LEVELS:
1371 g = self.glue_spec
1372 self.glue_spec = g._replace(width=g.width * SHRINK_FACTOR)
1375class HCentered(Hlist):
1376 """
1377 A convenience class to create an `Hlist` whose contents are
1378 centered within its enclosing box.
1379 """
1381 def __init__(self, elements):
1382 super().__init__([Glue('ss'), *elements, Glue('ss')], do_kern=False)
1385class VCentered(Vlist):
1386 """
1387 A convenience class to create a `Vlist` whose contents are
1388 centered within its enclosing box.
1389 """
1391 def __init__(self, elements):
1392 super().__init__([Glue('ss'), *elements, Glue('ss')])
1395class Kern(Node):
1396 """
1397 A `Kern` node has a width field to specify a (normally
1398 negative) amount of spacing. This spacing correction appears in
1399 horizontal lists between letters like A and V when the font
1400 designer said that it looks better to move them closer together or
1401 further apart. A kern node can also appear in a vertical list,
1402 when its *width* denotes additional spacing in the vertical
1403 direction.
1404 """
1406 height = 0
1407 depth = 0
1409 def __init__(self, width):
1410 super().__init__()
1411 self.width = width
1413 def __repr__(self):
1414 return "k%.02f" % self.width
1416 def shrink(self):
1417 super().shrink()
1418 if self.size < NUM_SIZE_LEVELS:
1419 self.width *= SHRINK_FACTOR
1422class AutoHeightChar(Hlist):
1423 """
1424 A character as close to the given height and depth as possible.
1426 When using a font with multiple height versions of some characters (such as
1427 the BaKoMa fonts), the correct glyph will be selected, otherwise this will
1428 always just return a scaled version of the glyph.
1429 """
1431 def __init__(self, c, height, depth, state, always=False, factor=None):
1432 alternatives = state.fontset.get_sized_alternatives_for_symbol(
1433 state.font, c)
1435 xHeight = state.fontset.get_xheight(
1436 state.font, state.fontsize, state.dpi)
1438 state = state.copy()
1439 target_total = height + depth
1440 for fontname, sym in alternatives:
1441 state.font = fontname
1442 char = Char(sym, state)
1443 # Ensure that size 0 is chosen when the text is regular sized but
1444 # with descender glyphs by subtracting 0.2 * xHeight
1445 if char.height + char.depth >= target_total - 0.2 * xHeight:
1446 break
1448 shift = 0
1449 if state.font != 0:
1450 if factor is None:
1451 factor = target_total / (char.height + char.depth)
1452 state.fontsize *= factor
1453 char = Char(sym, state)
1455 shift = (depth - char.depth)
1457 super().__init__([char])
1458 self.shift_amount = shift
1461class AutoWidthChar(Hlist):
1462 """
1463 A character as close to the given width as possible.
1465 When using a font with multiple width versions of some characters (such as
1466 the BaKoMa fonts), the correct glyph will be selected, otherwise this will
1467 always just return a scaled version of the glyph.
1468 """
1470 def __init__(self, c, width, state, always=False, char_class=Char):
1471 alternatives = state.fontset.get_sized_alternatives_for_symbol(
1472 state.font, c)
1474 state = state.copy()
1475 for fontname, sym in alternatives:
1476 state.font = fontname
1477 char = char_class(sym, state)
1478 if char.width >= width:
1479 break
1481 factor = width / char.width
1482 state.fontsize *= factor
1483 char = char_class(sym, state)
1485 super().__init__([char])
1486 self.width = char.width
1489def ship(box, xy=(0, 0)):
1490 """
1491 Ship out *box* at offset *xy*, converting it to an `Output`.
1493 Since boxes can be inside of boxes inside of boxes, the main work of `ship`
1494 is done by two mutually recursive routines, `hlist_out` and `vlist_out`,
1495 which traverse the `Hlist` nodes and `Vlist` nodes inside of horizontal
1496 and vertical boxes. The global variables used in TeX to store state as it
1497 processes have become local variables here.
1498 """
1499 ox, oy = xy
1500 cur_v = 0.
1501 cur_h = 0.
1502 off_h = ox
1503 off_v = oy + box.height
1504 output = Output(box)
1506 def clamp(value):
1507 return -1e9 if value < -1e9 else +1e9 if value > +1e9 else value
1509 def hlist_out(box):
1510 nonlocal cur_v, cur_h, off_h, off_v
1512 cur_g = 0
1513 cur_glue = 0.
1514 glue_order = box.glue_order
1515 glue_sign = box.glue_sign
1516 base_line = cur_v
1517 left_edge = cur_h
1519 for p in box.children:
1520 if isinstance(p, Char):
1521 p.render(output, cur_h + off_h, cur_v + off_v)
1522 cur_h += p.width
1523 elif isinstance(p, Kern):
1524 cur_h += p.width
1525 elif isinstance(p, List):
1526 # node623
1527 if len(p.children) == 0:
1528 cur_h += p.width
1529 else:
1530 edge = cur_h
1531 cur_v = base_line + p.shift_amount
1532 if isinstance(p, Hlist):
1533 hlist_out(p)
1534 else:
1535 # p.vpack(box.height + box.depth, 'exactly')
1536 vlist_out(p)
1537 cur_h = edge + p.width
1538 cur_v = base_line
1539 elif isinstance(p, Box):
1540 # node624
1541 rule_height = p.height
1542 rule_depth = p.depth
1543 rule_width = p.width
1544 if np.isinf(rule_height):
1545 rule_height = box.height
1546 if np.isinf(rule_depth):
1547 rule_depth = box.depth
1548 if rule_height > 0 and rule_width > 0:
1549 cur_v = base_line + rule_depth
1550 p.render(output,
1551 cur_h + off_h, cur_v + off_v,
1552 rule_width, rule_height)
1553 cur_v = base_line
1554 cur_h += rule_width
1555 elif isinstance(p, Glue):
1556 # node625
1557 glue_spec = p.glue_spec
1558 rule_width = glue_spec.width - cur_g
1559 if glue_sign != 0: # normal
1560 if glue_sign == 1: # stretching
1561 if glue_spec.stretch_order == glue_order:
1562 cur_glue += glue_spec.stretch
1563 cur_g = round(clamp(box.glue_set * cur_glue))
1564 elif glue_spec.shrink_order == glue_order:
1565 cur_glue += glue_spec.shrink
1566 cur_g = round(clamp(box.glue_set * cur_glue))
1567 rule_width += cur_g
1568 cur_h += rule_width
1570 def vlist_out(box):
1571 nonlocal cur_v, cur_h, off_h, off_v
1573 cur_g = 0
1574 cur_glue = 0.
1575 glue_order = box.glue_order
1576 glue_sign = box.glue_sign
1577 left_edge = cur_h
1578 cur_v -= box.height
1579 top_edge = cur_v
1581 for p in box.children:
1582 if isinstance(p, Kern):
1583 cur_v += p.width
1584 elif isinstance(p, List):
1585 if len(p.children) == 0:
1586 cur_v += p.height + p.depth
1587 else:
1588 cur_v += p.height
1589 cur_h = left_edge + p.shift_amount
1590 save_v = cur_v
1591 p.width = box.width
1592 if isinstance(p, Hlist):
1593 hlist_out(p)
1594 else:
1595 vlist_out(p)
1596 cur_v = save_v + p.depth
1597 cur_h = left_edge
1598 elif isinstance(p, Box):
1599 rule_height = p.height
1600 rule_depth = p.depth
1601 rule_width = p.width
1602 if np.isinf(rule_width):
1603 rule_width = box.width
1604 rule_height += rule_depth
1605 if rule_height > 0 and rule_depth > 0:
1606 cur_v += rule_height
1607 p.render(output,
1608 cur_h + off_h, cur_v + off_v,
1609 rule_width, rule_height)
1610 elif isinstance(p, Glue):
1611 glue_spec = p.glue_spec
1612 rule_height = glue_spec.width - cur_g
1613 if glue_sign != 0: # normal
1614 if glue_sign == 1: # stretching
1615 if glue_spec.stretch_order == glue_order:
1616 cur_glue += glue_spec.stretch
1617 cur_g = round(clamp(box.glue_set * cur_glue))
1618 elif glue_spec.shrink_order == glue_order: # shrinking
1619 cur_glue += glue_spec.shrink
1620 cur_g = round(clamp(box.glue_set * cur_glue))
1621 rule_height += cur_g
1622 cur_v += rule_height
1623 elif isinstance(p, Char):
1624 raise RuntimeError(
1625 "Internal mathtext error: Char node found in vlist")
1627 hlist_out(box)
1628 return output
1631##############################################################################
1632# PARSER
1635def Error(msg):
1636 """Helper class to raise parser errors."""
1637 def raise_error(s, loc, toks):
1638 raise ParseFatalException(s, loc, msg)
1640 return Empty().setParseAction(raise_error)
1643class ParserState:
1644 """
1645 Parser state.
1647 States are pushed and popped from a stack as necessary, and the "current"
1648 state is always at the top of the stack.
1650 Upon entering and leaving a group { } or math/non-math, the stack is pushed
1651 and popped accordingly.
1652 """
1654 def __init__(self, fontset, font, font_class, fontsize, dpi):
1655 self.fontset = fontset
1656 self._font = font
1657 self.font_class = font_class
1658 self.fontsize = fontsize
1659 self.dpi = dpi
1661 def copy(self):
1662 return copy.copy(self)
1664 @property
1665 def font(self):
1666 return self._font
1668 @font.setter
1669 def font(self, name):
1670 if name in ('rm', 'it', 'bf'):
1671 self.font_class = name
1672 self._font = name
1674 def get_current_underline_thickness(self):
1675 """Return the underline thickness for this state."""
1676 return self.fontset.get_underline_thickness(
1677 self.font, self.fontsize, self.dpi)
1680def cmd(expr, args):
1681 r"""
1682 Helper to define TeX commands.
1684 ``cmd("\cmd", args)`` is equivalent to
1685 ``"\cmd" - (args | Error("Expected \cmd{arg}{...}"))`` where the names in
1686 the error message are taken from element names in *args*. If *expr*
1687 already includes arguments (e.g. "\cmd{arg}{...}"), then they are stripped
1688 when constructing the parse element, but kept (and *expr* is used as is) in
1689 the error message.
1690 """
1692 def names(elt):
1693 if isinstance(elt, ParseExpression):
1694 for expr in elt.exprs:
1695 yield from names(expr)
1696 elif elt.resultsName:
1697 yield elt.resultsName
1699 csname = expr.split("{", 1)[0]
1700 err = (csname + "".join("{%s}" % name for name in names(args))
1701 if expr == csname else expr)
1702 return csname - (args | Error(f"Expected {err}"))
1705class Parser:
1706 """
1707 A pyparsing-based parser for strings containing math expressions.
1709 Raw text may also appear outside of pairs of ``$``.
1711 The grammar is based directly on that in TeX, though it cuts a few corners.
1712 """
1714 class _MathStyle(enum.Enum):
1715 DISPLAYSTYLE = 0
1716 TEXTSTYLE = 1
1717 SCRIPTSTYLE = 2
1718 SCRIPTSCRIPTSTYLE = 3
1720 _binary_operators = set(
1721 '+ * - \N{MINUS SIGN}'
1722 r'''
1723 \pm \sqcap \rhd
1724 \mp \sqcup \unlhd
1725 \times \vee \unrhd
1726 \div \wedge \oplus
1727 \ast \setminus \ominus
1728 \star \wr \otimes
1729 \circ \diamond \oslash
1730 \bullet \bigtriangleup \odot
1731 \cdot \bigtriangledown \bigcirc
1732 \cap \triangleleft \dagger
1733 \cup \triangleright \ddagger
1734 \uplus \lhd \amalg
1735 \dotplus \dotminus'''.split())
1737 _relation_symbols = set(r'''
1738 = < > :
1739 \leq \geq \equiv \models
1740 \prec \succ \sim \perp
1741 \preceq \succeq \simeq \mid
1742 \ll \gg \asymp \parallel
1743 \subset \supset \approx \bowtie
1744 \subseteq \supseteq \cong \Join
1745 \sqsubset \sqsupset \neq \smile
1746 \sqsubseteq \sqsupseteq \doteq \frown
1747 \in \ni \propto \vdash
1748 \dashv \dots \doteqdot'''.split())
1750 _arrow_symbols = set(r'''
1751 \leftarrow \longleftarrow \uparrow
1752 \Leftarrow \Longleftarrow \Uparrow
1753 \rightarrow \longrightarrow \downarrow
1754 \Rightarrow \Longrightarrow \Downarrow
1755 \leftrightarrow \longleftrightarrow \updownarrow
1756 \Leftrightarrow \Longleftrightarrow \Updownarrow
1757 \mapsto \longmapsto \nearrow
1758 \hookleftarrow \hookrightarrow \searrow
1759 \leftharpoonup \rightharpoonup \swarrow
1760 \leftharpoondown \rightharpoondown \nwarrow
1761 \rightleftharpoons \leadsto'''.split())
1763 _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols
1765 _punctuation_symbols = set(r', ; . ! \ldotp \cdotp'.split())
1767 _overunder_symbols = set(r'''
1768 \sum \prod \coprod \bigcap \bigcup \bigsqcup \bigvee
1769 \bigwedge \bigodot \bigotimes \bigoplus \biguplus
1770 '''.split())
1772 _overunder_functions = set("lim liminf limsup sup max min".split())
1774 _dropsub_symbols = set(r'''\int \oint'''.split())
1776 _fontnames = set("rm cal it tt sf bf default bb frak scr regular".split())
1778 _function_names = set("""
1779 arccos csc ker min arcsin deg lg Pr arctan det lim sec arg dim
1780 liminf sin cos exp limsup sinh cosh gcd ln sup cot hom log tan
1781 coth inf max tanh""".split())
1783 _ambi_delims = set(r"""
1784 | \| / \backslash \uparrow \downarrow \updownarrow \Uparrow
1785 \Downarrow \Updownarrow . \vert \Vert""".split())
1786 _left_delims = set(r"( [ \{ < \lfloor \langle \lceil".split())
1787 _right_delims = set(r") ] \} > \rfloor \rangle \rceil".split())
1788 _delims = _left_delims | _right_delims | _ambi_delims
1790 def __init__(self):
1791 p = types.SimpleNamespace()
1793 def set_names_and_parse_actions():
1794 for key, val in vars(p).items():
1795 if not key.startswith('_'):
1796 # Set names on everything -- very useful for debugging
1797 val.setName(key)
1798 # Set actions
1799 if hasattr(self, key):
1800 val.setParseAction(getattr(self, key))
1802 # Root definitions.
1804 # In TeX parlance, a csname is a control sequence name (a "\foo").
1805 def csnames(group, names):
1806 ends_with_alpha = []
1807 ends_with_nonalpha = []
1808 for name in names:
1809 if name[-1].isalpha():
1810 ends_with_alpha.append(name)
1811 else:
1812 ends_with_nonalpha.append(name)
1813 return Regex(r"\\(?P<{}>(?:{})(?![A-Za-z]){})".format(
1814 group,
1815 "|".join(map(re.escape, ends_with_alpha)),
1816 "".join(f"|{s}" for s in map(re.escape, ends_with_nonalpha)),
1817 ))
1819 p.float_literal = Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)")
1820 p.space = oneOf(self._space_widths)("space")
1822 p.style_literal = oneOf(
1823 [str(e.value) for e in self._MathStyle])("style_literal")
1825 p.symbol = Regex(
1826 r"[a-zA-Z0-9 +\-*/<>=:,.;!\?&'@()\[\]|\U00000080-\U0001ffff]"
1827 r"|\\[%${}\[\]_|]"
1828 + r"|\\(?:{})(?![A-Za-z])".format(
1829 "|".join(map(re.escape, tex2uni)))
1830 )("sym").leaveWhitespace()
1831 p.unknown_symbol = Regex(r"\\[A-Za-z]*")("name")
1833 p.font = csnames("font", self._fontnames)
1834 p.start_group = (
1835 Optional(r"\math" + oneOf(self._fontnames)("font")) + "{")
1836 p.end_group = Literal("}")
1838 p.delim = oneOf(self._delims)
1840 set_names_and_parse_actions() # for root definitions.
1842 # Mutually recursive definitions. (Minimizing the number of Forward
1843 # elements is important for speed.)
1844 p.accent = Forward()
1845 p.auto_delim = Forward()
1846 p.binom = Forward()
1847 p.customspace = Forward()
1848 p.frac = Forward()
1849 p.dfrac = Forward()
1850 p.function = Forward()
1851 p.genfrac = Forward()
1852 p.group = Forward()
1853 p.operatorname = Forward()
1854 p.overline = Forward()
1855 p.overset = Forward()
1856 p.placeable = Forward()
1857 p.required_group = Forward()
1858 p.simple = Forward()
1859 p.optional_group = Forward()
1860 p.sqrt = Forward()
1861 p.subsuper = Forward()
1862 p.token = Forward()
1863 p.underset = Forward()
1865 set_names_and_parse_actions() # for mutually recursive definitions.
1867 p.customspace <<= cmd(r"\hspace", "{" + p.float_literal("space") + "}")
1869 p.accent <<= (
1870 csnames("accent", [*self._accent_map, *self._wide_accents])
1871 - p.placeable("sym"))
1873 p.function <<= csnames("name", self._function_names)
1874 p.operatorname <<= cmd(
1875 r"\operatorname",
1876 "{" + ZeroOrMore(p.simple | p.unknown_symbol)("name") + "}")
1878 p.group <<= p.start_group + ZeroOrMore(p.token)("group") + p.end_group
1880 p.optional_group <<= "{" + ZeroOrMore(p.token)("group") + "}"
1881 p.required_group <<= "{" + OneOrMore(p.token)("group") + "}"
1883 p.frac <<= cmd(
1884 r"\frac", p.required_group("num") + p.required_group("den"))
1885 p.dfrac <<= cmd(
1886 r"\dfrac", p.required_group("num") + p.required_group("den"))
1887 p.binom <<= cmd(
1888 r"\binom", p.required_group("num") + p.required_group("den"))
1890 p.genfrac <<= cmd(
1891 r"\genfrac",
1892 "{" + Optional(p.delim)("ldelim") + "}"
1893 + "{" + Optional(p.delim)("rdelim") + "}"
1894 + "{" + p.float_literal("rulesize") + "}"
1895 + "{" + Optional(p.style_literal)("style") + "}"
1896 + p.required_group("num")
1897 + p.required_group("den"))
1899 p.sqrt <<= cmd(
1900 r"\sqrt{value}",
1901 Optional("[" + OneOrMore(NotAny("]") + p.token)("root") + "]")
1902 + p.required_group("value"))
1904 p.overline <<= cmd(r"\overline", p.required_group("body"))
1906 p.overset <<= cmd(
1907 r"\overset",
1908 p.optional_group("annotation") + p.optional_group("body"))
1909 p.underset <<= cmd(
1910 r"\underset",
1911 p.optional_group("annotation") + p.optional_group("body"))
1913 p.placeable <<= (
1914 p.accent # Must be before symbol as all accents are symbols
1915 | p.symbol # Must be second to catch all named symbols and single
1916 # chars not in a group
1917 | p.function
1918 | p.operatorname
1919 | p.group
1920 | p.frac
1921 | p.dfrac
1922 | p.binom
1923 | p.genfrac
1924 | p.overset
1925 | p.underset
1926 | p.sqrt
1927 | p.overline
1928 )
1930 p.simple <<= (
1931 p.space
1932 | p.customspace
1933 | p.font
1934 | p.subsuper
1935 )
1937 p.subsuper <<= (
1938 (Optional(p.placeable)("nucleus")
1939 + OneOrMore(oneOf(["_", "^"]) - p.placeable)("subsuper")
1940 + Regex("'*")("apostrophes"))
1941 | Regex("'+")("apostrophes")
1942 | (p.placeable("nucleus") + Regex("'*")("apostrophes"))
1943 )
1945 p.token <<= (
1946 p.simple
1947 | p.auto_delim
1948 | p.unknown_symbol # Must be last
1949 )
1951 p.auto_delim <<= (
1952 r"\left" - (p.delim("left") | Error("Expected a delimiter"))
1953 + ZeroOrMore(p.simple | p.auto_delim)("mid")
1954 + r"\right" - (p.delim("right") | Error("Expected a delimiter"))
1955 )
1957 # Leaf definitions.
1958 p.math = OneOrMore(p.token)
1959 p.math_string = QuotedString('$', '\\', unquoteResults=False)
1960 p.non_math = Regex(r"(?:(?:\\[$])|[^$])*").leaveWhitespace()
1961 p.main = (
1962 p.non_math + ZeroOrMore(p.math_string + p.non_math) + StringEnd()
1963 )
1964 set_names_and_parse_actions() # for leaf definitions.
1966 self._expression = p.main
1967 self._math_expression = p.math
1969 # To add space to nucleus operators after sub/superscripts
1970 self._in_subscript_or_superscript = False
1972 def parse(self, s, fonts_object, fontsize, dpi):
1973 """
1974 Parse expression *s* using the given *fonts_object* for
1975 output, at the given *fontsize* and *dpi*.
1977 Returns the parse tree of `Node` instances.
1978 """
1979 self._state_stack = [
1980 ParserState(fonts_object, 'default', 'rm', fontsize, dpi)]
1981 self._em_width_cache = {}
1982 try:
1983 result = self._expression.parseString(s)
1984 except ParseBaseException as err:
1985 raise ValueError("\n".join(["",
1986 err.line,
1987 " " * (err.column - 1) + "^",
1988 str(err)])) from err
1989 self._state_stack = None
1990 self._in_subscript_or_superscript = False
1991 # prevent operator spacing from leaking into a new expression
1992 self._em_width_cache = {}
1993 self._expression.resetCache()
1994 return result[0]
1996 def get_state(self):
1997 """Get the current `State` of the parser."""
1998 return self._state_stack[-1]
2000 def pop_state(self):
2001 """Pop a `State` off of the stack."""
2002 self._state_stack.pop()
2004 def push_state(self):
2005 """Push a new `State` onto the stack, copying the current state."""
2006 self._state_stack.append(self.get_state().copy())
2008 def main(self, s, loc, toks):
2009 return [Hlist(toks)]
2011 def math_string(self, s, loc, toks):
2012 return self._math_expression.parseString(toks[0][1:-1])
2014 def math(self, s, loc, toks):
2015 hlist = Hlist(toks)
2016 self.pop_state()
2017 return [hlist]
2019 def non_math(self, s, loc, toks):
2020 s = toks[0].replace(r'\$', '$')
2021 symbols = [Char(c, self.get_state()) for c in s]
2022 hlist = Hlist(symbols)
2023 # We're going into math now, so set font to 'it'
2024 self.push_state()
2025 self.get_state().font = mpl.rcParams['mathtext.default']
2026 return [hlist]
2028 float_literal = staticmethod(pyparsing_common.convertToFloat)
2030 def _make_space(self, percentage):
2031 # In TeX, an em (the unit usually used to measure horizontal lengths)
2032 # is not the width of the character 'm'; it is the same in different
2033 # font styles (e.g. roman or italic). Mathtext, however, uses 'm' in
2034 # the italic style so that horizontal spaces don't depend on the
2035 # current font style.
2036 state = self.get_state()
2037 key = (state.font, state.fontsize, state.dpi)
2038 width = self._em_width_cache.get(key)
2039 if width is None:
2040 metrics = state.fontset.get_metrics(
2041 'it', mpl.rcParams['mathtext.default'], 'm',
2042 state.fontsize, state.dpi)
2043 width = metrics.advance
2044 self._em_width_cache[key] = width
2045 return Kern(width * percentage)
2047 _space_widths = {
2048 r'\,': 0.16667, # 3/18 em = 3 mu
2049 r'\thinspace': 0.16667, # 3/18 em = 3 mu
2050 r'\/': 0.16667, # 3/18 em = 3 mu
2051 r'\>': 0.22222, # 4/18 em = 4 mu
2052 r'\:': 0.22222, # 4/18 em = 4 mu
2053 r'\;': 0.27778, # 5/18 em = 5 mu
2054 r'\ ': 0.33333, # 6/18 em = 6 mu
2055 r'~': 0.33333, # 6/18 em = 6 mu, nonbreakable
2056 r'\enspace': 0.5, # 9/18 em = 9 mu
2057 r'\quad': 1, # 1 em = 18 mu
2058 r'\qquad': 2, # 2 em = 36 mu
2059 r'\!': -0.16667, # -3/18 em = -3 mu
2060 }
2062 def space(self, s, loc, toks):
2063 num = self._space_widths[toks["space"]]
2064 box = self._make_space(num)
2065 return [box]
2067 def customspace(self, s, loc, toks):
2068 return [self._make_space(toks["space"])]
2070 def symbol(self, s, loc, toks):
2071 c = toks["sym"]
2072 if c == "-":
2073 # "U+2212 minus sign is the preferred representation of the unary
2074 # and binary minus sign rather than the ASCII-derived U+002D
2075 # hyphen-minus, because minus sign is unambiguous and because it
2076 # is rendered with a more desirable length, usually longer than a
2077 # hyphen." (https://www.unicode.org/reports/tr25/)
2078 c = "\N{MINUS SIGN}"
2079 try:
2080 char = Char(c, self.get_state())
2081 except ValueError as err:
2082 raise ParseFatalException(s, loc,
2083 "Unknown symbol: %s" % c) from err
2085 if c in self._spaced_symbols:
2086 # iterate until we find previous character, needed for cases
2087 # such as ${ -2}$, $ -2$, or $ -2$.
2088 prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
2089 # Binary operators at start of string should not be spaced
2090 if (c in self._binary_operators and
2091 (len(s[:loc].split()) == 0 or prev_char == '{' or
2092 prev_char in self._left_delims)):
2093 return [char]
2094 else:
2095 return [Hlist([self._make_space(0.2),
2096 char,
2097 self._make_space(0.2)],
2098 do_kern=True)]
2099 elif c in self._punctuation_symbols:
2100 prev_char = next((c for c in s[:loc][::-1] if c != ' '), '')
2101 next_char = next((c for c in s[loc + 1:] if c != ' '), '')
2103 # Do not space commas between brackets
2104 if c == ',':
2105 if prev_char == '{' and next_char == '}':
2106 return [char]
2108 # Do not space dots as decimal separators
2109 if c == '.' and prev_char.isdigit() and next_char.isdigit():
2110 return [char]
2111 else:
2112 return [Hlist([char, self._make_space(0.2)], do_kern=True)]
2113 return [char]
2115 def unknown_symbol(self, s, loc, toks):
2116 raise ParseFatalException(s, loc, f"Unknown symbol: {toks['name']}")
2118 _accent_map = {
2119 r'hat': r'\circumflexaccent',
2120 r'breve': r'\combiningbreve',
2121 r'bar': r'\combiningoverline',
2122 r'grave': r'\combininggraveaccent',
2123 r'acute': r'\combiningacuteaccent',
2124 r'tilde': r'\combiningtilde',
2125 r'dot': r'\combiningdotabove',
2126 r'ddot': r'\combiningdiaeresis',
2127 r'dddot': r'\combiningthreedotsabove',
2128 r'ddddot': r'\combiningfourdotsabove',
2129 r'vec': r'\combiningrightarrowabove',
2130 r'"': r'\combiningdiaeresis',
2131 r"`": r'\combininggraveaccent',
2132 r"'": r'\combiningacuteaccent',
2133 r'~': r'\combiningtilde',
2134 r'.': r'\combiningdotabove',
2135 r'^': r'\circumflexaccent',
2136 r'overrightarrow': r'\rightarrow',
2137 r'overleftarrow': r'\leftarrow',
2138 r'mathring': r'\circ',
2139 }
2141 _wide_accents = set(r"widehat widetilde widebar".split())
2143 def accent(self, s, loc, toks):
2144 state = self.get_state()
2145 thickness = state.get_current_underline_thickness()
2146 accent = toks["accent"]
2147 sym = toks["sym"]
2148 if accent in self._wide_accents:
2149 accent_box = AutoWidthChar(
2150 '\\' + accent, sym.width, state, char_class=Accent)
2151 else:
2152 accent_box = Accent(self._accent_map[accent], state)
2153 if accent == 'mathring':
2154 accent_box.shrink()
2155 accent_box.shrink()
2156 centered = HCentered([Hbox(sym.width / 4.0), accent_box])
2157 centered.hpack(sym.width, 'exactly')
2158 return Vlist([
2159 centered,
2160 Vbox(0., thickness * 2.0),
2161 Hlist([sym])
2162 ])
2164 def function(self, s, loc, toks):
2165 hlist = self.operatorname(s, loc, toks)
2166 hlist.function_name = toks["name"]
2167 return hlist
2169 def operatorname(self, s, loc, toks):
2170 self.push_state()
2171 state = self.get_state()
2172 state.font = 'rm'
2173 hlist_list = []
2174 # Change the font of Chars, but leave Kerns alone
2175 name = toks["name"]
2176 for c in name:
2177 if isinstance(c, Char):
2178 c.font = 'rm'
2179 c._update_metrics()
2180 hlist_list.append(c)
2181 elif isinstance(c, str):
2182 hlist_list.append(Char(c, state))
2183 else:
2184 hlist_list.append(c)
2185 next_char_loc = loc + len(name) + 1
2186 if isinstance(name, ParseResults):
2187 next_char_loc += len('operatorname{}')
2188 next_char = next((c for c in s[next_char_loc:] if c != ' '), '')
2189 delimiters = self._delims | {'^', '_'}
2190 if (next_char not in delimiters and
2191 name not in self._overunder_functions):
2192 # Add thin space except when followed by parenthesis, bracket, etc.
2193 hlist_list += [self._make_space(self._space_widths[r'\,'])]
2194 self.pop_state()
2195 # if followed by a super/subscript, set flag to true
2196 # This flag tells subsuper to add space after this operator
2197 if next_char in {'^', '_'}:
2198 self._in_subscript_or_superscript = True
2199 else:
2200 self._in_subscript_or_superscript = False
2202 return Hlist(hlist_list)
2204 def start_group(self, s, loc, toks):
2205 self.push_state()
2206 # Deal with LaTeX-style font tokens
2207 if toks.get("font"):
2208 self.get_state().font = toks.get("font")
2209 return []
2211 def group(self, s, loc, toks):
2212 grp = Hlist(toks.get("group", []))
2213 return [grp]
2215 def required_group(self, s, loc, toks):
2216 return Hlist(toks.get("group", []))
2218 optional_group = required_group
2220 def end_group(self, s, loc, toks):
2221 self.pop_state()
2222 return []
2224 def font(self, s, loc, toks):
2225 self.get_state().font = toks["font"]
2226 return []
2228 def is_overunder(self, nucleus):
2229 if isinstance(nucleus, Char):
2230 return nucleus.c in self._overunder_symbols
2231 elif isinstance(nucleus, Hlist) and hasattr(nucleus, 'function_name'):
2232 return nucleus.function_name in self._overunder_functions
2233 return False
2235 def is_dropsub(self, nucleus):
2236 if isinstance(nucleus, Char):
2237 return nucleus.c in self._dropsub_symbols
2238 return False
2240 def is_slanted(self, nucleus):
2241 if isinstance(nucleus, Char):
2242 return nucleus.is_slanted()
2243 return False
2245 def is_between_brackets(self, s, loc):
2246 return False
2248 def subsuper(self, s, loc, toks):
2249 nucleus = toks.get("nucleus", Hbox(0))
2250 subsuper = toks.get("subsuper", [])
2251 napostrophes = len(toks.get("apostrophes", []))
2253 if not subsuper and not napostrophes:
2254 return nucleus
2256 sub = super = None
2257 while subsuper:
2258 op, arg, *subsuper = subsuper
2259 if op == '_':
2260 if sub is not None:
2261 raise ParseFatalException("Double subscript")
2262 sub = arg
2263 else:
2264 if super is not None:
2265 raise ParseFatalException("Double superscript")
2266 super = arg
2268 state = self.get_state()
2269 rule_thickness = state.fontset.get_underline_thickness(
2270 state.font, state.fontsize, state.dpi)
2271 xHeight = state.fontset.get_xheight(
2272 state.font, state.fontsize, state.dpi)
2274 if napostrophes:
2275 if super is None:
2276 super = Hlist([])
2277 for i in range(napostrophes):
2278 super.children.extend(self.symbol(s, loc, {"sym": "\\prime"}))
2279 # kern() and hpack() needed to get the metrics right after
2280 # extending
2281 super.kern()
2282 super.hpack()
2284 # Handle over/under symbols, such as sum or prod
2285 if self.is_overunder(nucleus):
2286 vlist = []
2287 shift = 0.
2288 width = nucleus.width
2289 if super is not None:
2290 super.shrink()
2291 width = max(width, super.width)
2292 if sub is not None:
2293 sub.shrink()
2294 width = max(width, sub.width)
2296 vgap = rule_thickness * 3.0
2297 if super is not None:
2298 hlist = HCentered([super])
2299 hlist.hpack(width, 'exactly')
2300 vlist.extend([hlist, Vbox(0, vgap)])
2301 hlist = HCentered([nucleus])
2302 hlist.hpack(width, 'exactly')
2303 vlist.append(hlist)
2304 if sub is not None:
2305 hlist = HCentered([sub])
2306 hlist.hpack(width, 'exactly')
2307 vlist.extend([Vbox(0, vgap), hlist])
2308 shift = hlist.height + vgap + nucleus.depth
2309 vlist = Vlist(vlist)
2310 vlist.shift_amount = shift
2311 result = Hlist([vlist])
2312 return [result]
2314 # We remove kerning on the last character for consistency (otherwise
2315 # it will compute kerning based on non-shrunk characters and may put
2316 # them too close together when superscripted)
2317 # We change the width of the last character to match the advance to
2318 # consider some fonts with weird metrics: e.g. stix's f has a width of
2319 # 7.75 and a kerning of -4.0 for an advance of 3.72, and we want to put
2320 # the superscript at the advance
2321 last_char = nucleus
2322 if isinstance(nucleus, Hlist):
2323 new_children = nucleus.children
2324 if len(new_children):
2325 # remove last kern
2326 if (isinstance(new_children[-1], Kern) and
2327 hasattr(new_children[-2], '_metrics')):
2328 new_children = new_children[:-1]
2329 last_char = new_children[-1]
2330 if hasattr(last_char, '_metrics'):
2331 last_char.width = last_char._metrics.advance
2332 # create new Hlist without kerning
2333 nucleus = Hlist(new_children, do_kern=False)
2334 else:
2335 if isinstance(nucleus, Char):
2336 last_char.width = last_char._metrics.advance
2337 nucleus = Hlist([nucleus])
2339 # Handle regular sub/superscripts
2340 constants = _get_font_constant_set(state)
2341 lc_height = last_char.height
2342 lc_baseline = 0
2343 if self.is_dropsub(last_char):
2344 lc_baseline = last_char.depth
2346 # Compute kerning for sub and super
2347 superkern = constants.delta * xHeight
2348 subkern = constants.delta * xHeight
2349 if self.is_slanted(last_char):
2350 superkern += constants.delta * xHeight
2351 superkern += (constants.delta_slanted *
2352 (lc_height - xHeight * 2. / 3.))
2353 if self.is_dropsub(last_char):
2354 subkern = (3 * constants.delta -
2355 constants.delta_integral) * lc_height
2356 superkern = (3 * constants.delta +
2357 constants.delta_integral) * lc_height
2358 else:
2359 subkern = 0
2361 if super is None:
2362 # node757
2363 x = Hlist([Kern(subkern), sub])
2364 x.shrink()
2365 if self.is_dropsub(last_char):
2366 shift_down = lc_baseline + constants.subdrop * xHeight
2367 else:
2368 shift_down = constants.sub1 * xHeight
2369 x.shift_amount = shift_down
2370 else:
2371 x = Hlist([Kern(superkern), super])
2372 x.shrink()
2373 if self.is_dropsub(last_char):
2374 shift_up = lc_height - constants.subdrop * xHeight
2375 else:
2376 shift_up = constants.sup1 * xHeight
2377 if sub is None:
2378 x.shift_amount = -shift_up
2379 else: # Both sub and superscript
2380 y = Hlist([Kern(subkern), sub])
2381 y.shrink()
2382 if self.is_dropsub(last_char):
2383 shift_down = lc_baseline + constants.subdrop * xHeight
2384 else:
2385 shift_down = constants.sub2 * xHeight
2386 # If sub and superscript collide, move super up
2387 clr = (2.0 * rule_thickness -
2388 ((shift_up - x.depth) - (y.height - shift_down)))
2389 if clr > 0.:
2390 shift_up += clr
2391 x = Vlist([
2392 x,
2393 Kern((shift_up - x.depth) - (y.height - shift_down)),
2394 y])
2395 x.shift_amount = shift_down
2397 if not self.is_dropsub(last_char):
2398 x.width += constants.script_space * xHeight
2400 # Do we need to add a space after the nucleus?
2401 # To find out, check the flag set by operatorname
2402 spaced_nucleus = [nucleus, x]
2403 if self._in_subscript_or_superscript:
2404 spaced_nucleus += [self._make_space(self._space_widths[r'\,'])]
2405 self._in_subscript_or_superscript = False
2407 result = Hlist(spaced_nucleus)
2408 return [result]
2410 def _genfrac(self, ldelim, rdelim, rule, style, num, den):
2411 state = self.get_state()
2412 thickness = state.get_current_underline_thickness()
2414 for _ in range(style.value):
2415 num.shrink()
2416 den.shrink()
2417 cnum = HCentered([num])
2418 cden = HCentered([den])
2419 width = max(num.width, den.width)
2420 cnum.hpack(width, 'exactly')
2421 cden.hpack(width, 'exactly')
2422 vlist = Vlist([cnum, # numerator
2423 Vbox(0, thickness * 2.0), # space
2424 Hrule(state, rule), # rule
2425 Vbox(0, thickness * 2.0), # space
2426 cden # denominator
2427 ])
2429 # Shift so the fraction line sits in the middle of the
2430 # equals sign
2431 metrics = state.fontset.get_metrics(
2432 state.font, mpl.rcParams['mathtext.default'],
2433 '=', state.fontsize, state.dpi)
2434 shift = (cden.height -
2435 ((metrics.ymax + metrics.ymin) / 2 -
2436 thickness * 3.0))
2437 vlist.shift_amount = shift
2439 result = [Hlist([vlist, Hbox(thickness * 2.)])]
2440 if ldelim or rdelim:
2441 if ldelim == '':
2442 ldelim = '.'
2443 if rdelim == '':
2444 rdelim = '.'
2445 return self._auto_sized_delimiter(ldelim, result, rdelim)
2446 return result
2448 def style_literal(self, s, loc, toks):
2449 return self._MathStyle(int(toks["style_literal"]))
2451 def genfrac(self, s, loc, toks):
2452 return self._genfrac(
2453 toks.get("ldelim", ""), toks.get("rdelim", ""),
2454 toks["rulesize"], toks.get("style", self._MathStyle.TEXTSTYLE),
2455 toks["num"], toks["den"])
2457 def frac(self, s, loc, toks):
2458 return self._genfrac(
2459 "", "", self.get_state().get_current_underline_thickness(),
2460 self._MathStyle.TEXTSTYLE, toks["num"], toks["den"])
2462 def dfrac(self, s, loc, toks):
2463 return self._genfrac(
2464 "", "", self.get_state().get_current_underline_thickness(),
2465 self._MathStyle.DISPLAYSTYLE, toks["num"], toks["den"])
2467 def binom(self, s, loc, toks):
2468 return self._genfrac(
2469 "(", ")", 0,
2470 self._MathStyle.TEXTSTYLE, toks["num"], toks["den"])
2472 def _genset(self, s, loc, toks):
2473 annotation = toks["annotation"]
2474 body = toks["body"]
2475 thickness = self.get_state().get_current_underline_thickness()
2477 annotation.shrink()
2478 cannotation = HCentered([annotation])
2479 cbody = HCentered([body])
2480 width = max(cannotation.width, cbody.width)
2481 cannotation.hpack(width, 'exactly')
2482 cbody.hpack(width, 'exactly')
2484 vgap = thickness * 3
2485 if s[loc + 1] == "u": # \underset
2486 vlist = Vlist([cbody, # body
2487 Vbox(0, vgap), # space
2488 cannotation # annotation
2489 ])
2490 # Shift so the body sits in the same vertical position
2491 vlist.shift_amount = cbody.depth + cannotation.height + vgap
2492 else: # \overset
2493 vlist = Vlist([cannotation, # annotation
2494 Vbox(0, vgap), # space
2495 cbody # body
2496 ])
2498 # To add horizontal gap between symbols: wrap the Vlist into
2499 # an Hlist and extend it with an Hbox(0, horizontal_gap)
2500 return vlist
2502 overset = underset = _genset
2504 def sqrt(self, s, loc, toks):
2505 root = toks.get("root")
2506 body = toks["value"]
2507 state = self.get_state()
2508 thickness = state.get_current_underline_thickness()
2510 # Determine the height of the body, and add a little extra to
2511 # the height so it doesn't seem cramped
2512 height = body.height - body.shift_amount + thickness * 5.0
2513 depth = body.depth + body.shift_amount
2514 check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True)
2515 height = check.height - check.shift_amount
2516 depth = check.depth + check.shift_amount
2518 # Put a little extra space to the left and right of the body
2519 padded_body = Hlist([Hbox(2 * thickness), body, Hbox(2 * thickness)])
2520 rightside = Vlist([Hrule(state), Glue('fill'), padded_body])
2521 # Stretch the glue between the hrule and the body
2522 rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
2523 'exactly', depth)
2525 # Add the root and shift it upward so it is above the tick.
2526 # The value of 0.6 is a hard-coded hack ;)
2527 if not root:
2528 root = Box(check.width * 0.5, 0., 0.)
2529 else:
2530 root = Hlist(root)
2531 root.shrink()
2532 root.shrink()
2534 root_vlist = Vlist([Hlist([root])])
2535 root_vlist.shift_amount = -height * 0.6
2537 hlist = Hlist([root_vlist, # Root
2538 # Negative kerning to put root over tick
2539 Kern(-check.width * 0.5),
2540 check, # Check
2541 rightside]) # Body
2542 return [hlist]
2544 def overline(self, s, loc, toks):
2545 body = toks["body"]
2547 state = self.get_state()
2548 thickness = state.get_current_underline_thickness()
2550 height = body.height - body.shift_amount + thickness * 3.0
2551 depth = body.depth + body.shift_amount
2553 # Place overline above body
2554 rightside = Vlist([Hrule(state), Glue('fill'), Hlist([body])])
2556 # Stretch the glue between the hrule and the body
2557 rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0),
2558 'exactly', depth)
2560 hlist = Hlist([rightside])
2561 return [hlist]
2563 def _auto_sized_delimiter(self, front, middle, back):
2564 state = self.get_state()
2565 if len(middle):
2566 height = max(x.height for x in middle)
2567 depth = max(x.depth for x in middle)
2568 factor = None
2569 else:
2570 height = 0
2571 depth = 0
2572 factor = 1.0
2573 parts = []
2574 # \left. and \right. aren't supposed to produce any symbols
2575 if front != '.':
2576 parts.append(
2577 AutoHeightChar(front, height, depth, state, factor=factor))
2578 parts.extend(middle)
2579 if back != '.':
2580 parts.append(
2581 AutoHeightChar(back, height, depth, state, factor=factor))
2582 hlist = Hlist(parts)
2583 return hlist
2585 def auto_delim(self, s, loc, toks):
2586 return self._auto_sized_delimiter(
2587 toks["left"],
2588 # if "mid" in toks ... can be removed when requiring pyparsing 3.
2589 toks["mid"].asList() if "mid" in toks else [],
2590 toks["right"])