/lib/matplotlib/mathtext.py
Python | 1442 lines | 1260 code | 36 blank | 146 comment | 49 complexity | cd879810c884e5c809968da2c02c0d0d MD5 | raw file
- r"""
- :mod:`~matplotlib.mathtext` is a module for parsing a subset of the
- TeX math syntax and drawing them to a matplotlib backend.
- For a tutorial of its usage see :ref:`mathtext-tutorial`. This
- document is primarily concerned with implementation details.
- The module uses pyparsing_ to parse the TeX expression.
- .. _pyparsing: http://pyparsing.wikispaces.com/
- The Bakoma distribution of the TeX Computer Modern fonts, and STIX
- fonts are supported. There is experimental support for using
- arbitrary fonts, but results may vary without proper tweaking and
- metrics for those fonts.
- If you find TeX expressions that don't parse or render properly,
- please email mdroe@stsci.edu, but please check KNOWN ISSUES below first.
- """
- from __future__ import (absolute_import, division, print_function,
- unicode_literals)
- import six
- import os, sys
- if six.PY3:
- unichr = chr
- from math import ceil
- try:
- set
- except NameError:
- from sets import Set as set
- import unicodedata
- from warnings import warn
- from numpy import inf, isinf
- import numpy as np
- import pyparsing
- from pyparsing import Combine, Group, Optional, Forward, \
- Literal, OneOrMore, ZeroOrMore, ParseException, Empty, \
- ParseResults, Suppress, oneOf, StringEnd, ParseFatalException, \
- FollowedBy, Regex, ParserElement, QuotedString, ParseBaseException
- # Enable packrat parsing
- if (six.PY3 and
- [int(x) for x in pyparsing.__version__.split('.')] < [2, 0, 0]):
- warn("Due to a bug in pyparsing <= 2.0.0 on Python 3.x, packrat parsing "
- "has been disabled. Mathtext rendering will be much slower as a "
- "result. Install pyparsing 2.0.0 or later to improve performance.")
- else:
- ParserElement.enablePackrat()
- from matplotlib.afm import AFM
- from matplotlib.cbook import Bunch, get_realpath_and_stat, \
- is_string_like, maxdict
- from matplotlib.ft2font import FT2Font, FT2Image, KERNING_DEFAULT, LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING
- from matplotlib.font_manager import findfont, FontProperties
- from matplotlib._mathtext_data import latex_to_bakoma, \
- latex_to_standard, tex2uni, latex_to_cmex, stix_virtual_fonts
- from matplotlib import get_data_path, rcParams
- import matplotlib.colors as mcolors
- import matplotlib._png as _png
- ####################
- ##############################################################################
- # FONTS
- def get_unicode_index(symbol):
- """get_unicode_index(symbol) -> integer
- Return the integer index (from the Unicode table) of symbol. *symbol*
- can be a single unicode character, a TeX command (i.e. r'\pi'), or a
- Type1 symbol name (i.e. 'phi').
- """
- # From UTF #25: U+2212 minus sign is the preferred
- # representation of the unary and binary minus sign rather than
- # the ASCII-derived U+002D hyphen-minus, because minus sign is
- # unambiguous and because it is rendered with a more desirable
- # length, usually longer than a hyphen.
- if symbol == '-':
- return 0x2212
- try:# This will succeed if symbol is a single unicode char
- return ord(symbol)
- except TypeError:
- pass
- try:# Is symbol a TeX symbol (i.e. \alpha)
- return tex2uni[symbol.strip("\\")]
- except KeyError:
- message = """'%(symbol)s' is not a valid Unicode character or
- TeX/Type1 symbol"""%locals()
- raise ValueError(message)
- def unichr_safe(index):
- """Return the Unicode character corresponding to the index,
- or the replacement character if this is a narrow build of Python
- and the requested character is outside the BMP."""
- try:
- return unichr(index)
- except ValueError:
- return unichr(0xFFFD)
- class MathtextBackend(object):
- """
- The base class for the mathtext backend-specific code. The
- purpose of :class:`MathtextBackend` subclasses is to interface
- between mathtext and a specific matplotlib graphics backend.
- Subclasses need to override the following:
- - :meth:`render_glyph`
- - :meth:`render_filled_rect`
- - :meth:`get_results`
- And optionally, if you need to use a Freetype hinting style:
- - :meth:`get_hinting_type`
- """
- def __init__(self):
- self.width = 0
- self.height = 0
- self.depth = 0
- def set_canvas_size(self, w, h, d):
- 'Dimension the drawing canvas'
- self.width = w
- self.height = h
- self.depth = d
- def render_glyph(self, ox, oy, info):
- """
- Draw a glyph described by *info* to the reference point (*ox*,
- *oy*).
- """
- raise NotImplementedError()
- def render_filled_rect(self, x1, y1, x2, y2):
- """
- Draw a filled black rectangle from (*x1*, *y1*) to (*x2*, *y2*).
- """
- raise NotImplementedError()
- def get_results(self, box):
- """
- Return a backend-specific tuple to return to the backend after
- all processing is done.
- """
- raise NotImplementedError()
- def get_hinting_type(self):
- """
- Get the Freetype hinting type to use with this particular
- backend.
- """
- return LOAD_NO_HINTING
- class MathtextBackendAgg(MathtextBackend):
- """
- Render glyphs and rectangles to an FTImage buffer, which is later
- transferred to the Agg image by the Agg backend.
- """
- def __init__(self):
- self.ox = 0
- self.oy = 0
- self.image = None
- self.mode = 'bbox'
- self.bbox = [0, 0, 0, 0]
- MathtextBackend.__init__(self)
- def _update_bbox(self, x1, y1, x2, y2):
- self.bbox = [min(self.bbox[0], x1),
- min(self.bbox[1], y1),
- max(self.bbox[2], x2),
- max(self.bbox[3], y2)]
- def set_canvas_size(self, w, h, d):
- MathtextBackend.set_canvas_size(self, w, h, d)
- if self.mode != 'bbox':
- self.image = FT2Image(ceil(w), ceil(h + d))
- def render_glyph(self, ox, oy, info):
- if self.mode == 'bbox':
- self._update_bbox(ox + info.metrics.xmin,
- oy - info.metrics.ymax,
- ox + info.metrics.xmax,
- oy - info.metrics.ymin)
- else:
- info.font.draw_glyph_to_bitmap(
- self.image, ox, oy - info.metrics.iceberg, info.glyph,
- antialiased=rcParams['text.antialiased'])
- def render_rect_filled(self, x1, y1, x2, y2):
- if self.mode == 'bbox':
- self._update_bbox(x1, y1, x2, y2)
- else:
- height = max(int(y2 - y1) - 1, 0)
- if height == 0:
- center = (y2 + y1) / 2.0
- y = int(center - (height + 1) / 2.0)
- else:
- y = int(y1)
- self.image.draw_rect_filled(int(x1), y, ceil(x2), y + height)
- def get_results(self, box, used_characters):
- self.mode = 'bbox'
- orig_height = box.height
- orig_depth = box.depth
- ship(0, 0, box)
- bbox = self.bbox
- bbox = [bbox[0] - 1, bbox[1] - 1, bbox[2] + 1, bbox[3] + 1]
- self.mode = 'render'
- self.set_canvas_size(
- bbox[2] - bbox[0],
- (bbox[3] - bbox[1]) - orig_depth,
- (bbox[3] - bbox[1]) - orig_height)
- ship(-bbox[0], -bbox[1], box)
- result = (self.ox,
- self.oy,
- self.width,
- self.height + self.depth,
- self.depth,
- self.image,
- used_characters)
- self.image = None
- return result
- def get_hinting_type(self):
- from matplotlib.backends import backend_agg
- return backend_agg.get_hinting_flag()
- class MathtextBackendBitmap(MathtextBackendAgg):
- def get_results(self, box, used_characters):
- ox, oy, width, height, depth, image, characters = \
- MathtextBackendAgg.get_results(self, box, used_characters)
- return image, depth
- class MathtextBackendPs(MathtextBackend):
- """
- Store information to write a mathtext rendering to the PostScript
- backend.
- """
- def __init__(self):
- self.pswriter = six.moves.cStringIO()
- self.lastfont = None
- def render_glyph(self, ox, oy, info):
- oy = self.height - oy + info.offset
- postscript_name = info.postscript_name
- fontsize = info.fontsize
- symbol_name = info.symbol_name
- if (postscript_name, fontsize) != self.lastfont:
- ps = """/%(postscript_name)s findfont
- %(fontsize)s scalefont
- setfont
- """ % locals()
- self.lastfont = postscript_name, fontsize
- self.pswriter.write(ps)
- ps = """%(ox)f %(oy)f moveto
- /%(symbol_name)s glyphshow\n
- """ % locals()
- self.pswriter.write(ps)
- def render_rect_filled(self, x1, y1, x2, y2):
- ps = "%f %f %f %f rectfill\n" % (x1, self.height - y2, x2 - x1, y2 - y1)
- self.pswriter.write(ps)
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- return (self.width,
- self.height + self.depth,
- self.depth,
- self.pswriter,
- used_characters)
- class MathtextBackendPdf(MathtextBackend):
- """
- Store information to write a mathtext rendering to the PDF
- backend.
- """
- def __init__(self):
- self.glyphs = []
- self.rects = []
- def render_glyph(self, ox, oy, info):
- filename = info.font.fname
- oy = self.height - oy + info.offset
- self.glyphs.append(
- (ox, oy, filename, info.fontsize,
- info.num, info.symbol_name))
- def render_rect_filled(self, x1, y1, x2, y2):
- self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1))
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- return (self.width,
- self.height + self.depth,
- self.depth,
- self.glyphs,
- self.rects,
- used_characters)
- class MathtextBackendSvg(MathtextBackend):
- """
- Store information to write a mathtext rendering to the SVG
- backend.
- """
- def __init__(self):
- self.svg_glyphs = []
- self.svg_rects = []
- def render_glyph(self, ox, oy, info):
- oy = self.height - oy + info.offset
- self.svg_glyphs.append(
- (info.font, info.fontsize, info.num, ox, oy, info.metrics))
- def render_rect_filled(self, x1, y1, x2, y2):
- self.svg_rects.append(
- (x1, self.height - y1 + 1, x2 - x1, y2 - y1))
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- svg_elements = Bunch(svg_glyphs = self.svg_glyphs,
- svg_rects = self.svg_rects)
- return (self.width,
- self.height + self.depth,
- self.depth,
- svg_elements,
- used_characters)
- class MathtextBackendPath(MathtextBackend):
- """
- Store information to write a mathtext rendering to the text path
- machinery.
- """
- def __init__(self):
- self.glyphs = []
- self.rects = []
- def render_glyph(self, ox, oy, info):
- oy = self.height - oy + info.offset
- thetext = info.num
- self.glyphs.append(
- (info.font, info.fontsize, thetext, ox, oy))
- def render_rect_filled(self, x1, y1, x2, y2):
- self.rects.append(
- (x1, self.height-y2 , x2 - x1, y2 - y1))
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- return (self.width,
- self.height + self.depth,
- self.depth,
- self.glyphs,
- self.rects)
- class MathtextBackendCairo(MathtextBackend):
- """
- Store information to write a mathtext rendering to the Cairo
- backend.
- """
- def __init__(self):
- self.glyphs = []
- self.rects = []
- def render_glyph(self, ox, oy, info):
- oy = oy - info.offset - self.height
- thetext = unichr_safe(info.num)
- self.glyphs.append(
- (info.font, info.fontsize, thetext, ox, oy))
- def render_rect_filled(self, x1, y1, x2, y2):
- self.rects.append(
- (x1, y1 - self.height, x2 - x1, y2 - y1))
- def get_results(self, box, used_characters):
- ship(0, 0, box)
- return (self.width,
- self.height + self.depth,
- self.depth,
- self.glyphs,
- self.rects)
- class Fonts(object):
- """
- An abstract base class for a system of fonts to use for mathtext.
- The class must be able to take symbol keys and font file names and
- return the character metrics. It also delegates to a backend class
- to do the actual drawing.
- """
- def __init__(self, default_font_prop, mathtext_backend):
- """
- *default_font_prop*: A
- :class:`~matplotlib.font_manager.FontProperties` object to use
- for the default non-math font, or the base font for Unicode
- (generic) font rendering.
- *mathtext_backend*: A subclass of :class:`MathTextBackend`
- used to delegate the actual rendering.
- """
- self.default_font_prop = default_font_prop
- self.mathtext_backend = mathtext_backend
- self.used_characters = {}
- def destroy(self):
- """
- Fix any cyclical references before the object is about
- to be destroyed.
- """
- self.used_characters = None
- def get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi):
- """
- Get the kerning distance for font between *sym1* and *sym2*.
- *fontX*: one of the TeX font names::
- tt, it, rm, cal, sf, bf or default/regular (non-math)
- *fontclassX*: TODO
- *symX*: a symbol in raw TeX form. e.g., '1', 'x' or '\sigma'
- *fontsizeX*: the fontsize in points
- *dpi*: the current dots-per-inch
- """
- return 0.
- def get_metrics(self, font, font_class, sym, fontsize, dpi):
- """
- *font*: one of the TeX font names::
- tt, it, rm, cal, sf, bf or default/regular (non-math)
- *font_class*: TODO
- *sym*: a symbol in raw TeX form. e.g., '1', 'x' or '\sigma'
- *fontsize*: font size in points
- *dpi*: current dots-per-inch
- Returns an object with the following attributes:
- - *advance*: The advance distance (in points) of the glyph.
- - *height*: The height of the glyph in points.
- - *width*: The width of the glyph in points.
- - *xmin*, *xmax*, *ymin*, *ymax* - the ink rectangle of the glyph
- - *iceberg* - the distance from the baseline to the top of
- the glyph. This corresponds to TeX's definition of
- "height".
- """
- info = self._get_info(font, font_class, sym, fontsize, dpi)
- return info.metrics
- def set_canvas_size(self, w, h, d):
- """
- Set the size of the buffer used to render the math expression.
- Only really necessary for the bitmap backends.
- """
- self.width, self.height, self.depth = ceil(w), ceil(h), ceil(d)
- self.mathtext_backend.set_canvas_size(self.width, self.height, self.depth)
- def render_glyph(self, ox, oy, facename, font_class, sym, fontsize, dpi):
- """
- Draw a glyph at
- - *ox*, *oy*: position
- - *facename*: One of the TeX face names
- - *font_class*:
- - *sym*: TeX symbol name or single character
- - *fontsize*: fontsize in points
- - *dpi*: The dpi to draw at.
- """
- info = self._get_info(facename, font_class, sym, fontsize, dpi)
- realpath, stat_key = get_realpath_and_stat(info.font.fname)
- used_characters = self.used_characters.setdefault(
- stat_key, (realpath, set()))
- used_characters[1].add(info.num)
- self.mathtext_backend.render_glyph(ox, oy, info)
- def render_rect_filled(self, x1, y1, x2, y2):
- """
- Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*).
- """
- self.mathtext_backend.render_rect_filled(x1, y1, x2, y2)
- def get_xheight(self, font, fontsize, dpi):
- """
- Get the xheight for the given *font* and *fontsize*.
- """
- raise NotImplementedError()
- def get_underline_thickness(self, font, fontsize, dpi):
- """
- Get the line thickness that matches the given font. Used as a
- base unit for drawing lines such as in a fraction or radical.
- """
- raise NotImplementedError()
- def get_used_characters(self):
- """
- Get the set of characters that were used in the math
- expression. Used by backends that need to subset fonts so
- they know which glyphs to include.
- """
- return self.used_characters
- def get_results(self, box):
- """
- Get the data needed by the backend to render the math
- expression. The return value is backend-specific.
- """
- result = self.mathtext_backend.get_results(box, self.get_used_characters())
- self.destroy()
- return result
- def get_sized_alternatives_for_symbol(self, fontname, sym):
- """
- Override if your font provides multiple sizes of the same
- symbol. Should return a list of symbols matching *sym* in
- various sizes. The expression renderer will select the most
- appropriate size for a given situation from this list.
- """
- return [(fontname, sym)]
- class TruetypeFonts(Fonts):
- """
- A generic base class for all font setups that use Truetype fonts
- (through FT2Font).
- """
- class CachedFont:
- def __init__(self, font):
- self.font = font
- self.charmap = font.get_charmap()
- self.glyphmap = dict(
- [(glyphind, ccode) for ccode, glyphind in six.iteritems(self.charmap)])
- def __repr__(self):
- return repr(self.font)
- def __init__(self, default_font_prop, mathtext_backend):
- Fonts.__init__(self, default_font_prop, mathtext_backend)
- self.glyphd = {}
- self._fonts = {}
- filename = findfont(default_font_prop)
- default_font = self.CachedFont(FT2Font(filename))
- self._fonts['default'] = default_font
- self._fonts['regular'] = default_font
- def destroy(self):
- self.glyphd = None
- Fonts.destroy(self)
- def _get_font(self, font):
- if font in self.fontmap:
- basename = self.fontmap[font]
- else:
- basename = font
- cached_font = self._fonts.get(basename)
- if cached_font is None and os.path.exists(basename):
- font = FT2Font(basename)
- cached_font = self.CachedFont(font)
- self._fonts[basename] = cached_font
- self._fonts[font.postscript_name] = cached_font
- self._fonts[font.postscript_name.lower()] = cached_font
- return cached_font
- def _get_offset(self, cached_font, glyph, fontsize, dpi):
- if cached_font.font.postscript_name == 'Cmex10':
- return ((glyph.height/64.0/2.0) + (fontsize/3.0 * dpi/72.0))
- return 0.
- def _get_info(self, fontname, font_class, sym, fontsize, dpi):
- key = fontname, font_class, sym, fontsize, dpi
- bunch = self.glyphd.get(key)
- if bunch is not None:
- return bunch
- cached_font, num, symbol_name, fontsize, slanted = \
- self._get_glyph(fontname, font_class, sym, fontsize)
- font = cached_font.font
- font.set_size(fontsize, dpi)
- glyph = font.load_char(
- num,
- flags=self.mathtext_backend.get_hinting_type())
- xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox]
- offset = self._get_offset(cached_font, glyph, fontsize, dpi)
- metrics = Bunch(
- advance = glyph.linearHoriAdvance/65536.0,
- height = glyph.height/64.0,
- width = glyph.width/64.0,
- xmin = xmin,
- xmax = xmax,
- ymin = ymin+offset,
- ymax = ymax+offset,
- # iceberg is the equivalent of TeX's "height"
- iceberg = glyph.horiBearingY/64.0 + offset,
- slanted = slanted
- )
- result = self.glyphd[key] = Bunch(
- font = font,
- fontsize = fontsize,
- postscript_name = font.postscript_name,
- metrics = metrics,
- symbol_name = symbol_name,
- num = num,
- glyph = glyph,
- offset = offset
- )
- return result
- def get_xheight(self, font, fontsize, dpi):
- cached_font = self._get_font(font)
- cached_font.font.set_size(fontsize, dpi)
- pclt = cached_font.font.get_sfnt_table('pclt')
- if pclt is None:
- # Some fonts don't store the xHeight, so we do a poor man's xHeight
- metrics = self.get_metrics(font, rcParams['mathtext.default'], 'x', fontsize, dpi)
- return metrics.iceberg
- xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0)
- return xHeight
- def get_underline_thickness(self, font, fontsize, dpi):
- # This function used to grab underline thickness from the font
- # metrics, but that information is just too un-reliable, so it
- # is now hardcoded.
- return ((0.75 / 12.0) * fontsize * dpi) / 72.0
- def get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi):
- if font1 == font2 and fontsize1 == fontsize2:
- info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
- info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
- font = info1.font
- return font.get_kerning(info1.num, info2.num, KERNING_DEFAULT) / 64.0
- return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi)
- class BakomaFonts(TruetypeFonts):
- """
- Use the Bakoma TrueType fonts for rendering.
- Symbols are strewn about a number of font files, each of which has
- its own proprietary 8-bit encoding.
- """
- _fontmap = { 'cal' : 'cmsy10',
- 'rm' : 'cmr10',
- 'tt' : 'cmtt10',
- 'it' : 'cmmi10',
- 'bf' : 'cmb10',
- 'sf' : 'cmss10',
- 'ex' : 'cmex10'
- }
- def __init__(self, *args, **kwargs):
- self._stix_fallback = StixFonts(*args, **kwargs)
- TruetypeFonts.__init__(self, *args, **kwargs)
- self.fontmap = {}
- for key, val in six.iteritems(self._fontmap):
- fullpath = findfont(val)
- self.fontmap[key] = fullpath
- self.fontmap[val] = fullpath
- _slanted_symbols = set(r"\int \oint".split())
- def _get_glyph(self, fontname, font_class, sym, fontsize):
- symbol_name = None
- if fontname in self.fontmap and sym in latex_to_bakoma:
- basename, num = latex_to_bakoma[sym]
- slanted = (basename == "cmmi10") or sym in self._slanted_symbols
- cached_font = self._get_font(basename)
- if cached_font is not None:
- symbol_name = cached_font.font.get_glyph_name(num)
- num = cached_font.glyphmap[num]
- elif len(sym) == 1:
- slanted = (fontname == "it")
- cached_font = self._get_font(fontname)
- if cached_font is not None:
- num = ord(sym)
- gid = cached_font.charmap.get(num)
- if gid is not None:
- symbol_name = cached_font.font.get_glyph_name(
- cached_font.charmap[num])
- if symbol_name is None:
- return self._stix_fallback._get_glyph(
- fontname, font_class, sym, fontsize)
- return cached_font, num, symbol_name, fontsize, slanted
- # The Bakoma fonts contain many pre-sized alternatives for the
- # delimiters. The AutoSizedChar class will use these alternatives
- # and select the best (closest sized) glyph.
- _size_alternatives = {
- '(' : [('rm', '('), ('ex', '\xa1'), ('ex', '\xb3'),
- ('ex', '\xb5'), ('ex', '\xc3')],
- ')' : [('rm', ')'), ('ex', '\xa2'), ('ex', '\xb4'),
- ('ex', '\xb6'), ('ex', '\x21')],
- '{' : [('cal', '{'), ('ex', '\xa9'), ('ex', '\x6e'),
- ('ex', '\xbd'), ('ex', '\x28')],
- '}' : [('cal', '}'), ('ex', '\xaa'), ('ex', '\x6f'),
- ('ex', '\xbe'), ('ex', '\x29')],
- # The fourth size of '[' is mysteriously missing from the BaKoMa
- # font, so I've ommitted it for both '[' and ']'
- '[' : [('rm', '['), ('ex', '\xa3'), ('ex', '\x68'),
- ('ex', '\x22')],
- ']' : [('rm', ']'), ('ex', '\xa4'), ('ex', '\x69'),
- ('ex', '\x23')],
- r'\lfloor' : [('ex', '\xa5'), ('ex', '\x6a'),
- ('ex', '\xb9'), ('ex', '\x24')],
- r'\rfloor' : [('ex', '\xa6'), ('ex', '\x6b'),
- ('ex', '\xba'), ('ex', '\x25')],
- r'\lceil' : [('ex', '\xa7'), ('ex', '\x6c'),
- ('ex', '\xbb'), ('ex', '\x26')],
- r'\rceil' : [('ex', '\xa8'), ('ex', '\x6d'),
- ('ex', '\xbc'), ('ex', '\x27')],
- r'\langle' : [('ex', '\xad'), ('ex', '\x44'),
- ('ex', '\xbf'), ('ex', '\x2a')],
- r'\rangle' : [('ex', '\xae'), ('ex', '\x45'),
- ('ex', '\xc0'), ('ex', '\x2b')],
- r'\__sqrt__' : [('ex', '\x70'), ('ex', '\x71'),
- ('ex', '\x72'), ('ex', '\x73')],
- r'\backslash': [('ex', '\xb2'), ('ex', '\x2f'),
- ('ex', '\xc2'), ('ex', '\x2d')],
- r'/' : [('rm', '/'), ('ex', '\xb1'), ('ex', '\x2e'),
- ('ex', '\xcb'), ('ex', '\x2c')],
- r'\widehat' : [('rm', '\x5e'), ('ex', '\x62'), ('ex', '\x63'),
- ('ex', '\x64')],
- r'\widetilde': [('rm', '\x7e'), ('ex', '\x65'), ('ex', '\x66'),
- ('ex', '\x67')],
- r'<' : [('cal', 'h'), ('ex', 'D')],
- r'>' : [('cal', 'i'), ('ex', 'E')]
- }
- for alias, target in [('\leftparen', '('),
- ('\rightparent', ')'),
- ('\leftbrace', '{'),
- ('\rightbrace', '}'),
- ('\leftbracket', '['),
- ('\rightbracket', ']'),
- (r'\{', '{'),
- (r'\}', '}'),
- (r'\[', '['),
- (r'\]', ']')]:
- _size_alternatives[alias] = _size_alternatives[target]
- def get_sized_alternatives_for_symbol(self, fontname, sym):
- return self._size_alternatives.get(sym, [(fontname, sym)])
- class UnicodeFonts(TruetypeFonts):
- """
- An abstract base class for handling Unicode fonts.
- While some reasonably complete Unicode fonts (such as DejaVu) may
- work in some situations, the only Unicode font I'm aware of with a
- complete set of math symbols is STIX.
- This class will "fallback" on the Bakoma fonts when a required
- symbol can not be found in the font.
- """
- use_cmex = True
- def __init__(self, *args, **kwargs):
- # This must come first so the backend's owner is set correctly
- if rcParams['mathtext.fallback_to_cm']:
- self.cm_fallback = BakomaFonts(*args, **kwargs)
- else:
- self.cm_fallback = None
- TruetypeFonts.__init__(self, *args, **kwargs)
- self.fontmap = {}
- for texfont in "cal rm tt it bf sf".split():
- prop = rcParams['mathtext.' + texfont]
- font = findfont(prop)
- self.fontmap[texfont] = font
- prop = FontProperties('cmex10')
- font = findfont(prop)
- self.fontmap['ex'] = font
- _slanted_symbols = set(r"\int \oint".split())
- def _map_virtual_font(self, fontname, font_class, uniindex):
- return fontname, uniindex
- def _get_glyph(self, fontname, font_class, sym, fontsize):
- found_symbol = False
- if self.use_cmex:
- uniindex = latex_to_cmex.get(sym)
- if uniindex is not None:
- fontname = 'ex'
- found_symbol = True
- if not found_symbol:
- try:
- uniindex = get_unicode_index(sym)
- found_symbol = True
- except ValueError:
- uniindex = ord('?')
- warn("No TeX to unicode mapping for '%s'" %
- sym.encode('ascii', 'backslashreplace'),
- MathTextWarning)
- fontname, uniindex = self._map_virtual_font(
- fontname, font_class, uniindex)
- new_fontname = fontname
- # Only characters in the "Letter" class should be italicized in 'it'
- # mode. Greek capital letters should be Roman.
- if found_symbol:
- if fontname == 'it':
- if uniindex < 0x10000:
- unistring = unichr(uniindex)
- if (not unicodedata.category(unistring)[0] == "L"
- or unicodedata.name(unistring).startswith("GREEK CAPITAL")):
- new_fontname = 'rm'
- slanted = (new_fontname == 'it') or sym in self._slanted_symbols
- found_symbol = False
- cached_font = self._get_font(new_fontname)
- if cached_font is not None:
- try:
- glyphindex = cached_font.charmap[uniindex]
- found_symbol = True
- except KeyError:
- pass
- if not found_symbol:
- if self.cm_fallback:
- warn("Substituting with a symbol from Computer Modern.",
- MathTextWarning)
- return self.cm_fallback._get_glyph(
- fontname, 'it', sym, fontsize)
- else:
- if fontname in ('it', 'regular') and isinstance(self, StixFonts):
- return self._get_glyph('rm', font_class, sym, fontsize)
- warn("Font '%s' does not have a glyph for '%s' [U%x]" %
- (new_fontname, sym.encode('ascii', 'backslashreplace'), uniindex),
- MathTextWarning)
- warn("Substituting with a dummy symbol.", MathTextWarning)
- fontname = 'rm'
- new_fontname = fontname
- cached_font = self._get_font(fontname)
- uniindex = 0xA4 # currency character, for lack of anything better
- glyphindex = cached_font.charmap[uniindex]
- slanted = False
- symbol_name = cached_font.font.get_glyph_name(glyphindex)
- return cached_font, uniindex, symbol_name, fontsize, slanted
- def get_sized_alternatives_for_symbol(self, fontname, sym):
- if self.cm_fallback:
- return self.cm_fallback.get_sized_alternatives_for_symbol(
- fontname, sym)
- return [(fontname, sym)]
- class StixFonts(UnicodeFonts):
- """
- A font handling class for the STIX fonts.
- In addition to what UnicodeFonts provides, this class:
- - supports "virtual fonts" which are complete alpha numeric
- character sets with different font styles at special Unicode
- code points, such as "Blackboard".
- - handles sized alternative characters for the STIXSizeX fonts.
- """
- _fontmap = { 'rm' : 'STIXGeneral',
- 'it' : 'STIXGeneral:italic',
- 'bf' : 'STIXGeneral:weight=bold',
- 'nonunirm' : 'STIXNonUnicode',
- 'nonuniit' : 'STIXNonUnicode:italic',
- 'nonunibf' : 'STIXNonUnicode:weight=bold',
- 0 : 'STIXGeneral',
- 1 : 'STIXSizeOneSym',
- 2 : 'STIXSizeTwoSym',
- 3 : 'STIXSizeThreeSym',
- 4 : 'STIXSizeFourSym',
- 5 : 'STIXSizeFiveSym'
- }
- use_cmex = False
- cm_fallback = False
- _sans = False
- def __init__(self, *args, **kwargs):
- TruetypeFonts.__init__(self, *args, **kwargs)
- self.fontmap = {}
- for key, name in six.iteritems(self._fontmap):
- fullpath = findfont(name)
- self.fontmap[key] = fullpath
- self.fontmap[name] = fullpath
- def _map_virtual_font(self, fontname, font_class, uniindex):
- # Handle these "fonts" that are actually embedded in
- # other fonts.
- mapping = stix_virtual_fonts.get(fontname)
- if (self._sans and mapping is None and
- fontname not in ('regular', 'default')):
- mapping = stix_virtual_fonts['sf']
- doing_sans_conversion = True
- else:
- doing_sans_conversion = False
- if mapping is not None:
- if isinstance(mapping, dict):
- mapping = mapping.get(font_class, 'rm')
- # Binary search for the source glyph
- lo = 0
- hi = len(mapping)
- while lo < hi:
- mid = (lo+hi)//2
- range = mapping[mid]
- if uniindex < range[0]:
- hi = mid
- elif uniindex <= range[1]:
- break
- else:
- lo = mid + 1
- if uniindex >= range[0] and uniindex <= range[1]:
- uniindex = uniindex - range[0] + range[3]
- fontname = range[2]
- elif not doing_sans_conversion:
- # This will generate a dummy character
- uniindex = 0x1
- fontname = rcParams['mathtext.default']
- # Handle private use area glyphs
- if (fontname in ('it', 'rm', 'bf') and
- uniindex >= 0xe000 and uniindex <= 0xf8ff):
- fontname = 'nonuni' + fontname
- return fontname, uniindex
- _size_alternatives = {}
- def get_sized_alternatives_for_symbol(self, fontname, sym):
- fixes = {'\{': '{', '\}': '}', '\[': '[', '\]': ']'}
- sym = fixes.get(sym, sym)
- alternatives = self._size_alternatives.get(sym)
- if alternatives:
- return alternatives
- alternatives = []
- try:
- uniindex = get_unicode_index(sym)
- except ValueError:
- return [(fontname, sym)]
- fix_ups = {
- ord('<'): 0x27e8,
- ord('>'): 0x27e9 }
- uniindex = fix_ups.get(uniindex, uniindex)
- for i in range(6):
- cached_font = self._get_font(i)
- glyphindex = cached_font.charmap.get(uniindex)
- if glyphindex is not None:
- alternatives.append((i, unichr_safe(uniindex)))
- # The largest size of the radical symbol in STIX has incorrect
- # metrics that cause it to be disconnected from the stem.
- if sym == r'\__sqrt__':
- alternatives = alternatives[:-1]
- self._size_alternatives[sym] = alternatives
- return alternatives
- class StixSansFonts(StixFonts):
- """
- A font handling class for the STIX fonts (that uses sans-serif
- characters by default).
- """
- _sans = True
- class StandardPsFonts(Fonts):
- """
- Use the standard postscript fonts for rendering to backend_ps
- Unlike the other font classes, BakomaFont and UnicodeFont, this
- one requires the Ps backend.
- """
- basepath = os.path.join( get_data_path(), 'fonts', 'afm' )
- fontmap = { 'cal' : 'pzcmi8a', # Zapf Chancery
- 'rm' : 'pncr8a', # New Century Schoolbook
- 'tt' : 'pcrr8a', # Courier
- 'it' : 'pncri8a', # New Century Schoolbook Italic
- 'sf' : 'phvr8a', # Helvetica
- 'bf' : 'pncb8a', # New Century Schoolbook Bold
- None : 'psyr' # Symbol
- }
- def __init__(self, default_font_prop):
- Fonts.__init__(self, default_font_prop, MathtextBackendPs())
- self.glyphd = {}
- self.fonts = {}
- filename = findfont(default_font_prop, fontext='afm',
- directory=self.basepath)
- if filename is None:
- filename = findfont('Helvetica', fontext='afm',
- directory=self.basepath)
- with open(filename, 'r') as fd:
- default_font = AFM(fd)
- default_font.fname = filename
- self.fonts['default'] = default_font
- self.fonts['regular'] = default_font
- self.pswriter = six.moves.cStringIO()
- def _get_font(self, font):
- if font in self.fontmap:
- basename = self.fontmap[font]
- else:
- basename = font
- cached_font = self.fonts.get(basename)
- if cached_font is None:
- fname = os.path.join(self.basepath, basename + ".afm")
- with open(fname, 'r') as fd:
- cached_font = AFM(fd)
- cached_font.fname = fname
- self.fonts[basename] = cached_font
- self.fonts[cached_font.get_fontname()] = cached_font
- return cached_font
- def _get_info (self, fontname, font_class, sym, fontsize, dpi):
- 'load the cmfont, metrics and glyph with caching'
- key = fontname, sym, fontsize, dpi
- tup = self.glyphd.get(key)
- if tup is not None:
- return tup
- # Only characters in the "Letter" class should really be italicized.
- # This class includes greek letters, so we're ok
- if (fontname == 'it' and
- (len(sym) > 1 or
- not unicodedata.category(six.text_type(sym)).startswith("L"))):
- fontname = 'rm'
- found_symbol = False
- if sym in latex_to_standard:
- fontname, num = latex_to_standard[sym]
- glyph = chr(num)
- found_symbol = True
- elif len(sym) == 1:
- glyph = sym
- num = ord(glyph)
- found_symbol = True
- else:
- warn("No TeX to built-in Postscript mapping for '%s'" % sym,
- MathTextWarning)
- slanted = (fontname == 'it')
- font = self._get_font(fontname)
- if found_symbol:
- try:
- symbol_name = font.get_name_char(glyph)
- except KeyError:
- warn("No glyph in standard Postscript font '%s' for '%s'" %
- (font.postscript_name, sym),
- MathTextWarning)
- found_symbol = False
- if not found_symbol:
- glyph = sym = '?'
- num = ord(glyph)
- symbol_name = font.get_name_char(glyph)
- offset = 0
- scale = 0.001 * fontsize
- xmin, ymin, xmax, ymax = [val * scale
- for val in font.get_bbox_char(glyph)]
- metrics = Bunch(
- advance = font.get_width_char(glyph) * scale,
- width = font.get_width_char(glyph) * scale,
- height = font.get_height_char(glyph) * scale,
- xmin = xmin,
- xmax = xmax,
- ymin = ymin+offset,
- ymax = ymax+offset,
- # iceberg is the equivalent of TeX's "height"
- iceberg = ymax + offset,
- slanted = slanted
- )
- self.glyphd[key] = Bunch(
- font = font,
- fontsize = fontsize,
- postscript_name = font.get_fontname(),
- metrics = metrics,
- symbol_name = symbol_name,
- num = num,
- glyph = glyph,
- offset = offset
- )
- return self.glyphd[key]
- def get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi):
- if font1 == font2 and fontsize1 == fontsize2:
- info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi)
- info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi)
- font = info1.font
- return (font.get_kern_dist(info1.glyph, info2.glyph)
- * 0.001 * fontsize1)
- return Fonts.get_kern(self, font1, fontclass1, sym1, fontsize1,
- font2, fontclass2, sym2, fontsize2, dpi)
- def get_xheight(self, font, fontsize, dpi):
- cached_font = self._get_font(font)
- return cached_font.get_xheight() * 0.001 * fontsize
- def get_underline_thickness(self, font, fontsize, dpi):
- cached_font = self._get_font(font)
- return cached_font.get_underline_thickness() * 0.001 * fontsize
- ##############################################################################
- # TeX-LIKE BOX MODEL
- # The following is based directly on the document 'woven' from the
- # TeX82 source code. This information is also available in printed
- # form:
- #
- # Knuth, Donald E.. 1986. Computers and Typesetting, Volume B:
- # TeX: The Program. Addison-Wesley Professional.
- #
- # The most relevant "chapters" are:
- # Data structures for boxes and their friends
- # Shipping pages out (Ship class)
- # Packaging (hpack and vpack)
- # Data structures for math mode
- # Subroutines for math mode
- # Typesetting math formulas
- #
- # Many of the docstrings below refer to a numbered "node" in that
- # book, e.g., node123
- #
- # Note that (as TeX) y increases downward, unlike many other parts of
- # matplotlib.
- # How much text shrinks when going to the next-smallest level. GROW_FACTOR
- # must be the inverse of SHRINK_FACTOR.
- SHRINK_FACTOR = 0.7
- GROW_FACTOR = 1.0 / SHRINK_FACTOR
- # The number of different sizes of chars to use, beyond which they will not
- # get any smaller
- NUM_SIZE_LEVELS = 6
- # Percentage of x-height of additional horiz. space after sub/superscripts
- SCRIPT_SPACE = 0.2
- # Percentage of x-height that sub/superscripts drop below the baseline
- SUBDROP = 0.3
- # Percentage of x-height that superscripts drop below the baseline
- SUP1 = 0.5
- # Percentage of x-height that subscripts drop below the baseline
- SUB1 = 0.0
- # Percentage of x-height that superscripts are offset relative to the subscript
- DELTA = 0.18
- class MathTextWarning(Warning):
- pass
- class Node(object):
- """
- A node in the TeX box model
- """
- def __init__(self):
- self.size = 0
- def __repr__(self):
- return self.__internal_repr__()
- def __internal_repr__(self):
- return self.__class__.__name__
- def get_kerning(self, next):
- return 0.0
- def shrink(self):
- """
- Shrinks one level smaller. There are only three levels of
- sizes, after which things will no longer get smaller.
- """
- self.size += 1
- def grow(self):
- """
- Grows one level larger. There is no limit to how big
- something can get.
- """
- self.size -= 1
- def render(self, x, y):
- pass
- class Box(Node):
- """
- Represents any node with a physical location.
- """
- def __init__(self, width, height, depth):
- Node.__init__(self)
- self.width = width
- self.height = height
- self.depth = depth
- def shrink(self):
- Node.shrink(self)
- if self.size < NUM_SIZE_LEVELS:
- self.width *= SHRINK_FACTOR
- self.height *= SHRINK_FACTOR
- self.depth *= SHRINK_FACTOR
- def grow(self):
- Node.grow(self)
- self.width *= GROW_FACTOR
- self.height *= GROW_FACTOR
- self.depth *= GROW_FACTOR
- def render(self, x1, y1, x2, y2):
- pass
- class Vbox(Box):
- """
- A box with only height (zero width).
- """
- def __init__(self, height, depth):
- Box.__init__(self, 0., height, depth)
- class Hbox(Box):
- """
- A box with only width (zero height and depth).
- """
- def __init__(self, width):
- Box.__init__(self, width, 0., 0.)
- class Char(Node):
- """
- Represents a single character. Unlike TeX, the font information
- and metrics are stored with each :class:`Char` to make it easier
- to lookup the font metrics when needed. Note that TeX boxes have
- a width, height, and depth, unlike Type1 and Truetype which use a
- full bounding box and an advance in the x-direction. The metrics
- must be converted to the TeX way, and the advance (if different
- from width) must be converted into a :class:`Kern` node when the
- :class:`Char` is added to its parent :class:`Hlist`.
- """
- def __init__(self, c, state):
- Node.__init__(self)
- self.c = c
- self.font_output = state.font_output
- assert isinstance(state.font, (six.string_types, int))
- self.font = state.font
- self.font_class = state.font_class
- self.fontsize = state.fontsize
- self.dpi = state.dpi
- # The real width, height and depth will be set during the
- # pack phase, after we know the real fontsize
- self._update_metrics()
- def __internal_repr__(self):
- return '`%s`' % self.c
- def _update_metrics(self):
- metrics = self._metrics = self.font_output.get_metrics(
- self.font, self.font_class, self.c, self.fontsize, self.dpi)
- if self.c == ' ':
- self.width = metrics.advance
- else:
- self.width = metrics.width
- self.height = metrics.iceberg
- self.depth = -(metrics.iceberg - metrics.height)
- def is_slanted(self):
- return self._metrics.slanted
- def get_kerning(self, next):
- """
- Return the amount of kerning between this and the given
- character. Called when characters are strung together into
- :class:`Hlist` to create :class:`Kern` nodes.
- """
- advance = self._metrics.advance - self.width
- kern = 0.
- if isinstance(next, Char):
- kern = self.font_output.get_kern(
- self.font, self.font_class, self.c, self.fontsize,
- next.font, next.font_class, next.c, next.fontsize,
- self.dpi)
- return advance + kern
- def render(self, x, y):
- """
- Render the character to the canvas
- """
- self.font_output.render_glyph(
- x, y,
- self.font, self.font_class, self.c, self.fontsize, self.dpi)
- def shrink(self):
- Node.shrink(self)
- if self.size < NUM_SIZE_LEVELS:
- self.fontsize *= SHRINK_FACTOR
- self.width *= SHRINK_FACTOR
- self.height *= SHRINK_FACTOR
- self.depth *= SHRINK_FACTOR
- def grow(self):
- Node.grow(self)
- self.fontsize *= GROW_FACTOR
- self.width *= GROW_FACTOR
- self.height *= GROW_FACTOR
- self.depth *= GROW_FACTOR
- class Accent(Char):
- """
- The font metrics need to be dealt with differently for accents,
- since they are already offset correctly from the baseline in
- TrueType fonts.
- """
- def _update_metrics(self):
- metrics = self._metrics = self.font_output.get_metrics(
- self.font, self.font_class, self.c, self.fontsize, self.dpi)
- self.width = metrics.xmax - metrics.xmin
- self.height = metrics.ymax - metrics.ymin
- self.depth = 0
- def shrink(self):
- Char.shrink(self)
- self._update_metrics()
- def grow(self):
- Char.grow(self)
- self._update_metrics()
- def render(self, x, y):
- """
- Render the character to the canvas.
- """
- self.font_output.render_glyph(
- x - self._metrics.xmin, y + self._metrics.ymin,
- self.font, self.font_class, self.c, self.fontsize, self.dpi)
- class List(Box):
- """
- A list of nodes (either horizontal or vertical).
- """
- def __init__(self, elements):
- Box.__init__(self, 0., 0., 0.)
- self.shift_amount = 0. # An arbitrary offset
- self.children = elements # The child nodes of this list
- # The following parameters are set in the vpack and hpack functions
- self.glue_set = 0. # The glue setting of this list
- self.glue_sign = 0 # 0: normal, -1: shrinking, 1: stretching
- self.glue_order = 0 # The order of infinity (0 - 3) for the glue
- def __repr__(self):
- return '[%s <%.02f %.02f %.02f %.02f> %s]' % (
- self.__internal_repr__(),
- self.width, self.height,
- self.depth, self.shift_amount,
- ' '.join([repr(x) for x in self.children]))
- def _determine_order(self, totals):
- """
- A helper function to determine the highest order of glue
- used by the members of this list. Used by vpack and hpack.
- """
- o = 0
- for i in range(len(totals) - 1, 0, -1):
- if totals[i] != 0.0:
- o = i
- break
- return o
- def _set_glue(self, x, sign, totals, error_type):
- o = self._determine_order(totals)
- self.glue_order = o
- self.glue_sign = sign
- if totals[o] != 0.:
- self.glue_set = x / totals[o]
- else:
- self.glue_sign = 0
- self.glue_ratio = 0.
- if o == 0:
- if len(self.children):
- warn("%s %s: %r" % (error_type, self.__class__.__name__, self),
- MathTextWarning)
- def shrink(self):
- for child in self.children:
- child.shrink()
- Box.shrink(self)
- if self.size < NUM_SIZE_LEVELS:
- self.shift_amount *= SHRINK_FACTOR
- self.glue_set *= SHRINK_FACTOR
- def grow(self):
- for child in self.children:
- child.grow()
- Box.grow(self)
- self.shift_amount *= GROW_FAC