diff --git a/doc/api/next_api_changes/deprecations/22506-AL.rst b/doc/api/next_api_changes/deprecations/22506-AL.rst new file mode 100644 index 000000000000..fe3611563c63 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/22506-AL.rst @@ -0,0 +1,3 @@ +``MathtextBackend``, ``MathtextBackendAgg``, ``MathtextBackendPath``, ``MathTextWarning`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are deprecated with no replacement. diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index e91a4fb2957a..44bb5a9e8fd8 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -23,7 +23,7 @@ from ._mathtext_data import ( latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni) from .font_manager import FontProperties, findfont, get_font -from .ft2font import KERNING_DEFAULT +from .ft2font import FT2Image, KERNING_DEFAULT ParserElement.enablePackrat() @@ -67,6 +67,85 @@ def get_unicode_index(symbol, math=False): # Publicly exported. .format(symbol)) from err +VectorParse = namedtuple("VectorParse", "width height depth glyphs rects", + module="matplotlib.mathtext") +VectorParse.__doc__ = r""" +The namedtuple type returned by ``MathTextParser("path").parse(...)``. + +This tuple contains the global metrics (*width*, *height*, *depth*), a list of +*glyphs* (including their positions) and of *rect*\angles. +""" + + +RasterParse = namedtuple("RasterParse", "ox oy width height depth image", + module="matplotlib.mathtext") +RasterParse.__doc__ = r""" +The namedtuple type returned by ``MathTextParser("agg").parse(...)``. + +This tuple contains the global metrics (*width*, *height*, *depth*), and a +raster *image*. The offsets *ox*, *oy* are always zero. +""" + + +class Output: + r""" + Result of `ship`\ping a box: lists of positioned glyphs and rectangles. + + This class is not exposed to end users, but converted to a `VectorParse` or + a `RasterParse` by `.MathTextParser.parse`. + """ + + def __init__(self, box): + self.box = box + self.glyphs = [] # (ox, oy, info) + self.rects = [] # (x1, y1, x2, y2) + + def to_vector(self): + w, h, d = map( + np.ceil, [self.box.width, self.box.height, self.box.depth]) + gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset) + for ox, oy, info in self.glyphs] + rs = [(x1, h - y2, x2 - x1, y2 - y1) + for x1, y1, x2, y2 in self.rects] + return VectorParse(w, h + d, d, gs, rs) + + def to_raster(self): + # Metrics y's and mathtext y's are oriented in opposite directions, + # hence the switch between ymin and ymax. + xmin = min([*[ox + info.metrics.xmin for ox, oy, info in self.glyphs], + *[x1 for x1, y1, x2, y2 in self.rects], 0]) - 1 + ymin = min([*[oy - info.metrics.ymax for ox, oy, info in self.glyphs], + *[y1 for x1, y1, x2, y2 in self.rects], 0]) - 1 + xmax = max([*[ox + info.metrics.xmax for ox, oy, info in self.glyphs], + *[x2 for x1, y1, x2, y2 in self.rects], 0]) + 1 + ymax = max([*[oy - info.metrics.ymin for ox, oy, info in self.glyphs], + *[y2 for x1, y1, x2, y2 in self.rects], 0]) + 1 + w = xmax - xmin + h = ymax - ymin - self.box.depth + d = ymax - ymin - self.box.height + image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0))) + + # Ideally, we could just use self.glyphs and self.rects here, shifting + # their coordinates by (-xmin, -ymin), but this yields slightly + # different results due to floating point slop; shipping twice is the + # old approach and keeps baseline images backcompat. + shifted = ship(self.box, (-xmin, -ymin)) + + for ox, oy, info in shifted.glyphs: + info.font.draw_glyph_to_bitmap( + image, ox, oy - info.metrics.iceberg, info.glyph, + antialiased=mpl.rcParams['text.antialiased']) + for x1, y1, x2, y2 in shifted.rects: + height = max(int(y2 - y1) - 1, 0) + if height == 0: + center = (y2 + y1) / 2 + y = int(center - (height + 1) / 2) + else: + y = int(y1) + image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height) + return RasterParse(0, 0, w, h + d, d, image) + + class Fonts: """ An abstract base class for a system of fonts to use for mathtext. @@ -76,18 +155,19 @@ class Fonts: to do the actual drawing. """ - def __init__(self, default_font_prop, mathtext_backend): + def __init__(self, default_font_prop, load_glyph_flags): """ Parameters ---------- default_font_prop : `~.font_manager.FontProperties` The default non-math font, or the base font for Unicode (generic) font rendering. - mathtext_backend : `MathtextBackend` subclass - Backend to which rendering is actually delegated. + load_glyph_flags : int + Flags passed to the glyph loader (e.g. ``FT_Load_Glyph`` and + ``FT_Load_Char`` for FreeType-based fonts). """ self.default_font_prop = default_font_prop - self.mathtext_backend = mathtext_backend + self.load_glyph_flags = load_glyph_flags def get_kern(self, font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi): @@ -137,19 +217,20 @@ def get_metrics(self, font, font_class, sym, fontsize, dpi): info = self._get_info(font, font_class, sym, fontsize, dpi) return info.metrics - def render_glyph(self, ox, oy, font, font_class, sym, fontsize, dpi): + def render_glyph( + self, output, ox, oy, font, font_class, sym, fontsize, dpi): """ At position (*ox*, *oy*), draw the glyph specified by the remaining parameters (see `get_metrics` for their detailed description). """ info = self._get_info(font, font_class, sym, fontsize, dpi) - self.mathtext_backend.render_glyph(ox, oy, info) + output.glyphs.append((ox, oy, info)) - def render_rect_filled(self, x1, y1, x2, y2): + def render_rect_filled(self, output, x1, y1, x2, y2): """ Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*). """ - self.mathtext_backend.render_rect_filled(x1, y1, x2, y2) + output.rects.append((x1, y1, x2, y2)) def get_xheight(self, font, fontsize, dpi): """ @@ -187,13 +268,14 @@ class TruetypeFonts(Fonts): A generic base class for all font setups that use Truetype fonts (through FT2Font). """ - def __init__(self, default_font_prop, mathtext_backend): - super().__init__(default_font_prop, mathtext_backend) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) # Per-instance cache. self._get_info = functools.lru_cache(None)(self._get_info) self._fonts = {} - filename = findfont(default_font_prop) + filename = findfont(self.default_font_prop) default_font = get_font(filename) self._fonts['default'] = default_font self._fonts['regular'] = default_font @@ -220,8 +302,7 @@ def _get_offset(self, font, glyph, fontsize, dpi): def _get_info(self, fontname, font_class, sym, fontsize, dpi): font, num, slanted = self._get_glyph(fontname, font_class, sym) font.set_size(fontsize, dpi) - glyph = font.load_char( - num, flags=self.mathtext_backend.get_hinting_type()) + glyph = font.load_char(num, flags=self.load_glyph_flags) xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] offset = self._get_offset(font, glyph, fontsize, dpi) @@ -820,12 +901,11 @@ class DejaVuSansFontConstants(FontConstantsBase): def _get_font_constant_set(state): constants = _font_constant_mapping.get( - state.font_output._get_font(state.font).family_name, - FontConstantsBase) + state.fontset._get_font(state.font).family_name, FontConstantsBase) # STIX sans isn't really its own fonts, just different code points # in the STIX fonts, so we have to detect this one separately. if (constants is STIXFontConstants and - isinstance(state.font_output, StixSansFonts)): + isinstance(state.fontset, StixSansFonts)): return STIXSansFontConstants return constants @@ -849,7 +929,7 @@ def shrink(self): """ self.size += 1 - def render(self, x, y): + def render(self, output, x, y): """Render this node.""" @@ -869,7 +949,7 @@ def shrink(self): self.height *= SHRINK_FACTOR self.depth *= SHRINK_FACTOR - def render(self, x1, y1, x2, y2): + def render(self, output, x1, y1, x2, y2): pass @@ -903,7 +983,7 @@ class Char(Node): def __init__(self, c, state): super().__init__() self.c = c - self.font_output = state.font_output + self.fontset = state.fontset self.font = state.font self.font_class = state.font_class self.fontsize = state.fontsize @@ -916,7 +996,7 @@ def __repr__(self): return '`%s`' % self.c def _update_metrics(self): - metrics = self._metrics = self.font_output.get_metrics( + metrics = self._metrics = self.fontset.get_metrics( self.font, self.font_class, self.c, self.fontsize, self.dpi) if self.c == ' ': self.width = metrics.advance @@ -938,15 +1018,16 @@ def get_kerning(self, next): advance = self._metrics.advance - self.width kern = 0. if isinstance(next, Char): - kern = self.font_output.get_kern( + kern = self.fontset.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): - self.font_output.render_glyph( - x, y, self.font, self.font_class, self.c, self.fontsize, self.dpi) + def render(self, output, x, y): + self.fontset.render_glyph( + output, x, y, + self.font, self.font_class, self.c, self.fontsize, self.dpi) def shrink(self): super().shrink() @@ -964,7 +1045,7 @@ class Accent(Char): TrueType fonts. """ def _update_metrics(self): - metrics = self._metrics = self.font_output.get_metrics( + metrics = self._metrics = self.fontset.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 @@ -974,9 +1055,9 @@ def shrink(self): super().shrink() self._update_metrics() - def render(self, x, y): - self.font_output.render_glyph( - x - self._metrics.xmin, y + self._metrics.ymin, + def render(self, output, x, y): + self.fontset.render_glyph( + output, x - self._metrics.xmin, y + self._metrics.ymin, self.font, self.font_class, self.c, self.fontsize, self.dpi) @@ -1228,10 +1309,10 @@ class Rule(Box): def __init__(self, width, height, depth, state): super().__init__(width, height, depth) - self.font_output = state.font_output + self.fontset = state.fontset - def render(self, x, y, w, h): - self.font_output.render_rect_filled(x, y, x + w, y + h) + def render(self, output, x, y, w, h): + self.fontset.render_rect_filled(output, x, y, x + w, y + h) class Hrule(Rule): @@ -1348,10 +1429,10 @@ class AutoHeightChar(Hlist): """ def __init__(self, c, height, depth, state, always=False, factor=None): - alternatives = state.font_output.get_sized_alternatives_for_symbol( + alternatives = state.fontset.get_sized_alternatives_for_symbol( state.font, c) - xHeight = state.font_output.get_xheight( + xHeight = state.fontset.get_xheight( state.font, state.fontsize, state.dpi) state = state.copy() @@ -1387,7 +1468,7 @@ class AutoWidthChar(Hlist): """ def __init__(self, c, width, state, always=False, char_class=Char): - alternatives = state.font_output.get_sized_alternatives_for_symbol( + alternatives = state.fontset.get_sized_alternatives_for_symbol( state.font, c) state = state.copy() @@ -1405,9 +1486,9 @@ def __init__(self, c, width, state, always=False, char_class=Char): self.width = char.width -def ship(ox, oy, box): +def ship(box, xy=(0, 0)): """ - Ship boxes to output once they have been set up, this sends them to output. + Ship out *box* at offset *xy*, converting it to an `Output`. Since boxes can be inside of boxes inside of boxes, the main work of `ship` is done by two mutually recursive routines, `hlist_out` and `vlist_out`, @@ -1415,11 +1496,12 @@ def ship(ox, oy, box): and vertical boxes. The global variables used in TeX to store state as it processes have become local variables here. """ - + ox, oy = xy cur_v = 0. cur_h = 0. off_h = ox off_v = oy + box.height + output = Output(box) def clamp(value): return -1e9 if value < -1e9 else +1e9 if value > +1e9 else value @@ -1436,7 +1518,7 @@ def hlist_out(box): for p in box.children: if isinstance(p, Char): - p.render(cur_h + off_h, cur_v + off_v) + p.render(output, cur_h + off_h, cur_v + off_v) cur_h += p.width elif isinstance(p, Kern): cur_h += p.width @@ -1465,7 +1547,8 @@ def hlist_out(box): rule_depth = box.depth if rule_height > 0 and rule_width > 0: cur_v = base_line + rule_depth - p.render(cur_h + off_h, cur_v + off_v, + p.render(output, + cur_h + off_h, cur_v + off_v, rule_width, rule_height) cur_v = base_line cur_h += rule_width @@ -1521,7 +1604,8 @@ def vlist_out(box): rule_height += rule_depth if rule_height > 0 and rule_depth > 0: cur_v += rule_height - p.render(cur_h + off_h, cur_v + off_v, + p.render(output, + cur_h + off_h, cur_v + off_v, rule_width, rule_height) elif isinstance(p, Glue): glue_spec = p.glue_spec @@ -1541,6 +1625,7 @@ def vlist_out(box): "Internal mathtext error: Char node found in vlist") hlist_out(box) + return output ############################################################################## @@ -1566,8 +1651,8 @@ class ParserState: and popped accordingly. """ - def __init__(self, font_output, font, font_class, fontsize, dpi): - self.font_output = font_output + def __init__(self, fontset, font, font_class, fontsize, dpi): + self.fontset = fontset self._font = font self.font_class = font_class self.fontsize = fontsize @@ -1588,7 +1673,7 @@ def font(self, name): def get_current_underline_thickness(self): """Return the underline thickness for this state.""" - return self.font_output.get_underline_thickness( + return self.fontset.get_underline_thickness( self.font, self.fontsize, self.dpi) @@ -1952,7 +2037,7 @@ def _make_space(self, percentage): key = (state.font, state.fontsize, state.dpi) width = self._em_width_cache.get(key) if width is None: - metrics = state.font_output.get_metrics( + metrics = state.fontset.get_metrics( 'it', mpl.rcParams['mathtext.default'], 'm', state.fontsize, state.dpi) width = metrics.advance @@ -2181,9 +2266,9 @@ def subsuper(self, s, loc, toks): super = arg state = self.get_state() - rule_thickness = state.font_output.get_underline_thickness( + rule_thickness = state.fontset.get_underline_thickness( state.font, state.fontsize, state.dpi) - xHeight = state.font_output.get_xheight( + xHeight = state.fontset.get_xheight( state.font, state.fontsize, state.dpi) if napostrophes: @@ -2343,7 +2428,7 @@ def _genfrac(self, ldelim, rdelim, rule, style, num, den): # Shift so the fraction line sits in the middle of the # equals sign - metrics = state.font_output.get_metrics( + metrics = state.fontset.get_metrics( state.font, mpl.rcParams['mathtext.default'], '=', state.fontsize, state.dpi) shift = (cden.height - diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index b613c3a28111..3769941119b6 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -24,14 +24,16 @@ from matplotlib import _api, rcParams, _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 = _mathtext.get_unicode_index get_unicode_index.__module__ = __name__ +@_api.deprecated("3.6") class MathtextBackend: """ The base class for the mathtext backend-specific code. `MathtextBackend` @@ -87,6 +89,7 @@ def get_hinting_type(self): return LOAD_NO_HINTING +@_api.deprecated("3.6") class MathtextBackendAgg(MathtextBackend): """ Render glyphs and rectangles to an FTImage buffer, which is later @@ -135,32 +138,16 @@ def render_rect_filled(self, x1, y1, x2, y2): self.image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height) def get_results(self, box): - self.mode = 'bbox' - orig_height = box.height - orig_depth = box.depth - _mathtext.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) - _mathtext.ship(-bbox[0], -bbox[1], box) - result = (self.ox, - self.oy, - self.width, - self.height + self.depth, - self.depth, - self.image) self.image = None - return result + 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 @@ -182,14 +169,10 @@ 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): - _mathtext.ship(0, 0, box) - return self._Result(self.width, - self.height + self.depth, - self.depth, - self.glyphs, - self.rects) + return _mathtext.ship(box).to_vector() +@_api.deprecated("3.6") class MathTextWarning(Warning): pass @@ -200,12 +183,6 @@ class MathTextWarning(Warning): class MathTextParser: _parser = None - - _backend_mapping = { - 'agg': MathtextBackendAgg, - 'path': MathtextBackendPath, - 'macosx': MathtextBackendAgg, - } _font_type_mapping = { 'cm': _mathtext.BakomaFonts, 'dejavuserif': _mathtext.DejaVuSerifFonts, @@ -216,8 +193,18 @@ class MathTextParser: } def __init__(self, output): - """Create a MathTextParser for the given backend *output*.""" - self._output = output.lower() + """ + 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): """ @@ -227,6 +214,9 @@ def parse(self, s, dpi=72, prop=None): 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 @@ -236,24 +226,29 @@ def parse(self, s, dpi=72, prop=None): @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()) - backend = self._backend_mapping[self._output]() - font_output = fontset_class(prop, backend) + 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() - # This is a class variable so we don't rebuild the parser - # with each request. - if self._parser is None: + if self._parser is None: # Cache the parser globally. self.__class__._parser = _mathtext.Parser() - box = self._parser.parse(s, font_output, fontsize, dpi) - backend.set_canvas_size(*np.ceil([box.width, box.height, box.depth])) - return backend.get_results(box) + 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,