Repository navigation
Expand file tree
/
Copy pathmathtext.py
More file actions
3500 lines (2962 loc) · 118 KB
/
Copy pathmathtext.py
File metadata and controls
3500 lines (2962 loc) · 118 KB
Edit and raw actions
OlderNewer
1
r"""
2
:mod:`~matplotlib.mathtext` is a module for parsing a subset of the
3
TeX math syntax and drawing them to a matplotlib backend.
4
5
For a tutorial of its usage see :doc:`/tutorials/text/mathtext`. This
6
document is primarily concerned with implementation details.
7
8
The module uses pyparsing_ to parse the TeX expression.
9
10
.. _pyparsing: http://pyparsing.wikispaces.com/
11
12
The Bakoma distribution of the TeX Computer Modern fonts, and STIX
13
fonts are supported. There is experimental support for using
14
arbitrary fonts, but results may vary without proper tweaking and
15
metrics for those fonts.
16
"""
17
18
from collections import namedtuple
19
import functools
20
from io import StringIO
21
import logging
22
import os
23
import types
24
import unicodedata
25
26
import numpy as np
27
from pyparsing import (
28
Combine, Empty, FollowedBy, Forward, Group, Literal, oneOf, OneOrMore,
29
Optional, ParseBaseException, ParseFatalException, ParserElement,
30
QuotedString, Regex, StringEnd, Suppress, ZeroOrMore)
31
32
from matplotlib import cbook, colors as mcolors, rcParams
33
from matplotlib.afm import AFM
34
from matplotlib.cbook import get_realpath_and_stat
35
from matplotlib.ft2font import FT2Image, KERNING_DEFAULT, LOAD_NO_HINTING
36
from matplotlib.font_manager import findfont, FontProperties, get_font
37
from matplotlib._mathtext_data import (latex_to_bakoma, latex_to_standard,
38
tex2uni, latex_to_cmex,
39
stix_virtual_fonts)
40
41
ParserElement.enablePackrat()
42
_log = logging.getLogger(__name__)
43
44
45
##############################################################################
46
# FONTS
47
48
def 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
82
class 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
137
class 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
212
class 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
219
class 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
262
class 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
292
class 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
322
class 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
350
class 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
379
class 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
540
class 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
647
class 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
760
class 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
869
class 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
913
class 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
930
class 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
947
class 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: