From 4d40c1bfb18acfc4450dda3369129cca4030bcff Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 24 Sep 2024 02:14:34 -0400 Subject: [PATCH] Deprecate passing floats to FT2Image These were silently truncated to int anyway, so we should make the types explicit. --- .../deprecations/28967-ES.rst | 7 +++ lib/matplotlib/_mathtext.py | 6 +-- lib/matplotlib/ft2font.pyi | 6 +-- src/ft2font_wrapper.cpp | 50 +++++++++++++++++-- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/doc/api/next_api_changes/deprecations/28967-ES.rst b/doc/api/next_api_changes/deprecations/28967-ES.rst index 7a7a5bd4e6c9..8bb238def943 100644 --- a/doc/api/next_api_changes/deprecations/28967-ES.rst +++ b/doc/api/next_api_changes/deprecations/28967-ES.rst @@ -3,3 +3,10 @@ Passing floating-point values to ``RendererAgg.draw_text_image`` Any floating-point values passed to the *x* and *y* parameters were truncated to integers silently. This behaviour is now deprecated, and only `int` values should be used. + +Passing floating-point values to ``FT2Image`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Any floating-point values passed to the `.FT2Image` constructor, or the *x0*, *y0*, *x1*, +and *y1* parameters of `.FT2Image.draw_rect_filled` were truncated to integers silently. +This behaviour is now deprecated, and only `int` values should be used. diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index c9bb90608a85..6a1d9add9e8a 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -153,7 +153,7 @@ def to_raster(self, *, antialiased: bool) -> RasterParse: w = xmax - xmin h = ymax - ymin - self.box.depth d = ymax - ymin - self.box.height - image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0))) + image = FT2Image(int(np.ceil(w)), int(np.ceil(h + max(d, 0)))) # Ideally, we could just use self.glyphs and self.rects here, shifting # their coordinates by (-xmin, -ymin), but this yields slightly @@ -163,7 +163,7 @@ def to_raster(self, *, antialiased: bool) -> RasterParse: for ox, oy, info in shifted.glyphs: info.font.draw_glyph_to_bitmap( - image, ox, oy - info.metrics.iceberg, info.glyph, + image, int(ox), int(oy - info.metrics.iceberg), info.glyph, antialiased=antialiased) for x1, y1, x2, y2 in shifted.rects: height = max(int(y2 - y1) - 1, 0) @@ -172,7 +172,7 @@ def to_raster(self, *, antialiased: bool) -> RasterParse: y = int(center - (height + 1) / 2) else: y = int(y1) - image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height) + image.draw_rect_filled(int(x1), y, int(np.ceil(x2)), y + height) return RasterParse(0, 0, w, h + d, d, image) diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 88c65d139939..1638bac692d3 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -198,7 +198,7 @@ class FT2Font(Buffer): def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... def clear(self) -> None: ... def draw_glyph_to_bitmap( - self, image: FT2Image, x: float, y: float, glyph: Glyph, antialiased: bool = ... + self, image: FT2Image, x: int, y: int, glyph: Glyph, antialiased: bool = ... ) -> None: ... def draw_glyphs_to_bitmap(self, antialiased: bool = ...) -> None: ... def get_bitmap_offset(self) -> tuple[int, int]: ... @@ -281,8 +281,8 @@ class FT2Font(Buffer): @final class FT2Image(Buffer): - def __init__(self, width: float, height: float) -> None: ... - def draw_rect_filled(self, x0: float, y0: float, x1: float, y1: float) -> None: ... + def __init__(self, width: int, height: int) -> None: ... + def draw_rect_filled(self, x0: int, y0: int, x1: int, y1: int) -> None: ... if sys.version_info[:2] >= (3, 12): def __buffer__(self, flags: int) -> memoryview: ... diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 59400d4a2de5..9f9fab999707 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -13,6 +13,30 @@ namespace py = pybind11; using namespace pybind11::literals; +template +using double_or_ = std::variant; + +template +static T +_double_to_(const char *name, double_or_ &var) +{ + if (auto value = std::get_if(&var)) { + auto api = py::module_::import("matplotlib._api"); + auto warn = api.attr("warn_deprecated"); + warn("since"_a="3.10", "name"_a=name, "obj_type"_a="parameter as float", + "alternative"_a="int({})"_s.format(name)); + return static_cast(*value); + } else if (auto value = std::get_if(&var)) { + return *value; + } else { + // pybind11 will have only allowed types that match the variant, so this `else` + // can't happen. We only have this case because older macOS doesn't support + // `std::get` and using the conditional `std::get_if` means an `else` to silence + // compiler warnings about "unhandled" cases. + throw std::runtime_error("Should not happen"); + } +} + /********************************************************************** * Enumerations * */ @@ -227,8 +251,15 @@ const char *PyFT2Image_draw_rect_filled__doc__ = R"""( )"""; static void -PyFT2Image_draw_rect_filled(FT2Image *self, double x0, double y0, double x1, double y1) +PyFT2Image_draw_rect_filled(FT2Image *self, + double_or_ vx0, double_or_ vy0, + double_or_ vx1, double_or_ vy1) { + auto x0 = _double_to_("x0", vx0); + auto y0 = _double_to_("y0", vy0); + auto x1 = _double_to_("x1", vx1); + auto y1 = _double_to_("y1", vy1); + self->draw_rect_filled(x0, y0, x1, y1); } @@ -920,7 +951,7 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( ---------- image : FT2Image The image buffer on which to draw the glyph. - x, y : float + x, y : int The pixel location at which to draw the glyph. glyph : Glyph The glyph to draw. @@ -933,9 +964,13 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( )"""; static void -PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, FT2Image &image, double xd, double yd, +PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, FT2Image &image, + double_or_ vxd, double_or_ vyd, PyGlyph *glyph, bool antialiased = true) { + auto xd = _double_to_("x", vxd); + auto yd = _double_to_("y", vyd); + self->x->draw_glyph_to_bitmap(image, xd, yd, glyph->glyphInd, antialiased); } @@ -1625,7 +1660,14 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) py::class_(m, "FT2Image", py::is_final(), py::buffer_protocol(), PyFT2Image__doc__) - .def(py::init(), "width"_a, "height"_a, PyFT2Image_init__doc__) + .def(py::init( + [](double_or_ width, double_or_ height) { + return new FT2Image( + _double_to_("width", width), + _double_to_("height", height) + ); + }), + "width"_a, "height"_a, PyFT2Image_init__doc__) .def("draw_rect_filled", &PyFT2Image_draw_rect_filled, "x0"_a, "y0"_a, "x1"_a, "y1"_a, PyFT2Image_draw_rect_filled__doc__)