Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@
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

Expand Down Expand Up @@ -299,6 +302,23 @@
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))

Check warning on line 316 in lib/matplotlib/backends/backend_agg.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Undefined name `radians` Raw Output: message:"Undefined name `radians`" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/backends/backend_agg.py" range:{start:{line:316 column:22} end:{line:316 column:29}}} severity:WARNING source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"F821" url:"https://docs.astral.sh/ruff/rules/undefined-name"}

Check warning on line 316 in lib/matplotlib/backends/backend_agg.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Undefined name `sin` Raw Output: message:"Undefined name `sin`" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/backends/backend_agg.py" range:{start:{line:316 column:18} end:{line:316 column:21}}} severity:WARNING source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"F821" url:"https://docs.astral.sh/ruff/rules/undefined-name"}
yd = d * cos(radians(angle))

Check warning on line 317 in lib/matplotlib/backends/backend_agg.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Undefined name `radians` Raw Output: message:"Undefined name `radians`" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/backends/backend_agg.py" range:{start:{line:317 column:22} end:{line:317 column:29}}} severity:WARNING source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"F821" url:"https://docs.astral.sh/ruff/rules/undefined-name"}

Check warning on line 317 in lib/matplotlib/backends/backend_agg.py

View workflow job for this annotation

GitHub Actions / ruff

[rdjson] reported by reviewdog 🐶 Undefined name `cos` Raw Output: message:"Undefined name `cos`" location:{path:"/home/runner/work/matplotlib/matplotlib/lib/matplotlib/backends/backend_agg.py" range:{start:{line:317 column:18} end:{line:317 column:21}}} severity:WARNING source:{name:"ruff" url:"https://docs.astral.sh/ruff"} code:{value:"F821" url:"https://docs.astral.sh/ruff/rules/undefined-name"}
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
Expand Down
10 changes: 10 additions & 0 deletions lib/matplotlib/texmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
4 changes: 4 additions & 0 deletions lib/matplotlib/texmanager.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
77 changes: 77 additions & 0 deletions src/_backend_agg.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ class RendererAgg
template <class ImageArray>
void draw_text_image(GCAgg &gc, ImageArray &image, int x, int y, double angle);

template <class ImageArray>
void draw_text_image_mm4(GCAgg &gc, ImageArray &image, int x, int y, double angle);

template <class ImageArray>
void draw_image(GCAgg &gc,
double x,
Expand Down Expand Up @@ -784,6 +787,80 @@ inline void RendererAgg::draw_text_image(GCAgg &gc, ImageArray &image, int x, in
}
}

template <class ImageArray>
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<double>(image.shape(0)),
image_width = static_cast<double>(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<agg::path_storage> 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<agg::pixfmt_rgba32, interp_t> sg(
pixf_img, agg::rgba::no_color(), interpolator
);

theRasterizer.add_path(rect2);
agg::scanline_u8 sl;
agg::span_allocator<agg::rgba8> 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<agg::rgba8*>(static_cast<void*>(data));
pixFmt.blend_color_hspan(text.x1, yi, deltax, src_span, 0, agg::cover_full);
}
}
}
}

class span_conv_alpha
{
public:
Expand Down
44 changes: 44 additions & 0 deletions src/_backend_agg_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<agg::int8u, py::array::c_style | py::array::forcecast> image_obj,
std::variant<double, int> vx,
std::variant<double, int> vy,
double angle,
GCAgg &gc)
{
int x, y;

if (auto value = std::get_if<double>(&vx)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go straight for the newer API here, no need to introduce the old one.

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<int>(*value);
} else if (auto value = std::get_if<int>(&vx)) {
x = *value;
} else {
throw std::runtime_error("Should not happen");
}

if (auto value = std::get_if<double>(&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<int>(*value);
} else if (auto value = std::get_if<int>(&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,
Expand Down Expand Up @@ -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,
Expand Down
Loading