diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index feb4b0c8be01..5db60d49ce87 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -22,6 +22,7 @@ """ from contextlib import nullcontext +import math from math import radians, cos, sin import numpy as np @@ -31,6 +32,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 from matplotlib.mathtext import MathTextParser @@ -219,7 +221,8 @@ def get_text_width_height_descent(self, s, prop, ismath): _api.check_in_list(["TeX", True, False], ismath=ismath) if ismath == "TeX": - return super().get_text_width_height_descent(s, prop, ismath) + return [*map( + math.ceil, super().get_text_width_height_descent(s, prop, ismath))] if ismath: ox, oy, width, height, descent, font_image = \ @@ -238,19 +241,45 @@ 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() - Z = texmanager.get_grey(s, size, self.dpi) - Z = np.array(Z * 255.0, np.uint8) + size = prop.get_size_in_points() + dvifile = self.get_texmanager().make_dvi(s, size) + with Dvi(dvifile, self.dpi) as dvi: + page, = dvi + w = math.ceil(page.width) + h = math.ceil(page.height) + d = math.ceil(page.descent) + + image = np.zeros((h + d, w), np.uint8) + + for text in page.text: + hf = mpl.rcParams["text.hinting_factor"] + 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) + matrix = [ + [round(65536 * extend / hf), round(65536 * extend * slant)], + [0, 65536], + ] + font._set_transform(matrix, [0, 0]) + glyph = font.load_glyph(text.index) + # text.y is upwards from baseline, _draw_glyph_at wants upwards from bottom. + font._draw_glyph_at(image, text.x, d + text.y, glyph, + antialiased=gc.get_antialiased()) + + for box in page.boxes: + x0 = round(box.x) + x1 = x0 + max(round(box.width), 1) + y1 = round(h - box.y) + y0 = y1 - max(round(box.height), 1) + image[y0:y1, x0:x1] = 0xff - w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX") xd = d * sin(radians(angle)) yd = d * cos(radians(angle)) x = round(x + xd) y = round(y + yd) - self._renderer.draw_text_image(Z, x, y, angle, gc) + self._renderer.draw_text_image(image, x, y, angle, gc) def get_canvas_width_height(self): # docstring inherited diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/eqnarray.png b/lib/matplotlib/tests/baseline_images/test_usetex/eqnarray.png index 249f15d238dd..6d65164512cd 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_usetex/eqnarray.png and b/lib/matplotlib/tests/baseline_images/test_usetex/eqnarray.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/rotation.png b/lib/matplotlib/tests/baseline_images/test_usetex/rotation.png index 99bab74390b8..47eceac5670b 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_usetex/rotation.png and b/lib/matplotlib/tests/baseline_images/test_usetex/rotation.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/rotation.svg b/lib/matplotlib/tests/baseline_images/test_usetex/rotation.svg index c68bffefde86..d5fe3d73d461 100644 --- a/lib/matplotlib/tests/baseline_images/test_usetex/rotation.svg +++ b/lib/matplotlib/tests/baseline_images/test_usetex/rotation.svg @@ -6,11 +6,11 @@ - 2023-04-27T20:38:40.258942 + 2025-09-04T13:58:02.266478 image/svg+xml - Matplotlib v3.8.0.dev964+g2e2d2d5f57.d20230428, https://matplotlib.org/ + Matplotlib v3.11.0.dev1290+ga0f65604c4, https://matplotlib.org/ @@ -33,438 +33,438 @@ z - - - + + - - + + - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - @@ -479,27 +479,27 @@ z - @@ -515,50 +515,50 @@ z - - @@ -575,45 +575,45 @@ z - - @@ -630,20 +630,20 @@ z - @@ -693,27 +693,27 @@ z - @@ -898,27 +898,27 @@ z - @@ -1058,34 +1058,34 @@ z - @@ -1225,23 +1225,23 @@ z - @@ -1380,7 +1380,7 @@ z - + diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png index e4a9183612f5..905daf8dbf34 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png and b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png differ diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 890fc61974b0..e57f0ed4f863 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -283,6 +283,17 @@ void FT2Font::set_size(double ptsize, double dpi) } } +void FT2Font::_set_transform( + std::array, 2> matrix, std::array delta) +{ + FT_Matrix m = {matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]}; + FT_Vector d = {delta[0], delta[1]}; + FT_Set_Transform(face, &m, &d); + for (auto & fallback : fallbacks) { + fallback->_set_transform(matrix, delta); + } +} + void FT2Font::set_charmap(int i) { if (i >= face->num_charmaps) { @@ -696,6 +707,24 @@ void FT2Font::draw_glyph_to_bitmap( draw_bitmap(im, &bitmap->bitmap, x + bitmap->left, y); } +void FT2Font::_draw_glyph_at( + py::array_t im, + double x, double y, size_t glyphInd, bool antialiased) +{ + if (glyphInd >= glyphs.size()) { + throw std::runtime_error("glyph num is out of range"); + } + FT_Vector sub_offset = {FT_Fixed(x * 64 + .5), FT_Fixed(y * 64 + .5)}; + FT_CHECK( + FT_Glyph_To_Bitmap, + &glyphs[glyphInd], + antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO, + &sub_offset, // additional translation + 1); // destroy image + FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[glyphInd]; + draw_bitmap(im, &bitmap->bitmap, bitmap->left, im.shape(0) - bitmap->top); +} + std::string FT2Font::get_glyph_name(unsigned int glyph_number) { std::string buffer; diff --git a/src/ft2font.h b/src/ft2font.h index ffaf511ab9ca..09ce2f0ed074 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -107,6 +107,8 @@ class FT2Font void close(); void clear(); void set_size(double ptsize, double dpi); + void _set_transform( + std::array, 2> matrix, std::array delta); void set_charmap(int i); void select_charmap(unsigned long i); void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, @@ -132,6 +134,9 @@ class FT2Font void draw_glyph_to_bitmap( py::array_t im, int x, int y, size_t glyphInd, bool antialiased); + void _draw_glyph_at( + py::array_t im, + double x, double y, size_t glyphInd, bool antialiased); std::string get_glyph_name(unsigned int glyph_number); long get_name_index(char *name); FT_UInt get_char_index(FT_ULong charcode, bool fallback); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 65fcb4b7e013..d0b231a01299 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -535,6 +535,18 @@ const char *PyFT2Font_set_size__doc__ = R"""( The DPI used for rendering the text. )"""; +const char *PyFT2Font__set_transform__doc__ = R"""( + Set the transform of the text. + + This is a low-level function directly taking inputs in 26.6 format. Refer + to the FreeType docs of FT_Set_Transform for further description. + + Parameters + ---------- + matrix : (2, 2) array of int + delta : (2,) array of int +)"""; + const char *PyFT2Font_set_charmap__doc__ = R"""( Make the i-th charmap current. @@ -912,7 +924,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 @@ -936,6 +948,36 @@ PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, py::buffer &image, xd, yd, glyph->glyphInd, antialiased); } +const char *PyFT2Font__draw_glyph_at__doc__ = R"""( + Draw a single glyph to the bitmap at pixel locations x, y. + + Parameters + ---------- + image : FT2Image + The image buffer on which to draw the glyph. If the buffer is too + small, the glyph will be cropped. + x, y : float + The position of the glyph's origin. + glyph : Glyph + The glyph to draw. + antialiased : bool, default: True + Whether to render glyphs 8-bit antialiased or in pure black-and-white. + + See Also + -------- + .draw_glyphs_to_bitmap +)"""; + +static void +PyFT2Font__draw_glyph_at(PyFT2Font *self, py::buffer &image, + double x, double y, + PyGlyph *glyph, bool antialiased = true) +{ + self->_draw_glyph_at( + py::array_t{image}, + x, y, glyph->glyphInd, antialiased); +} + const char *PyFT2Font_get_glyph_name__doc__ = R"""( Retrieve the ASCII name of a given glyph *index* in a face. @@ -1527,6 +1569,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def("clear", &PyFT2Font::clear, PyFT2Font_clear__doc__) .def("set_size", &PyFT2Font::set_size, "ptsize"_a, "dpi"_a, PyFT2Font_set_size__doc__) + .def("_set_transform", &PyFT2Font::_set_transform, "matrix"_a, "delta"_a) .def("set_charmap", &PyFT2Font::set_charmap, "i"_a, PyFT2Font_set_charmap__doc__) .def("select_charmap", &PyFT2Font::select_charmap, "i"_a, @@ -1567,6 +1610,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) PyFT2Font_draw_glyph_to_bitmap__doc__); } cls + .def("_draw_glyph_at", &PyFT2Font__draw_glyph_at, + "image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true, + PyFT2Font__draw_glyph_at__doc__) .def("get_glyph_name", &PyFT2Font::get_glyph_name, "index"_a, PyFT2Font_get_glyph_name__doc__) .def("get_charmap", &PyFT2Font_get_charmap, PyFT2Font_get_charmap__doc__)