diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 7372cf5b4e32..d96a27b544af 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -31,6 +31,7 @@ from matplotlib import _api, cbook from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) +from matplotlib.dviread import Dvi from matplotlib.font_manager import fontManager as _fontManager, get_font from matplotlib.ft2font import LoadFlags, RenderMode from matplotlib.mathtext import MathTextParser @@ -266,19 +267,84 @@ def get_text_width_height_descent(self, s, prop, ismath): def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): # docstring inherited # todo, handle props, angle, origins + size = prop.get_size_in_points() - texmanager = self.get_texmanager() + if mpl.rcParams["text.latex.engine"] == "latex+dvipng": + Z = self.get_texmanager().get_grey(s, size, self.dpi) + Z = (Z * 0xff).astype(np.uint8) + w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX") + xd = d * math.sin(math.radians(angle)) + yd = d * math.cos(math.radians(angle)) + x = round(x + xd) + y = round(y + yd) + self._renderer.draw_text_image(Z, x, y, angle, gc) + return + + dvifile = self.get_texmanager().make_dvi(s, size) + with Dvi(dvifile, self.dpi) as dvi: + page, = dvi - Z = texmanager.get_grey(s, size, self.dpi) - Z = np.array(Z * 255.0, np.uint8) + cos = math.cos(math.radians(angle)) + sin = math.sin(math.radians(angle)) - w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX") - xd = d * math.sin(math.radians(angle)) - yd = d * math.cos(math.radians(angle)) - x = round(x + xd) - y = round(y + yd) - self._renderer.draw_text_image(Z, x, y, angle, gc) + for text in page.text: + hf = mpl.rcParams["text.hinting_factor"] + # Resolving text.index will implicitly call get_font(), which + # resets the font transform, so it has to be done before explicitly + # setting the font transform below. + index = text.index + font = get_font(text.font_path) + font.set_size(text.font_size, self.dpi) + slant = text.font_effects.get("slant", 0) + extend = text.font_effects.get("extend", 1) + font._set_transform( + (0x10000 * np.array([[cos, -sin], [sin, cos]]) + @ [[extend, extend * slant], [0, 1]] + @ [[1 / hf, 0], [0, 1]]).round().astype(int), + [round(0x40 * (x + text.x * cos - text.y * sin)), + # FreeType's y is upwards. + round(0x40 * (self.height - y + text.x * sin + text.y * cos))] + ) + bitmap = font._render_glyph( + index, get_hinting_flag(), + RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO) + buffer = np.asarray(bitmap.buffer) + if not gc.get_antialiased(): + buffer *= 0xff + # draw_text_image's y is downwards & the bitmap bottom side. + self._renderer.draw_text_image( + buffer, + bitmap.left, int(self.height) - bitmap.top + buffer.shape[0], + 0, gc) + + rgba = gc.get_rgb() + if len(rgba) == 3 or gc.get_forced_alpha(): + rgba = rgba[:3] + (gc.get_alpha(),) + gc1 = self.new_gc() + gc1.set_linewidth(0) + gc1.set_snap(gc.get_snap()) + for box in page.boxes: + bx = box.x + by = box.y + bw = box.width + bh = box.height + if gc1.get_snap() in [None, True]: + # Prevent thin bars from disappearing by growing symmetrically. + if bw < 1: + bx -= (1 - bw) / 2 + bw = 1 + if bh < 1: + by -= (1 - bh) / 2 + bh = 1 + path = Path._create_closed([ + (bx, by), (bx + bw, by), (bx + bw, by + bh), (bx, by + bh)]) + self._renderer.draw_path( + gc1, path, + mpl.transforms.Affine2D() + .rotate_deg(angle).translate(x, self.height - y), + rgba) + gc1.restore() def get_canvas_width_height(self): # docstring inherited diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 524cdeb7169c..4fd08aa60578 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -331,6 +331,16 @@ # zapf chancery, charter, serif, sans-serif, helvetica, # avant garde, courier, monospace, computer modern roman, # computer modern sans serif, computer modern typewriter + +## The TeX engine/format to use. The following values are supported: +## - "latex": The classic TeX engine (the current default). All backends render +## TeX's output by parsing the DVI output into glyphs and boxes and emitting +## those one by one. +## - "latex+dvipng": The same as "latex", with the exception that Agg-based +## backends rely on dvipng to rasterize TeX's output. This value was the +## default up to Matplotlib 3.10. +#text.latex.engine: latex + #text.latex.preamble: # IMPROPER USE OF THIS FEATURE WILL LEAD TO LATEX FAILURES # AND IS THEREFORE UNSUPPORTED. PLEASE DO NOT ASK FOR HELP # IF THIS FEATURE DOES NOT DO WHAT YOU EXPECT IT TO. diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index f92f6b7645fa..f70697fdab45 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1073,6 +1073,7 @@ def _convert_validator_spec(key, conv): # text props "text.color": validate_color, "text.usetex": validate_bool, + "text.latex.engine": ["latex", "latex+dvipng"], "text.latex.preamble": validate_string, "text.hinting": ["default", "no_autohint", "force_autohint", "no_hinting", "auto", "native", "either", "none"], @@ -1831,6 +1832,20 @@ class _Param: "monospace, computer modern roman, computer modern sans serif, " "computer modern typewriter" ), + _Param( + "text.latex.engine", + default="latex", + validator=["latex", "latex+dvipng"], + description=( + "The TeX engine/format to use. The following values are supported:\n" + "- 'latex': The classic TeX engine (the current default). All backends " + "render TeX's output by parsing the DVI output into glyphs and boxes and " + "emitting those one by one.\n" + "- 'latex+dvipng': The same as 'latex', with the exception that Agg-based " + "backends rely on dvipng to rasterize TeX's output. This value was the " + "default up to Matplotlib 3.10." + ) + ), _Param( "text.latex.preamble", default="", diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index ea9f6d7db2fd..3c94c26c64ee 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -452,6 +452,7 @@ "text.hinting_factor", "text.kerning_factor", "text.language", + "text.latex.engine", "text.latex.preamble", "text.parse_math", "text.usetex", diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 084feb299a63..0a661f1bcee3 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -971,7 +971,7 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( image : 2d array of uint8 The image buffer on which to draw the glyph. x, y : int - The pixel location at which to draw the glyph. + The position of the glyph's top left corner. glyph : Glyph The glyph to draw. antialiased : bool, default: True