diff --git a/doc/api/next_api_changes/deprecations/18002-AL.rst b/doc/api/next_api_changes/deprecations/18002-AL.rst new file mode 100644 index 000000000000..a61429712401 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/18002-AL.rst @@ -0,0 +1,11 @@ +Deprecation of various mathtext helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The ``MathtextBackendPdf``, ``MathtextBackendPs``, ``MathtextBackendSvg``, +and ``MathtextBackendCairo`` classes from the :mod:`.mathtext` module, as +well as the corresponding ``.mathtext_parser`` attributes on ``RendererPdf``, +``RendererPS``, ``RendererSVG``, and ``RendererCairo``, are deprecated. The +``MathtextBackendPath`` class can be used to obtain a list of glyphs and +rectangles in a mathtext expression, and renderer-specific logic should be +directly implemented in the renderer. + +``StandardPsFonts.pswriter`` is unused and deprecated. diff --git a/lib/matplotlib/afm.py b/lib/matplotlib/afm.py index ad3e41c08ce6..0106664f5ccc 100644 --- a/lib/matplotlib/afm.py +++ b/lib/matplotlib/afm.py @@ -469,6 +469,10 @@ def get_fontname(self): """Return the font name, e.g., 'Times-Roman'.""" return self._header[b'FontName'] + @property + def postscript_name(self): # For consistency with FT2Font. + return self.get_fontname() + def get_fullname(self): """Return the font full name, e.g., 'Times-Roman'.""" name = self._header.get(b'FullName') diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 5426e3d60ad0..3fc96e291ca6 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -45,6 +45,7 @@ def track(self, font, s): fname = font.fname self.used.setdefault(fname, set()).update(map(ord, s)) + # Not public, can be removed when pdf/ps merge_used_characters is removed. def merge(self, other): """Update self with a font path to character codepoints.""" for fname, charset in other.items(): @@ -87,7 +88,12 @@ def get_text_width_height_descent(self, s, prop, ismath): s, fontsize, renderer=self) return w, h, d elif ismath: - parse = self.mathtext_parser.parse(s, 72, prop) + # Circular import. + from matplotlib.backends.backend_ps import RendererPS + parse = self._text2path.mathtext_parser.parse( + s, 72, prop, + _force_standard_ps_fonts=(isinstance(self, RendererPS) + and mpl.rcParams["ps.useafm"])) return parse.width, parse.height, parse.depth elif mpl.rcParams[self._use_afm_rc_name]: font = self._get_font_afm(prop) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 9faf19f2080b..7f06aefdb2e6 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -131,9 +131,13 @@ def __init__(self, dpi): self.gc = GraphicsContextCairo(renderer=self) self.text_ctx = cairo.Context( cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)) - self.mathtext_parser = MathTextParser('Cairo') RendererBase.__init__(self) + @cbook.deprecated("3.4") + @property + def mathtext_parser(self): + return MathTextParser('Cairo') + def set_ctx_from_surface(self, surface): self.gc.ctx = cairo.Context(surface) # Although it may appear natural to automatically call @@ -254,26 +258,25 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx = gc.ctx - width, height, descent, glyphs, rects = self.mathtext_parser.parse( - s, self.dpi, prop) + width, height, descent, glyphs, rects = \ + self._text2path.mathtext_parser.parse(s, self.dpi, prop) ctx.save() ctx.translate(x, y) if angle: ctx.rotate(np.deg2rad(-angle)) - for font, fontsize, s, ox, oy in glyphs: + for font, fontsize, idx, ox, oy in glyphs: ctx.new_path() - ctx.move_to(ox, oy) - + ctx.move_to(ox, -oy) ctx.select_font_face( *_cairo_font_args_from_font_prop(ttfFontProperty(font))) ctx.set_font_size(fontsize * self.dpi / 72) - ctx.show_text(s) + ctx.show_text(chr(idx)) for ox, oy, w, h in rects: ctx.new_path() - ctx.rectangle(ox, oy, w, h) + ctx.rectangle(ox, -oy, w, -h) ctx.set_source_rgb(0, 0, 0) ctx.fill_preserve() @@ -290,8 +293,9 @@ def get_text_width_height_descent(self, s, prop, ismath): return super().get_text_width_height_descent(s, prop, ismath) if ismath: - dims = self.mathtext_parser.parse(s, self.dpi, prop) - return dims[0:3] # return width, height, descent + width, height, descent, *_ = \ + self._text2path.mathtext_parser.parse(s, self.dpi, prop) + return width, height, descent ctx = self.text_ctx # problem - scale remembers last setting and font can become diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 71dfdc91be64..7c03abe8bba5 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1772,9 +1772,13 @@ def __init__(self, file, image_dpi, height, width): super().__init__(width, height) self.file = file self.gc = self.new_gc() - self.mathtext_parser = MathTextParser("Pdf") self.image_dpi = image_dpi + @cbook.deprecated("3.4") + @property + def mathtext_parser(self): + return MathTextParser("Pdf") + def finalize(self): self.file.output(*self.gc.finalize()) @@ -2020,9 +2024,8 @@ def _setup_textpos(self, x, y, angle, oldx=0, oldy=0, oldangle=0): def draw_mathtext(self, gc, x, y, s, prop, angle): # TODO: fix positioning and encoding - width, height, descent, glyphs, rects, used_characters = \ - self.mathtext_parser.parse(s, 72, prop) - self.file._character_tracker.merge(used_characters) + width, height, descent, glyphs, rects = \ + self._text2path.mathtext_parser.parse(s, 72, prop) # When using Type 3 fonts, we can't use character codes higher # than 255, so we use the "Do" command to render those @@ -2040,7 +2043,9 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self.file.output(Op.begin_text) prev_font = None, None oldx, oldy = 0, 0 - for ox, oy, fontname, fontsize, num, symbol_name in glyphs: + for font, fontsize, num, ox, oy in glyphs: + self.file._character_tracker.track(font, chr(num)) + fontname = font.fname if is_opentype_cff_font(fontname): fonttype = 42 else: @@ -2060,7 +2065,8 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): # If using Type 3 fonts, render all of the multi-byte characters # as XObjects using the 'Do' command. if global_fonttype == 3: - for ox, oy, fontname, fontsize, num, symbol_name in glyphs: + for font, fontsize, num, ox, oy in glyphs: + fontname = font.fname if is_opentype_cff_font(fontname): fonttype = 42 else: @@ -2072,6 +2078,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): 0.001 * fontsize, 0, 0, 0.001 * fontsize, ox, oy, Op.concat_matrix) + symbol_name = font.get_glyph_name(font.get_char_index(num)) name = self.file._get_xobject_symbol_name( fontname, symbol_name) self.file.output(Name(name), Op.use_xobject) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index e6d4074f1cf1..eb213b334846 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -20,6 +20,7 @@ import matplotlib as mpl from matplotlib import cbook, _path from matplotlib import _text_layout +from matplotlib.afm import AFM from matplotlib.backend_bases import ( _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase) @@ -167,7 +168,11 @@ def __init__(self, width, height, pswriter, imagedpi=72): self._path_collection_id = 0 self._character_tracker = _backend_pdf_ps.CharacterTracker() - self.mathtext_parser = MathTextParser("PS") + + @cbook.deprecated("3.3") + @property + def mathtext_parser(self): + return MathTextParser("PS") @cbook.deprecated("3.3") @property @@ -599,18 +604,33 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): if debugPS: self._pswriter.write("% mathtext\n") - width, height, descent, pswriter, used_characters = \ - self.mathtext_parser.parse(s, 72, prop) - self._character_tracker.merge(used_characters) + width, height, descent, glyphs, rects = \ + self._text2path.mathtext_parser.parse( + s, 72, prop, + _force_standard_ps_fonts=mpl.rcParams["ps.useafm"]) self.set_color(*gc.get_rgb()) - thetext = pswriter.getvalue() - self._pswriter.write(f"""\ -gsave -{x:f} {y:f} translate -{angle:f} rotate -{thetext} -grestore -""") + self._pswriter.write( + f"gsave\n" + f"{x:f} {y:f} translate\n" + f"{angle:f} rotate\n") + lastfont = None + for font, fontsize, num, ox, oy in glyphs: + self._character_tracker.track(font, chr(num)) + if (font.postscript_name, fontsize) != lastfont: + lastfont = font.postscript_name, fontsize + self._pswriter.write( + f"/{font.postscript_name} findfont\n" + f"{fontsize} scalefont\n" + f"setfont\n") + symbol_name = ( + font.get_name_char(chr(num)) if isinstance(font, AFM) else + font.get_glyph_name(font.get_char_index(num))) + self._pswriter.write( + f"{ox:f} {oy:f} moveto\n" + f"/{symbol_name} glyphshow\n") + for ox, oy, w, h in rects: + self._pswriter.write(f"{ox} {oy} {w} {h} rectfill\n") + self._pswriter.write("grestore\n") def draw_gouraud_triangle(self, gc, points, colors, trans): self.draw_gouraud_triangles(gc, points.reshape((1, 3, 2)), diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index aefb3c9b5b9d..de0f1790a24d 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -294,7 +294,6 @@ def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, self._has_gouraud = False self._n_gradients = 0 self._fonts = OrderedDict() - self.mathtext_parser = MathTextParser('SVG') RendererBase.__init__(self) self._glyph_map = dict() @@ -312,6 +311,11 @@ def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, self._write_metadata(metadata) self._write_default_style() + @cbook.deprecated("3.4") + @property + def mathtext_parser(self): + return MathTextParser('SVG') + def finalize(self): self._write_clips() self._write_hatches() @@ -1173,26 +1177,23 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): else: writer.comment(s) - width, height, descent, svg_elements, used_characters = \ - self.mathtext_parser.parse(s, 72, prop) - svg_glyphs = svg_elements.svg_glyphs - svg_rects = svg_elements.svg_rects - - attrib = {} - attrib['style'] = generate_css(style) - attrib['transform'] = generate_transform([ - ('translate', (x, y)), - ('rotate', (-angle,))]) + width, height, descent, glyphs, rects = \ + self._text2path.mathtext_parser.parse(s, 72, prop) # Apply attributes to 'g', not 'text', because we likely have some # rectangles as well with the same style and transformation. - writer.start('g', attrib=attrib) + writer.start('g', + style=generate_css(style), + transform=generate_transform([ + ('translate', (x, y)), + ('rotate', (-angle,))]), + ) writer.start('text') # Sort the characters by font, and output one tspan for each. spans = OrderedDict() - for font, fontsize, thetext, new_x, new_y, metrics in svg_glyphs: + for font, fontsize, thetext, new_x, new_y in glyphs: style = generate_css({ 'font-size': short_float_fmt(fontsize) + 'px', 'font-family': font.family_name, @@ -1223,15 +1224,14 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer.end('text') - if len(svg_rects): - for x, y, width, height in svg_rects: - writer.element( - 'rect', - x=short_float_fmt(x), - y=short_float_fmt(-y + height), - width=short_float_fmt(width), - height=short_float_fmt(height) - ) + for x, y, width, height in rects: + writer.element( + 'rect', + x=short_float_fmt(x), + y=short_float_fmt(-y-1), + width=short_float_fmt(width), + height=short_float_fmt(height) + ) writer.end('g') diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index 8ecffa7c961a..d817a589d1cf 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -216,6 +216,7 @@ def get_results(self, box, used_characters): return image, depth +@cbook.deprecated("3.4", alternative="MathtextBackendPath") class MathtextBackendPs(MathtextBackend): """ Store information to write a mathtext rendering to the PostScript backend. @@ -258,6 +259,7 @@ def get_results(self, box, used_characters): used_characters) +@cbook.deprecated("3.4", alternative="MathtextBackendPath") class MathtextBackendPdf(MathtextBackend): """Store information to write a mathtext rendering to the PDF backend.""" @@ -288,6 +290,7 @@ def get_results(self, box, used_characters): used_characters) +@cbook.deprecated("3.4", alternative="MathtextBackendPath") class MathtextBackendSvg(MathtextBackend): """ Store information to write a mathtext rendering to the SVG @@ -324,28 +327,29 @@ class MathtextBackendPath(MathtextBackend): machinery. """ + _Result = namedtuple("_Result", "width height depth glyphs rects") + 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)) + 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, used_characters): ship(0, 0, box) - return (self.width, - self.height + self.depth, - self.depth, - self.glyphs, - self.rects) + return self._Result(self.width, + self.height + self.depth, + self.depth, + self.glyphs, + self.rects) +@cbook.deprecated("3.4", alternative="MathtextBackendPath") class MathtextBackendCairo(MathtextBackend): """ Store information to write a mathtext rendering to the Cairo @@ -1102,7 +1106,7 @@ class StandardPsFonts(Fonts): } def __init__(self, default_font_prop): - super().__init__(default_font_prop, MathtextBackendPs()) + super().__init__(default_font_prop, MathtextBackendPath()) self.glyphd = {} self.fonts = {} @@ -1117,7 +1121,11 @@ def __init__(self, default_font_prop): self.fonts['default'] = default_font self.fonts['regular'] = default_font - self.pswriter = StringIO() + + @cbook.deprecated("3.4") + @property + def pswriter(self): + return StringIO() def _get_font(self, font): if font in self.fontmap: @@ -3344,7 +3352,7 @@ def __init__(self, output): """Create a MathTextParser for the given backend *output*.""" self._output = output.lower() - def parse(self, s, dpi=72, prop=None): + def parse(self, s, dpi=72, prop=None, *, _force_standard_ps_fonts=False): """ Parse the given math expression *s* at the given *dpi*. If *prop* is provided, it is a `.FontProperties` object specifying the "default" @@ -3356,16 +3364,16 @@ def parse(self, s, dpi=72, prop=None): # lru_cache can't decorate parse() directly because the ps.useafm and # mathtext.fontset rcParams also affect the parse (e.g. by affecting # the glyph metrics). - return self._parse_cached( - s, dpi, prop, rcParams['ps.useafm'], rcParams['mathtext.fontset']) + return self._parse_cached(s, dpi, prop, _force_standard_ps_fonts, + rcParams['mathtext.fontset']) @functools.lru_cache(50) - def _parse_cached(self, s, dpi, prop, ps_useafm, fontset): + def _parse_cached(self, s, dpi, prop, force_standard_ps_fonts, fontset): if prop is None: prop = FontProperties() - if self._output == 'ps' and ps_useafm: + if force_standard_ps_fonts: font_output = StandardPsFonts(prop) else: backend = self._backend_mapping[self._output]() diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 8a6520ce7abc..a910fd544626 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -65,8 +65,7 @@ def get_text_width_height_descent(self, s, prop, ismath): if ismath: prop = prop.copy() prop.set_size(self.FONT_SCALE) - - width, height, descent, trash, used_characters = \ + width, height, descent, *_ = \ self.mathtext_parser.parse(s, 72, prop) return width * scale, height * scale, descent * scale