288 lines
8.9 KiB
Python
288 lines
8.9 KiB
Python
|
r"""
|
||
|
A module for parsing a subset of the TeX math syntax and rendering it to a
|
||
|
Matplotlib backend.
|
||
|
|
||
|
For a tutorial of its usage, see :doc:`/tutorials/text/mathtext`. This
|
||
|
document is primarily concerned with implementation details.
|
||
|
|
||
|
The module uses pyparsing_ to parse the TeX expression.
|
||
|
|
||
|
.. _pyparsing: https://pypi.org/project/pyparsing/
|
||
|
|
||
|
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.
|
||
|
"""
|
||
|
|
||
|
from collections import namedtuple
|
||
|
import functools
|
||
|
import logging
|
||
|
|
||
|
import numpy as np
|
||
|
|
||
|
import matplotlib as mpl
|
||
|
from matplotlib import _api, _mathtext
|
||
|
from matplotlib.ft2font import FT2Image, LOAD_NO_HINTING
|
||
|
from matplotlib.font_manager import FontProperties
|
||
|
from ._mathtext import ( # noqa: reexported API
|
||
|
RasterParse, VectorParse, get_unicode_index)
|
||
|
|
||
|
_log = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
get_unicode_index.__module__ = __name__
|
||
|
|
||
|
|
||
|
@_api.deprecated("3.6")
|
||
|
class MathtextBackend:
|
||
|
"""
|
||
|
The base class for the mathtext backend-specific code. `MathtextBackend`
|
||
|
subclasses interface between mathtext and specific Matplotlib graphics
|
||
|
backends.
|
||
|
|
||
|
Subclasses need to override the following:
|
||
|
|
||
|
- :meth:`render_glyph`
|
||
|
- :meth:`render_rect_filled`
|
||
|
- :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):
|
||
|
"""Set the dimension of 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_rect_filled(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
|
||
|
|
||
|
|
||
|
@_api.deprecated("3.6")
|
||
|
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]
|
||
|
super().__init__()
|
||
|
|
||
|
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):
|
||
|
super().set_canvas_size(w, h, d)
|
||
|
if self.mode != 'bbox':
|
||
|
self.image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0)))
|
||
|
|
||
|
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=mpl.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, np.ceil(x2), y + height)
|
||
|
|
||
|
def get_results(self, box):
|
||
|
self.image = None
|
||
|
self.mode = 'render'
|
||
|
return _mathtext.ship(box).to_raster()
|
||
|
|
||
|
def get_hinting_type(self):
|
||
|
from matplotlib.backends import backend_agg
|
||
|
return backend_agg.get_hinting_flag()
|
||
|
|
||
|
|
||
|
@_api.deprecated("3.6")
|
||
|
class MathtextBackendPath(MathtextBackend):
|
||
|
"""
|
||
|
Store information to write a mathtext rendering to the text path
|
||
|
machinery.
|
||
|
"""
|
||
|
|
||
|
_Result = namedtuple("_Result", "width height depth glyphs rects")
|
||
|
|
||
|
def __init__(self):
|
||
|
super().__init__()
|
||
|
self.glyphs = []
|
||
|
self.rects = []
|
||
|
|
||
|
def render_glyph(self, ox, oy, info):
|
||
|
oy = self.height - oy + info.offset
|
||
|
self.glyphs.append((info.font, info.fontsize, info.num, 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):
|
||
|
return _mathtext.ship(box).to_vector()
|
||
|
|
||
|
|
||
|
@_api.deprecated("3.6")
|
||
|
class MathTextWarning(Warning):
|
||
|
pass
|
||
|
|
||
|
|
||
|
##############################################################################
|
||
|
# MAIN
|
||
|
|
||
|
|
||
|
class MathTextParser:
|
||
|
_parser = None
|
||
|
_font_type_mapping = {
|
||
|
'cm': _mathtext.BakomaFonts,
|
||
|
'dejavuserif': _mathtext.DejaVuSerifFonts,
|
||
|
'dejavusans': _mathtext.DejaVuSansFonts,
|
||
|
'stix': _mathtext.StixFonts,
|
||
|
'stixsans': _mathtext.StixSansFonts,
|
||
|
'custom': _mathtext.UnicodeFonts,
|
||
|
}
|
||
|
|
||
|
def __init__(self, output):
|
||
|
"""
|
||
|
Create a MathTextParser for the given backend *output*.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
output : {"path", "agg"}
|
||
|
Whether to return a `VectorParse` ("path") or a
|
||
|
`RasterParse` ("agg", or its synonym "macosx").
|
||
|
"""
|
||
|
self._output_type = _api.check_getitem(
|
||
|
{"path": "vector", "agg": "raster", "macosx": "raster"},
|
||
|
output=output.lower())
|
||
|
|
||
|
def parse(self, s, dpi=72, prop=None):
|
||
|
"""
|
||
|
Parse the given math expression *s* at the given *dpi*. If *prop* is
|
||
|
provided, it is a `.FontProperties` object specifying the "default"
|
||
|
font to use in the math expression, used for all non-math text.
|
||
|
|
||
|
The results are cached, so multiple calls to `parse`
|
||
|
with the same expression should be fast.
|
||
|
|
||
|
Depending on the *output* type, this returns either a `VectorParse` or
|
||
|
a `RasterParse`.
|
||
|
"""
|
||
|
# lru_cache can't decorate parse() directly because prop
|
||
|
# is mutable; key the cache using an internal copy (see
|
||
|
# text._get_text_metrics_with_cache for a similar case).
|
||
|
prop = prop.copy() if prop is not None else None
|
||
|
return self._parse_cached(s, dpi, prop)
|
||
|
|
||
|
@functools.lru_cache(50)
|
||
|
def _parse_cached(self, s, dpi, prop):
|
||
|
from matplotlib.backends import backend_agg
|
||
|
|
||
|
if prop is None:
|
||
|
prop = FontProperties()
|
||
|
fontset_class = _api.check_getitem(
|
||
|
self._font_type_mapping, fontset=prop.get_math_fontfamily())
|
||
|
load_glyph_flags = {
|
||
|
"vector": LOAD_NO_HINTING,
|
||
|
"raster": backend_agg.get_hinting_flag(),
|
||
|
}[self._output_type]
|
||
|
fontset = fontset_class(prop, load_glyph_flags)
|
||
|
|
||
|
fontsize = prop.get_size_in_points()
|
||
|
|
||
|
if self._parser is None: # Cache the parser globally.
|
||
|
self.__class__._parser = _mathtext.Parser()
|
||
|
|
||
|
box = self._parser.parse(s, fontset, fontsize, dpi)
|
||
|
output = _mathtext.ship(box)
|
||
|
if self._output_type == "vector":
|
||
|
return output.to_vector()
|
||
|
elif self._output_type == "raster":
|
||
|
return output.to_raster()
|
||
|
|
||
|
|
||
|
def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None,
|
||
|
*, color=None):
|
||
|
"""
|
||
|
Given a math expression, renders it in a closely-clipped bounding
|
||
|
box to an image file.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
s : str
|
||
|
A math expression. The math portion must be enclosed in dollar signs.
|
||
|
filename_or_obj : str or path-like or file-like
|
||
|
Where to write the image data.
|
||
|
prop : `.FontProperties`, optional
|
||
|
The size and style of the text.
|
||
|
dpi : float, optional
|
||
|
The output dpi. If not set, the dpi is determined as for
|
||
|
`.Figure.savefig`.
|
||
|
format : str, optional
|
||
|
The output format, e.g., 'svg', 'pdf', 'ps' or 'png'. If not set, the
|
||
|
format is determined as for `.Figure.savefig`.
|
||
|
color : str, optional
|
||
|
Foreground color, defaults to :rc:`text.color`.
|
||
|
"""
|
||
|
from matplotlib import figure
|
||
|
|
||
|
parser = MathTextParser('path')
|
||
|
width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
|
||
|
|
||
|
fig = figure.Figure(figsize=(width / 72.0, height / 72.0))
|
||
|
fig.text(0, depth/height, s, fontproperties=prop, color=color)
|
||
|
fig.savefig(filename_or_obj, dpi=dpi, format=format)
|
||
|
|
||
|
return depth
|