Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1r""" 

2:mod:`~matplotlib.mathtext` is a module for parsing a subset of the 

3TeX math syntax and drawing them to a matplotlib backend. 

4 

5For a tutorial of its usage see :doc:`/tutorials/text/mathtext`. This 

6document is primarily concerned with implementation details. 

7 

8The module uses pyparsing_ to parse the TeX expression. 

9 

10.. _pyparsing: http://pyparsing.wikispaces.com/ 

11 

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

17 

18from collections import namedtuple 

19import functools 

20from io import StringIO 

21import logging 

22import os 

23import types 

24import unicodedata 

25 

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) 

31 

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) 

40 

41ParserElement.enablePackrat() 

42_log = logging.getLogger(__name__) 

43 

44 

45############################################################################## 

46# FONTS 

47 

48def get_unicode_index(symbol, math=True): 

49 r""" 

50 Return the integer index (from the Unicode table) of *symbol*. 

51 

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

80 

81 

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. 

87 

88 Subclasses need to override the following: 

89 

90 - :meth:`render_glyph` 

91 - :meth:`render_rect_filled` 

92 - :meth:`get_results` 

93 

94 And optionally, if you need to use a FreeType hinting style: 

95 

96 - :meth:`get_hinting_type` 

97 """ 

98 def __init__(self): 

99 self.width = 0 

100 self.height = 0 

101 self.depth = 0 

102 

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 

108 

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

115 

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

121 

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

128 

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 

135 

136 

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) 

149 

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

155 

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

160 

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

171 

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) 

183 

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 

206 

207 def get_hinting_type(self): 

208 from matplotlib.backends import backend_agg 

209 return backend_agg.get_hinting_flag() 

210 

211 

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 

217 

218 

219class MathtextBackendPs(MathtextBackend): 

220 """ 

221 Store information to write a mathtext rendering to the PostScript backend. 

222 """ 

223 

224 _PSResult = namedtuple( 

225 "_PSResult", "width height depth pswriter used_characters") 

226 

227 def __init__(self): 

228 self.pswriter = StringIO() 

229 self.lastfont = None 

230 

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 

236 

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

243 

244 self.pswriter.write( 

245 f"{ox:f} {oy:f} moveto\n" 

246 f"/{symbol_name} glyphshow\n") 

247 

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) 

252 

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) 

260 

261 

262class MathtextBackendPdf(MathtextBackend): 

263 """Store information to write a mathtext rendering to the PDF backend.""" 

264 

265 _PDFResult = namedtuple( 

266 "_PDFResult", "width height depth glyphs rects used_characters") 

267 

268 def __init__(self): 

269 self.glyphs = [] 

270 self.rects = [] 

271 

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

278 

279 def render_rect_filled(self, x1, y1, x2, y2): 

280 self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1)) 

281 

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) 

290 

291 

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 = [] 

300 

301 def render_glyph(self, ox, oy, info): 

302 oy = self.height - oy + info.offset 

303 

304 self.svg_glyphs.append( 

305 (info.font, info.fontsize, info.num, ox, oy, info.metrics)) 

306 

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

310 

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) 

320 

321 

322class MathtextBackendPath(MathtextBackend): 

323 """ 

324 Store information to write a mathtext rendering to the text path 

325 machinery. 

326 """ 

327 

328 def __init__(self): 

329 self.glyphs = [] 

330 self.rects = [] 

331 

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

337 

338 def render_rect_filled(self, x1, y1, x2, y2): 

339 self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1)) 

340 

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) 

348 

349 

350class MathtextBackendCairo(MathtextBackend): 

351 """ 

352 Store information to write a mathtext rendering to the Cairo 

353 backend. 

354 """ 

355 

356 def __init__(self): 

357 self.glyphs = [] 

358 self.rects = [] 

359 

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

365 

366 def render_rect_filled(self, x1, y1, x2, y2): 

367 self.rects.append( 

368 (x1, y1 - self.height, x2 - x1, y2 - y1)) 

369 

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) 

377 

378 

379class Fonts: 

380 """ 

381 An abstract base class for a system of fonts to use for mathtext. 

382 

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

387 

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. 

394 

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 = {} 

401 

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 

408 

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

413 

414 *fontX*: one of the TeX font names:: 

415 

416 tt, it, rm, cal, sf, bf or default/regular (non-math) 

417 

418 *fontclassX*: TODO 

419 

420 *symX*: a symbol in raw TeX form. e.g., '1', 'x' or '\sigma' 

421 

422 *fontsizeX*: the fontsize in points 

423 

424 *dpi*: the current dots-per-inch 

425 """ 

426 return 0. 

427 

428 def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True): 

429 r""" 

430 *font*: one of the TeX font names:: 

431 

432 tt, it, rm, cal, sf, bf or default/regular (non-math) 

433 

434 *font_class*: TODO 

435 

436 *sym*: a symbol in raw TeX form. e.g., '1', 'x' or '\sigma' 

437 

438 *fontsize*: font size in points 

439 

440 *dpi*: current dots-per-inch 

441 

442 *math*: whether sym is a math character 

443 

444 Returns an object with the following attributes: 

445 

446 - *advance*: The advance distance (in points) of the glyph. 

447 

448 - *height*: The height of the glyph in points. 

449 

450 - *width*: The width of the glyph in points. 

451 

452 - *xmin*, *xmax*, *ymin*, *ymax* - the ink rectangle of the glyph 

453 

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 

460 

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) 

469 

470 def render_glyph(self, ox, oy, facename, font_class, sym, fontsize, dpi): 

471 """ 

472 Draw a glyph at 

473 

474 - *ox*, *oy*: position 

475 

476 - *facename*: One of the TeX face names 

477 

478 - *font_class*: 

479 

480 - *sym*: TeX symbol name or single character 

481 

482 - *fontsize*: fontsize in points 

483 

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) 

492 

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) 

498 

499 def get_xheight(self, font, fontsize, dpi): 

500 """ 

501 Get the xheight for the given *font* and *fontsize*. 

502 """ 

503 raise NotImplementedError() 

504 

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

511 

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 

519 

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 

529 

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

538 

539 

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 = {} 

549 

550 filename = findfont(default_font_prop) 

551 default_font = get_font(filename) 

552 self._fonts['default'] = default_font 

553 self._fonts['regular'] = default_font 

554 

555 def destroy(self): 

556 self.glyphd = None 

557 Fonts.destroy(self) 

558 

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 

571 

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. 

576 

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 

582 

583 font, num, symbol_name, fontsize, slanted = \ 

584 self._get_glyph(fontname, font_class, sym, fontsize, math) 

585 

586 font.set_size(fontsize, dpi) 

587 glyph = font.load_char( 

588 num, 

589 flags=self.mathtext_backend.get_hinting_type()) 

590 

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 ) 

605 

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 

617 

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 

629 

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 

635 

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) 

645 

646 

647class BakomaFonts(TruetypeFonts): 

648 """ 

649 Use the Bakoma TrueType fonts for rendering. 

650 

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 } 

663 

664 def __init__(self, *args, **kwargs): 

665 self._stix_fallback = StixFonts(*args, **kwargs) 

666 

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 

673 

674 _slanted_symbols = set(r"\int \oint".split()) 

675 

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) 

688 

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) 

693 

694 if symbol_name is None: 

695 return self._stix_fallback._get_glyph( 

696 fontname, font_class, sym, fontsize, math) 

697 

698 return font, num, symbol_name, fontsize, slanted 

699 

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 } 

743 

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] 

755 

756 def get_sized_alternatives_for_symbol(self, fontname, sym): 

757 return self._size_alternatives.get(sym, [(fontname, sym)]) 

758 

759 

760class UnicodeFonts(TruetypeFonts): 

761 """ 

762 An abstract base class for handling Unicode fonts. 

763 

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. 

767 

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 

772 

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 

788 

789 _slanted_symbols = set(r"\int \oint".split()) 

790 

791 def _map_virtual_font(self, fontname, font_class, uniindex): 

792 return fontname, uniindex 

793 

794 def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): 

795 found_symbol = False 

796 

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 

802 

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

811 

812 fontname, uniindex = self._map_virtual_font( 

813 fontname, font_class, uniindex) 

814 

815 new_fontname = fontname 

816 

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' 

825 

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 

833 

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 

858 

859 symbol_name = font.get_glyph_name(glyphindex) 

860 return font, uniindex, symbol_name, fontsize, slanted 

861 

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

867 

868 

869class DejaVuFonts(UnicodeFonts): 

870 use_cmex = False 

871 

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 

893 

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) 

911 

912 

913class DejaVuSerifFonts(DejaVuFonts): 

914 """ 

915 A font handling class for the DejaVu Serif fonts 

916 

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 } 

928 

929 

930class DejaVuSansFonts(DejaVuFonts): 

931 """ 

932 A font handling class for the DejaVu Sans fonts 

933 

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 } 

945 

946 

947class StixFonts(UnicodeFonts): 

948 """ 

949 A font handling class for the STIX fonts. 

950 

951 In addition to what UnicodeFonts provides, this class: 

952 

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

956 

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 

976 

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 

984 

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 

995 

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'] 

1002 

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 

1015 

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'] 

1023 

1024 # Handle private use area glyphs 

1025 if fontname in ('it', 'rm', 'bf') and 0xe000 <= uniindex <= 0xf8ff: 

1026 fontname = 'nonuni' + fontname 

1027 

1028 return fontname, uniindex 

1029 

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 

1049 

1050 

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 

1057 

1058 

1059class StandardPsFonts(Fonts): 

1060 """ 

1061 Use the standard postscript fonts for rendering to backend_ps 

1062 

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

1067 

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 } 

1077 

1078 def __init__(self, default_font_prop): 

1079 Fonts.__init__(self, default_font_prop, MathtextBackendPs()) 

1080 self.glyphd = {} 

1081 self.fonts = {} 

1082 

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 

1091 

1092 self.fonts['default'] = default_font 

1093 self.fonts['regular'] = default_font 

1094 self.pswriter = StringIO() 

1095 

1096 def _get_font(self, font): 

1097 if font in self.fontmap: 

1098 basename = self.fontmap[font] 

1099 else: 

1100 basename = font 

1101 

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 

1111 

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) 

1116 

1117 if tup is not None: 

1118 return tup 

1119 

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' 

1126 

1127 found_symbol = False 

1128 

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

1140 

1141 slanted = (fontname == 'it') 

1142 font = self._get_font(fontname) 

1143 

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 

1152 

1153 if not found_symbol: 

1154 glyph = '?' 

1155 num = ord(glyph) 

1156 symbol_name = font.get_name_char(glyph) 

1157 

1158 offset = 0 

1159 

1160 scale = 0.001 * fontsize 

1161 

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 ) 

1176 

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 ) 

1187 

1188 return self.glyphd[key] 

1189 

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) 

1200 

1201 def get_xheight(self, font, fontsize, dpi): 

1202 font = self._get_font(font) 

1203 return font.get_xheight() * 0.001 * fontsize 

1204 

1205 def get_underline_thickness(self, font, fontsize, dpi): 

1206 font = self._get_font(font) 

1207 return font.get_underline_thickness() * 0.001 * fontsize 

1208 

1209 

1210############################################################################## 

1211# TeX-LIKE BOX MODEL 

1212 

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. 

1233 

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 

1241 

1242 

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 

1251 

1252 # Percentage of x-height that sub/superscripts drop below the baseline 

1253 subdrop = 0.4 

1254 

1255 # Percentage of x-height that superscripts are raised from the baseline 

1256 sup1 = 0.7 

1257 

1258 # Percentage of x-height that subscripts drop below the baseline 

1259 sub1 = 0.3 

1260 

1261 # Percentage of x-height that subscripts drop below the baseline when a 

1262 # superscript is present 

1263 sub2 = 0.5 

1264 

1265 # Percentage of x-height that sub/supercripts are offset relative to the 

1266 # nucleus edge for non-slanted nuclei 

1267 delta = 0.025 

1268 

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 

1273 

1274 # Percentage of x-height that supercripts and subscripts are offset for 

1275 # integrals 

1276 delta_integral = 0.1 

1277 

1278 

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 

1288 

1289 

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 

1297 

1298 

1299class STIXSansFontConstants(FontConstantsBase): 

1300 script_space = 0.05 

1301 sup1 = 0.8 

1302 delta_slanted = 0.6 

1303 delta_integral = 0.3 

1304 

1305 

1306class DejaVuSerifFontConstants(FontConstantsBase): 

1307 pass 

1308 

1309 

1310class DejaVuSansFontConstants(FontConstantsBase): 

1311 pass 

1312 

1313 

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 } 

1337 

1338 

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 

1349 

1350 

1351class MathTextWarning(Warning): 

1352 pass 

1353 

1354 

1355class Node: 

1356 """ 

1357 A node in the TeX box model 

1358 """ 

1359 def __init__(self): 

1360 self.size = 0 

1361 

1362 def __repr__(self): 

1363 return self.__class__.__name__ 

1364 

1365 def get_kerning(self, next): 

1366 return 0.0 

1367 

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 

1374 

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 

1381 

1382 def render(self, x, y): 

1383 pass 

1384 

1385 

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 

1395 

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 

1402 

1403 def grow(self): 

1404 Node.grow(self) 

1405 self.width *= GROW_FACTOR 

1406 self.height *= GROW_FACTOR 

1407 self.depth *= GROW_FACTOR 

1408 

1409 def render(self, x1, y1, x2, y2): 

1410 pass 

1411 

1412 

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) 

1419 

1420 

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

1427 

1428 

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

1452 

1453 def __repr__(self): 

1454 return '`%s`' % self.c 

1455 

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) 

1466 

1467 def is_slanted(self): 

1468 return self._metrics.slanted 

1469 

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 

1484 

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) 

1492 

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 

1500 

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 

1507 

1508 

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 

1521 

1522 def shrink(self): 

1523 Char.shrink(self) 

1524 self._update_metrics() 

1525 

1526 def grow(self): 

1527 Char.grow(self) 

1528 self._update_metrics() 

1529 

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) 

1537 

1538 

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 

1551 

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

1558 

1559 @staticmethod 

1560 def _determine_order(totals): 

1561 """ 

1562 Determine the highest order of glue used by the members of this list. 

1563 

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 

1570 

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) 

1584 

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 

1592 

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 

1599 

1600 

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

1610 

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 

1628 

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 

1635 

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 

1649 

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. 

1659 

1660 - *w*: specifies a width 

1661 

1662 - *m*: is either 'exactly' or 'additional'. 

1663 

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 

1697 

1698 if m == 'additional': 

1699 w += x 

1700 self.width = w 

1701 x = w - x 

1702 

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

1712 

1713 

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

1721 

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. 

1727 

1728 - *h*: specifies a height 

1729 - *m*: is either 'exactly' or 'additional'. 

1730 - *l*: a maximum height 

1731 

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

1765 

1766 self.width = w 

1767 if d > l: 

1768 x += d - l 

1769 self.depth = l 

1770 else: 

1771 self.depth = d 

1772 

1773 if m == 'additional': 

1774 h += x 

1775 self.height = h 

1776 x = h - x 

1777 

1778 if x == 0: 

1779 self.glue_sign = 0 

1780 self.glue_order = 0 

1781 self.glue_ratio = 0. 

1782 return 

1783 

1784 if x > 0.: 

1785 self._set_glue(x, 1, total_stretch, "Overfull") 

1786 else: 

1787 self._set_glue(x, -1, total_shrink, "Underfull") 

1788 

1789 

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 

1803 

1804 def render(self, x, y, w, h): 

1805 self.font_output.render_rect_filled(x, y, x + w, y + h) 

1806 

1807 

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) 

1818 

1819 

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) 

1828 

1829 

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 

1849 

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 

1856 

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 

1862 

1863 

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 

1875 

1876 def copy(self): 

1877 return GlueSpec( 

1878 self.width, 

1879 self.stretch, 

1880 self.stretch_order, 

1881 self.shrink, 

1882 self.shrink_order) 

1883 

1884 @classmethod 

1885 def factory(cls, glue_type): 

1886 return cls._types[glue_type] 

1887 

1888 

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} 

1899 

1900 

1901# Some convenient ways to get common kinds of glue 

1902 

1903 

1904class Fil(Glue): 

1905 def __init__(self): 

1906 Glue.__init__(self, 'fil') 

1907 

1908 

1909class Fill(Glue): 

1910 def __init__(self): 

1911 Glue.__init__(self, 'fill') 

1912 

1913 

1914class Filll(Glue): 

1915 def __init__(self): 

1916 Glue.__init__(self, 'filll') 

1917 

1918 

1919class NegFil(Glue): 

1920 def __init__(self): 

1921 Glue.__init__(self, 'neg_fil') 

1922 

1923 

1924class NegFill(Glue): 

1925 def __init__(self): 

1926 Glue.__init__(self, 'neg_fill') 

1927 

1928 

1929class NegFilll(Glue): 

1930 def __init__(self): 

1931 Glue.__init__(self, 'neg_filll') 

1932 

1933 

1934class SsGlue(Glue): 

1935 def __init__(self): 

1936 Glue.__init__(self, 'ss') 

1937 

1938 

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) 

1947 

1948 

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

1956 

1957 

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 

1970 

1971 def __init__(self, width): 

1972 Node.__init__(self) 

1973 self.width = width 

1974 

1975 def __repr__(self): 

1976 return "k%.02f" % self.width 

1977 

1978 def shrink(self): 

1979 Node.shrink(self) 

1980 if self.size < NUM_SIZE_LEVELS: 

1981 self.width *= SHRINK_FACTOR 

1982 

1983 def grow(self): 

1984 Node.grow(self) 

1985 self.width *= GROW_FACTOR 

1986 

1987 

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, []) 

2001 

2002 

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) 

2014 

2015 xHeight = state.font_output.get_xheight( 

2016 state.font, state.fontsize, state.dpi) 

2017 

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 

2027 

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) 

2034 

2035 shift = (depth - char.depth) 

2036 

2037 Hlist.__init__(self, [char]) 

2038 self.shift_amount = shift 

2039 

2040 

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) 

2052 

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 

2059 

2060 factor = width / char.width 

2061 state.fontsize *= factor 

2062 char = char_class(sym, state) 

2063 

2064 Hlist.__init__(self, [char]) 

2065 self.width = char.width 

2066 

2067 

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) 

2086 

2087 @staticmethod 

2088 def clamp(value): 

2089 if value < -1000000000.: 

2090 return -1000000000. 

2091 if value > 1000000000.: 

2092 return 1000000000. 

2093 return value 

2094 

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 

2105 

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 

2157 

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 

2169 

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 

2216 

2217 

2218ship = Ship() 

2219 

2220 

2221############################################################################## 

2222# PARSER 

2223 

2224 

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) 

2231 

2232 empty = Empty() 

2233 empty.setParseAction(raise_error) 

2234 return empty 

2235 

2236 

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

2242 

2243 The grammar is based directly on that in TeX, though it cuts a few 

2244 corners. 

2245 """ 

2246 

2247 _math_style_dict = dict(displaystyle=0, textstyle=1, 

2248 scriptstyle=2, scriptscriptstyle=3) 

2249 

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

2264 

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

2277 

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

2290 

2291 _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols 

2292 

2293 _punctuation_symbols = set(r', ; . ! \ldotp \cdotp'.split()) 

2294 

2295 _overunder_symbols = set(r''' 

2296 \sum \prod \coprod \bigcap \bigcup \bigsqcup \bigvee 

2297 \bigwedge \bigodot \bigotimes \bigoplus \biguplus 

2298 '''.split()) 

2299 

2300 _overunder_functions = set( 

2301 "lim liminf limsup sup max min".split()) 

2302 

2303 _dropsub_symbols = set(r'''\int \oint'''.split()) 

2304 

2305 _fontnames = set( 

2306 "rm cal it tt sf bf default bb frak circled scr regular".split()) 

2307 

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

2312 

2313 _ambi_delim = set(""" 

2314 | \\| / \\backslash \\uparrow \\downarrow \\updownarrow \\Uparrow 

2315 \\Downarrow \\Updownarrow . \\vert \\Vert \\\\|""".split()) 

2316 

2317 _left_delim = set(r"( [ \{ < \lfloor \langle \lceil".split()) 

2318 

2319 _right_delim = set(r") ] \} > \rfloor \rangle \rceil".split()) 

2320 

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

2371 

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) 

2376 

2377 p.float_literal <<= Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)") 

2378 p.int_literal <<= Regex("[-+]?[0-9]+") 

2379 

2380 p.lbrace <<= Literal('{').suppress() 

2381 p.rbrace <<= Literal('}').suppress() 

2382 p.lbracket <<= Literal('[').suppress() 

2383 p.rbracket <<= Literal(']').suppress() 

2384 p.bslash <<= Literal('\\') 

2385 

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 ) 

2392 

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

2403 

2404 p.apostrophe <<= Regex("'+") 

2405 

2406 p.c_over_c <<= ( 

2407 Suppress(p.bslash) 

2408 + oneOf(list(self._char_over_chars)) 

2409 ) 

2410 

2411 p.accent <<= Group( 

2412 Suppress(p.bslash) 

2413 + oneOf([*self._accent_map, *self._wide_accents]) 

2414 - p.placeable 

2415 ) 

2416 

2417 p.function <<= ( 

2418 Suppress(p.bslash) 

2419 + oneOf(list(self._function_names)) 

2420 ) 

2421 

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 ) 

2429 

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 ) 

2435 

2436 p.frac <<= Group( 

2437 Suppress(Literal(r"\frac")) 

2438 - ((p.required_group + p.required_group) 

2439 | Error(r"Expected \frac{num}{den}")) 

2440 ) 

2441 

2442 p.dfrac <<= Group( 

2443 Suppress(Literal(r"\dfrac")) 

2444 - ((p.required_group + p.required_group) 

2445 | Error(r"Expected \dfrac{num}{den}")) 

2446 ) 

2447 

2448 p.stackrel <<= Group( 

2449 Suppress(Literal(r"\stackrel")) 

2450 - ((p.required_group + p.required_group) 

2451 | Error(r"Expected \stackrel{num}{den}")) 

2452 ) 

2453 

2454 p.binom <<= Group( 

2455 Suppress(Literal(r"\binom")) 

2456 - ((p.required_group + p.required_group) 

2457 | Error(r"Expected \binom{num}{den}")) 

2458 ) 

2459 

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'\}']) 

2464 

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 ) 

2478 

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 ) 

2485 

2486 p.overline <<= Group( 

2487 Suppress(Literal(r"\overline")) 

2488 - (p.required_group | Error("Expected \\overline{value}")) 

2489 ) 

2490 

2491 p.unknown_symbol <<= Combine(p.bslash + Regex("[A-Za-z]*")) 

2492 

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 ) 

2498 

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 ) 

2517 

2518 p.simple <<= ( 

2519 p.space 

2520 | p.customspace 

2521 | p.font 

2522 | p.subsuper 

2523 ) 

2524 

2525 p.subsuperop <<= oneOf(["_", "^"]) 

2526 

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 ) 

2534 

2535 p.token <<= ( 

2536 p.simple 

2537 | p.auto_delim 

2538 | p.unknown_symbol # Must be last 

2539 ) 

2540 

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 ) 

2550 

2551 p.math <<= OneOrMore(p.token) 

2552 

2553 p.math_string <<= QuotedString('$', '\\', unquoteResults=False) 

2554 

2555 p.non_math <<= Regex(r"(?:(?:\\[$])|[^$])*").leaveWhitespace() 

2556 

2557 p.main <<= ( 

2558 p.non_math + ZeroOrMore(p.math_string + p.non_math) + StringEnd() 

2559 ) 

2560 

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

2566 

2567 self._expression = p.main 

2568 self._math_expression = p.math 

2569 

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

2574 

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] 

2591 

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. 

2599 

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 

2609 

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) 

2617 

2618 @property 

2619 def font(self): 

2620 return self._font 

2621 

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 

2632 

2633 def get_state(self): 

2634 """ 

2635 Get the current :class:`State` of the parser. 

2636 """ 

2637 return self._state_stack[-1] 

2638 

2639 def pop_state(self): 

2640 """ 

2641 Pop a :class:`State` off of the stack. 

2642 """ 

2643 self._state_stack.pop() 

2644 

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

2651 

2652 def main(self, s, loc, toks): 

2653 return [Hlist(toks)] 

2654 

2655 def math_string(self, s, loc, toks): 

2656 return self._math_expression.parseString(toks[0][1:-1]) 

2657 

2658 def math(self, s, loc, toks): 

2659 hlist = Hlist(toks) 

2660 self.pop_state() 

2661 return [hlist] 

2662 

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] 

2671 

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) 

2684 

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 } 

2699 

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] 

2705 

2706 def customspace(self, s, loc, toks): 

2707 return [self._make_space(float(toks[0]))] 

2708 

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) 

2715 

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: 

2731 

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] 

2738 

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] 

2747 

2748 snowflake = symbol 

2749 

2750 def unknown_symbol(self, s, loc, toks): 

2751 c = toks[0] 

2752 raise ParseFatalException(s, loc, "Unknown symbol: %s" % c) 

2753 

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 } 

2760 

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) 

2766 

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

2771 

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) 

2777 

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) 

2783 

2784 width = max(over.width, under.width) 

2785 

2786 over_centered = HCentered([over]) 

2787 over_centered.hpack(width, 'exactly') 

2788 

2789 under_centered = HCentered([under]) 

2790 under_centered.hpack(width, 'exactly') 

2791 

2792 return Vlist([ 

2793 over_centered, 

2794 Vbox(0., thickness * space), 

2795 under_centered 

2796 ]) 

2797 

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 } 

2818 

2819 _wide_accents = set(r"widehat widetilde widebar".split()) 

2820 

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

2825 

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

2849 

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 

2858 

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

2870 

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 [] 

2877 

2878 def group(self, s, loc, toks): 

2879 grp = Hlist(toks[0]) 

2880 return [grp] 

2881 required_group = simple_group = group 

2882 

2883 def end_group(self, s, loc, toks): 

2884 self.pop_state() 

2885 return [] 

2886 

2887 def font(self, s, loc, toks): 

2888 assert len(toks) == 1 

2889 name = toks[0] 

2890 self.get_state().font = name 

2891 return [] 

2892 

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 

2899 

2900 def is_dropsub(self, nucleus): 

2901 if isinstance(nucleus, Char): 

2902 return nucleus.c in self._dropsub_symbols 

2903 return False 

2904 

2905 def is_slanted(self, nucleus): 

2906 if isinstance(nucleus, Char): 

2907 return nucleus.is_slanted() 

2908 return False 

2909 

2910 def is_between_brackets(self, s, loc): 

2911 return False 

2912 

2913 def subsuper(self, s, loc, toks): 

2914 assert len(toks) == 1 

2915 

2916 nucleus = None 

2917 sub = None 

2918 super = None 

2919 

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 

2932 

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

2968 

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) 

2974 

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

2984 

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) 

2996 

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] 

3013 

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

3038 

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 

3045 

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 

3060 

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 

3096 

3097 if not self.is_dropsub(last_char): 

3098 x.width += constants.script_space * xHeight 

3099 result = Hlist([nucleus, x]) 

3100 

3101 return [result] 

3102 

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) 

3107 

3108 rule = float(rule) 

3109 

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

3125 

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 

3135 

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 

3144 

3145 def genfrac(self, s, loc, toks): 

3146 assert len(toks) == 1 

3147 assert len(toks[0]) == 6 

3148 

3149 return self._genfrac(*tuple(toks[0])) 

3150 

3151 def frac(self, s, loc, toks): 

3152 assert len(toks) == 1 

3153 assert len(toks[0]) == 2 

3154 state = self.get_state() 

3155 

3156 thickness = state.font_output.get_underline_thickness( 

3157 state.font, state.fontsize, state.dpi) 

3158 num, den = toks[0] 

3159 

3160 return self._genfrac('', '', thickness, 

3161 self._math_style_dict['textstyle'], num, den) 

3162 

3163 def dfrac(self, s, loc, toks): 

3164 assert len(toks) == 1 

3165 assert len(toks[0]) == 2 

3166 state = self.get_state() 

3167 

3168 thickness = state.font_output.get_underline_thickness( 

3169 state.font, state.fontsize, state.dpi) 

3170 num, den = toks[0] 

3171 

3172 return self._genfrac('', '', thickness, 

3173 self._math_style_dict['displaystyle'], num, den) 

3174 

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] 

3181 

3182 return self._genfrac('', '', 0.0, 

3183 self._math_style_dict['textstyle'], num, den) 

3184 

3185 def binom(self, s, loc, toks): 

3186 assert len(toks) == 1 

3187 assert len(toks[0]) == 2 

3188 num, den = toks[0] 

3189 

3190 return self._genfrac('(', ')', 0.0, 

3191 self._math_style_dict['textstyle'], num, den) 

3192 

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) 

3198 

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 

3206 

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) 

3217 

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

3226 

3227 root_vlist = Vlist([Hlist([root])]) 

3228 root_vlist.shift_amount = -height * 0.6 

3229 

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] 

3236 

3237 def overline(self, s, loc, toks): 

3238 assert len(toks) == 1 

3239 assert len(toks[0]) == 1 

3240 

3241 body = toks[0][0] 

3242 

3243 state = self.get_state() 

3244 thickness = state.font_output.get_underline_thickness( 

3245 state.font, state.fontsize, state.dpi) 

3246 

3247 height = body.height - body.shift_amount + thickness * 3.0 

3248 depth = body.depth + body.shift_amount 

3249 

3250 # Place overline above body 

3251 rightside = Vlist([Hrule(state), 

3252 Fill(), 

3253 Hlist([body])]) 

3254 

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) 

3258 

3259 hlist = Hlist([rightside]) 

3260 return [hlist] 

3261 

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 

3283 

3284 def auto_delim(self, s, loc, toks): 

3285 front, middle, back = toks 

3286 

3287 return self._auto_sized_delimiter(front, middle.asList(), back) 

3288 

3289 

3290############################################################################## 

3291# MAIN 

3292 

3293 

3294class MathTextParser: 

3295 _parser = None 

3296 

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 } 

3315 

3316 def __init__(self, output): 

3317 """ 

3318 Create a MathTextParser for the given backend *output*. 

3319 """ 

3320 self._output = output.lower() 

3321 

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. 

3330 

3331 The results are cached, so multiple calls to :meth:`parse` 

3332 with the same expression should be fast. 

3333 """ 

3334 

3335 if prop is None: 

3336 prop = FontProperties() 

3337 

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) 

3346 

3347 fontsize = prop.get_size_in_points() 

3348 

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

3353 

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) 

3357 

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 

3368 

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 

3380 

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. 

3393 

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) 

3402 

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 

3410 

3411 def to_png(self, filename, texstr, color='black', dpi=120, fontsize=14): 

3412 r""" 

3413 Render a tex expression to a PNG file. 

3414 

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. 

3427 

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 

3439 

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. 

3448 

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 

3458 

3459 

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. 

3464 

3465 *s* 

3466 A math expression. The math portion should be enclosed in 

3467 dollar signs. 

3468 

3469 *filename_or_obj* 

3470 A filepath or writable file-like object to write the image data 

3471 to. 

3472 

3473 *prop* 

3474 If provided, a FontProperties() object describing the size and 

3475 style of the text. 

3476 

3477 *dpi* 

3478 Override the output dpi, otherwise use the default associated 

3479 with the output format. 

3480 

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 

3488 

3489 if prop is None: 

3490 prop = FontProperties() 

3491 

3492 parser = MathTextParser('path') 

3493 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop) 

3494 

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) 

3499 

3500 return depth