diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index b435ae565ce4..473fe7da3a0c 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 @@ -30,8 +31,9 @@ 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.ft2font import FT2Image, LoadFlags from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.transforms import Bbox, BboxBase @@ -208,7 +210,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 = \ @@ -227,19 +230,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..96028d2951c3 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..1473a3304186 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/test_usetex.png b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png index e4a9183612f5..dc4c931cb9b6 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 b2c2c0fa9bd1..1fa821fe27f2 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -306,6 +306,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,27 @@ 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_Error error = FT_Glyph_To_Bitmap( + &glyphs[glyphInd], + antialiased ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO, + &sub_offset, // additional translation + 1 // destroy image + ); + if (error) { + throw_ft_error("Could not convert glyph to bitmap", error); + } + FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[glyphInd]; + draw_bitmap(im, &bitmap->bitmap, bitmap->left, im.shape(0) - bitmap->top); +} + void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback = false) { diff --git a/src/ft2font.h b/src/ft2font.h index 209581d8f362..febec104e1a1 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -79,6 +79,8 @@ class FT2Font virtual ~FT2Font(); 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, @@ -107,6 +109,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); void get_glyph_name(unsigned int glyph_number, std::string &buffer, bool fallback); 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 ca2db6aa0e5b..849a45ca1db6 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -541,6 +541,25 @@ PyFT2Font_set_size(PyFT2Font *self, double ptsize, double dpi) self->x->set_size(ptsize, dpi); } +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 +)"""; + +static void +PyFT2Font__set_transform( + PyFT2Font *self, std::array, 2> matrix, std::array delta) +{ + self->x->set_transform(matrix, delta); +} + const char *PyFT2Font_set_charmap__doc__ = R"""( Make the i-th charmap current. @@ -971,7 +990,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 @@ -990,9 +1009,36 @@ PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, py::buffer &image, auto xd = _double_to_("x", vxd); auto yd = _double_to_("y", vyd); - self->x->draw_glyph_to_bitmap( - py::array_t{image}, - xd, yd, glyph->glyphInd, antialiased); + self->x->draw_glyph_to_bitmap(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->x->draw_glyph_at(image, x, y, glyph->glyphInd, antialiased); } const char *PyFT2Font_get_glyph_name__doc__ = R"""( @@ -1615,6 +1661,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, @@ -1654,6 +1701,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__)