diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index f0006a2d7dbe..d8e7bc5322fb 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -271,6 +271,9 @@ def get_text_width_height_descent(self, s, prop, ismath): return w, h, d def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): + # Injection point + return self.draw_tex_mm4(gc, x, y, s, prop, angle) + # docstring inherited # todo, handle props, angle, origins @@ -299,6 +302,23 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): for text in page.text), ((box.x, box.y, box.width, box.height) for box in page.boxes)) + def draw_tex_mm4(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_rgba_mm4(s, size, self.dpi) + Z = np.array(Z * 255.0, np.uint8) + + 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_mm4(Z, x, y, angle, gc) + def get_canvas_width_height(self): # docstring inherited return self.width, self.height diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 35651a94aa85..2e0670f9cb8e 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -344,6 +344,16 @@ def get_grey(cls, tex, fontsize=None, dpi=None): cls._grey_arrayd[key] = alpha = rgba[:, :, -1] return alpha + @classmethod + def get_rgba_mm4(cls, tex, fontsize=None, dpi=None): + """Return the full PNG image in RGBA format.""" + fontsize = mpl._val_or_rc(fontsize, 'font.size') + dpi = mpl._val_or_rc(dpi, 'savefig.dpi') + key = cls._get_tex_source(tex, fontsize), dpi + pngfile = cls.make_png(tex, fontsize, dpi) + rgba = mpl.image.imread(pngfile) + return rgba + @classmethod def get_rgba(cls, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)): r""" diff --git a/lib/matplotlib/texmanager.pyi b/lib/matplotlib/texmanager.pyi index 94f0d76fa814..4d7528a48b09 100644 --- a/lib/matplotlib/texmanager.pyi +++ b/lib/matplotlib/texmanager.pyi @@ -25,6 +25,10 @@ class TexManager: cls, tex: str, fontsize: float | None = ..., dpi: float | None = ... ) -> np.ndarray: ... @classmethod + def get_rgba_mm4( + cls, tex: str, fontsize: float | None = ..., dpi: float | None = ... + ) -> np.ndarray: ... + @classmethod def get_rgba( cls, tex: str, diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 1ac3d4c06b13..823edcf82616 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -152,6 +152,9 @@ class RendererAgg template void draw_text_image(GCAgg &gc, ImageArray &image, int x, int y, double angle); + template + void draw_text_image_mm4(GCAgg &gc, ImageArray &image, int x, int y, double angle); + template void draw_image(GCAgg &gc, double x, @@ -784,6 +787,80 @@ inline void RendererAgg::draw_text_image(GCAgg &gc, ImageArray &image, int x, in } } +template +inline void RendererAgg::draw_text_image_mm4(GCAgg &gc, ImageArray &image, int x, int y, double angle) +{ + theRasterizer.reset_clipping(); + rendererBase.reset_clipping(true); + + if (angle != 0.0) { + auto image_height = static_cast(image.shape(0)), + image_width = static_cast(image.shape(1)); + + agg::rendering_buffer srcbuf( + image.mutable_data(0, 0, 0), + (unsigned)image_width, + (unsigned)image_height, + (unsigned)image_width * sizeof(agg::rgba8)); + agg::pixfmt_rgba32 pixf_img(srcbuf); + + set_clipbox(gc.cliprect, theRasterizer); + + agg::trans_affine mtx; + mtx *= agg::trans_affine_translation(0, -image_height); + mtx *= agg::trans_affine_rotation(-angle * (agg::pi / 180.0)); + mtx *= agg::trans_affine_translation(x, y); + + agg::path_storage rect; + rect.move_to(0, 0); + rect.line_to(image_width, 0); + rect.line_to(image_width, image_height); + rect.line_to(0, image_height); + rect.line_to(0, 0); + agg::conv_transform rect2(rect, mtx); + + agg::trans_affine inv_mtx(mtx); + inv_mtx.invert(); + + typedef agg::span_interpolator_linear<> interp_t; + interp_t interpolator(inv_mtx); + agg::span_image_filter_rgba_bilinear_clip sg( + pixf_img, agg::rgba::no_color(), interpolator + ); + + theRasterizer.add_path(rect2); + agg::scanline_u8 sl; + agg::span_allocator sa; + agg::render_scanlines_aa(theRasterizer, sl, rendererBase, sa, sg); + } else { + agg::rect_i fig, text; + fig.init(0, 0, width, height); + text.init(x, y - image.shape(0), x + image.shape(1), y); + text.clip(fig); + + if (gc.cliprect.x1 != 0.0 || gc.cliprect.y1 != 0.0 || gc.cliprect.x2 != 0.0 || gc.cliprect.y2 != 0.0) { + agg::rect_i clip; + + clip.init(mpl_round_to_int(gc.cliprect.x1), + mpl_round_to_int(height - gc.cliprect.y2), + mpl_round_to_int(gc.cliprect.x2), + mpl_round_to_int(height - gc.cliprect.y1)); + text.clip(clip); + } + + if (text.x2 > text.x1) { + int deltax = text.x2 - text.x1; + int deltax2 = text.x1 - x; + int deltay = y - image.shape(0); + for (int yi = text.y1; yi < text.y2; ++yi) { + unsigned char *data = &image(yi - deltay, deltax2, 0); + agg::rgba8 *src_span = static_cast(static_cast(data)); + pixFmt.blend_color_hspan(text.x1, yi, deltax, src_span, 0, agg::cover_full); + } + } + } +} + class span_conv_alpha { public: diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 3dd50b31f64a..2c7fb266edee 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -95,6 +95,48 @@ PyRendererAgg_draw_text_image(RendererAgg *self, self->draw_text_image(gc, image, x, y, angle); } +static void +PyRendererAgg_draw_text_image_mm4(RendererAgg *self, + py::array_t image_obj, + std::variant vx, + std::variant vy, + double angle, + GCAgg &gc) +{ + int x, y; + + if (auto value = std::get_if(&vx)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="x", "obj_type"_a="parameter as float", + "alternative"_a="int(x)"); + x = static_cast(*value); + } else if (auto value = std::get_if(&vx)) { + x = *value; + } else { + throw std::runtime_error("Should not happen"); + } + + if (auto value = std::get_if(&vy)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a="y", "obj_type"_a="parameter as float", + "alternative"_a="int(y)"); + y = static_cast(*value); + } else if (auto value = std::get_if(&vy)) { + y = *value; + } else { + throw std::runtime_error("Should not happen"); + } + + // TODO: This really shouldn't be mutable, but Agg's renderer buffers aren't const. + // NOTE: The only difference from the original is the number of dimensions. + // In my example, the shape was (10, 164, 4). The last is the color channels. + auto image = image_obj.mutable_unchecked<3>(); + + self->draw_text_image_mm4(gc, image, x, y, angle); +} + static void PyRendererAgg_draw_markers(RendererAgg *self, GCAgg &gc, @@ -227,6 +269,8 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used()) "face"_a = nullptr) .def("draw_text_image", &PyRendererAgg_draw_text_image, "image"_a, "x"_a, "y"_a, "angle"_a, "gc"_a) + .def("draw_text_image_mm4", &PyRendererAgg_draw_text_image_mm4, + "image"_a, "x"_a, "y"_a, "angle"_a, "gc"_a) .def("draw_image", &PyRendererAgg_draw_image, "gc"_a, "x"_a, "y"_a, "image"_a) .def("draw_path_collection", &PyRendererAgg_draw_path_collection,