From 67b4202058dd8e76d7376765a054ae1d89cb27b4 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 12 May 2025 20:08:56 +0200 Subject: [PATCH] Replace FT2Image by plain numpy arrays. --- .../deprecations/30044-AL.rst | 12 ++++ lib/matplotlib/_mathtext.py | 14 +++-- lib/matplotlib/ft2font.pyi | 2 +- lib/matplotlib/tests/test_ft2font.py | 5 +- src/ft2font.cpp | 59 ++++++------------- src/ft2font.h | 13 ++-- src/ft2font_wrapper.cpp | 46 +++++++++------ 7 files changed, 78 insertions(+), 73 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/30044-AL.rst diff --git a/doc/api/next_api_changes/deprecations/30044-AL.rst b/doc/api/next_api_changes/deprecations/30044-AL.rst new file mode 100644 index 000000000000..e004d5f2730f --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30044-AL.rst @@ -0,0 +1,12 @@ +``FT2Image`` +~~~~~~~~~~~~ +... is deprecated. Use 2D uint8 ndarrays instead. In particular: + +- The ``FT2Image`` constructor took ``width, height`` as separate parameters + but the ndarray constructor takes ``(height, width)`` as single tuple + parameter. +- `.FT2Font.draw_glyph_to_bitmap` now (also) takes 2D uint8 arrays as input. +- ``FT2Image.draw_rect_filled`` should be replaced by directly setting pixel + values to black. +- The ``image`` attribute of the object returned by ``MathTextParser("agg").parse`` + is now a 2D uint8 array. diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 3739a517978b..a528a65ca3cb 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -9,6 +9,7 @@ import enum import functools import logging +import math import os import re import types @@ -19,6 +20,7 @@ from typing import NamedTuple import numpy as np +from numpy.typing import NDArray from pyparsing import ( Empty, Forward, Literal, Group, NotAny, OneOrMore, Optional, ParseBaseException, ParseException, ParseExpression, ParseFatalException, @@ -30,7 +32,7 @@ from ._mathtext_data import ( latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni) from .font_manager import FontProperties, findfont, get_font -from .ft2font import FT2Font, FT2Image, Kerning, LoadFlags +from .ft2font import FT2Font, Kerning, LoadFlags if T.TYPE_CHECKING: @@ -99,7 +101,7 @@ class RasterParse(NamedTuple): The offsets are always zero. width, height, depth : float The global metrics. - image : FT2Image + image : 2D array of uint8 A raster image. """ ox: float @@ -107,7 +109,7 @@ class RasterParse(NamedTuple): width: float height: float depth: float - image: FT2Image + image: NDArray[np.uint8] RasterParse.__module__ = "matplotlib.mathtext" @@ -148,7 +150,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(int(np.ceil(w)), int(np.ceil(h + max(d, 0)))) + image = np.zeros((math.ceil(h + max(d, 0)), math.ceil(w)), np.uint8) # Ideally, we could just use self.glyphs and self.rects here, shifting # their coordinates by (-xmin, -ymin), but this yields slightly @@ -167,7 +169,9 @@ def to_raster(self, *, antialiased: bool) -> RasterParse: y = int(center - (height + 1) / 2) else: y = int(y1) - image.draw_rect_filled(int(x1), y, int(np.ceil(x2)), y + height) + x1 = math.floor(x1) + x2 = math.ceil(x2) + image[y:y+height+1, x1:x2+1] = 0xff return RasterParse(0, 0, w, h + d, d, image) diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index b12710afd801..a413cd3c1a76 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: int, y: int, glyph: Glyph, antialiased: bool = ... + self, image: NDArray[np.uint8], 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]: ... diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index a9f2a56658aa..8b448e17b7fd 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -18,7 +18,8 @@ def test_ft2image_draw_rect_filled(): width = 23 height = 42 for x0, y0, x1, y1 in itertools.product([1, 100], [2, 200], [4, 400], [8, 800]): - im = ft2font.FT2Image(width, height) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + im = ft2font.FT2Image(width, height) im.draw_rect_filled(x0, y0, x1, y1) a = np.asarray(im) assert a.dtype == np.uint8 @@ -823,7 +824,7 @@ def test_ft2font_drawing(): np.testing.assert_array_equal(image, expected) font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0) glyph = font.load_char(ord('M')) - image = ft2font.FT2Image(expected.shape[1], expected.shape[0]) + image = np.zeros(expected.shape, np.uint8) font.draw_glyph_to_bitmap(image, -1, 1, glyph, antialiased=False) np.testing.assert_array_equal(image, expected) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 94c554cf9f63..b2c2c0fa9bd1 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -63,51 +63,23 @@ void throw_ft_error(std::string message, FT_Error error) { throw std::runtime_error(os.str()); } -FT2Image::FT2Image() : m_buffer(nullptr), m_width(0), m_height(0) -{ -} - FT2Image::FT2Image(unsigned long width, unsigned long height) - : m_buffer(nullptr), m_width(0), m_height(0) + : m_buffer((unsigned char *)calloc(width * height, 1)), m_width(width), m_height(height) { - resize(width, height); } FT2Image::~FT2Image() { - delete[] m_buffer; + free(m_buffer); } -void FT2Image::resize(long width, long height) +void draw_bitmap( + py::array_t im, FT_Bitmap *bitmap, FT_Int x, FT_Int y) { - if (width <= 0) { - width = 1; - } - if (height <= 0) { - height = 1; - } - size_t numBytes = width * height; - - if ((unsigned long)width != m_width || (unsigned long)height != m_height) { - if (numBytes > m_width * m_height) { - delete[] m_buffer; - m_buffer = nullptr; - m_buffer = new unsigned char[numBytes]; - } + auto buf = im.mutable_data(0); - m_width = (unsigned long)width; - m_height = (unsigned long)height; - } - - if (numBytes && m_buffer) { - memset(m_buffer, 0, numBytes); - } -} - -void FT2Image::draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y) -{ - FT_Int image_width = (FT_Int)m_width; - FT_Int image_height = (FT_Int)m_height; + FT_Int image_width = (FT_Int)im.shape(1); + FT_Int image_height = (FT_Int)im.shape(0); FT_Int char_width = bitmap->width; FT_Int char_height = bitmap->rows; @@ -121,14 +93,14 @@ void FT2Image::draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y) if (bitmap->pixel_mode == FT_PIXEL_MODE_GRAY) { for (FT_Int i = y1; i < y2; ++i) { - unsigned char *dst = m_buffer + (i * image_width + x1); + unsigned char *dst = buf + (i * image_width + x1); unsigned char *src = bitmap->buffer + (((i - y_offset) * bitmap->pitch) + x_start); for (FT_Int j = x1; j < x2; ++j, ++dst, ++src) *dst |= *src; } } else if (bitmap->pixel_mode == FT_PIXEL_MODE_MONO) { for (FT_Int i = y1; i < y2; ++i) { - unsigned char *dst = m_buffer + (i * image_width + x1); + unsigned char *dst = buf + (i * image_width + x1); unsigned char *src = bitmap->buffer + ((i - y_offset) * bitmap->pitch); for (FT_Int j = x1; j < x2; ++j, ++dst) { int x = (j - x1 + x_start); @@ -259,7 +231,7 @@ FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_, std::vector &fallback_list, FT2Font::WarnFunc warn, bool warn_if_used) - : ft_glyph_warn(warn), warn_if_used(warn_if_used), image(), face(nullptr), + : ft_glyph_warn(warn), warn_if_used(warn_if_used), image({1, 1}), face(nullptr), hinting_factor(hinting_factor_), // set default kerning factor to 0, i.e., no kerning manipulation kerning_factor(0) @@ -676,7 +648,8 @@ void FT2Font::draw_glyphs_to_bitmap(bool antialiased) long width = (bbox.xMax - bbox.xMin) / 64 + 2; long height = (bbox.yMax - bbox.yMin) / 64 + 2; - image.resize(width, height); + image = py::array_t{{height, width}}; + std::memset(image.mutable_data(0), 0, image.nbytes()); for (auto & glyph : glyphs) { FT_Error error = FT_Glyph_To_Bitmap( @@ -692,11 +665,13 @@ void FT2Font::draw_glyphs_to_bitmap(bool antialiased) FT_Int x = (FT_Int)(bitmap->left - (bbox.xMin * (1. / 64.))); FT_Int y = (FT_Int)((bbox.yMax * (1. / 64.)) - bitmap->top + 1); - image.draw_bitmap(&bitmap->bitmap, x, y); + draw_bitmap(image, &bitmap->bitmap, x, y); } } -void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased) +void FT2Font::draw_glyph_to_bitmap( + py::array_t im, + int x, int y, size_t glyphInd, bool antialiased) { FT_Vector sub_offset; sub_offset.x = 0; // int((xd - (double)x) * 64.0); @@ -718,7 +693,7 @@ void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, FT_BitmapGlyph bitmap = (FT_BitmapGlyph)glyphs[glyphInd]; - im.draw_bitmap(&bitmap->bitmap, x + bitmap->left, y); + draw_bitmap(im, &bitmap->bitmap, x + bitmap->left, y); } void FT2Font::get_glyph_name(unsigned int glyph_number, std::string &buffer, diff --git a/src/ft2font.h b/src/ft2font.h index cb38e337157a..209581d8f362 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -22,6 +22,10 @@ extern "C" { #include FT_TRUETYPE_TABLES_H } +#include +#include +namespace py = pybind11; + /* By definition, FT_FIXED as 2 16bit values stored in a single long. */ @@ -32,7 +36,6 @@ extern "C" { class FT2Image { public: - FT2Image(); FT2Image(unsigned long width, unsigned long height); virtual ~FT2Image(); @@ -101,7 +104,9 @@ class FT2Font void get_bitmap_offset(long *x, long *y); long get_descent(); void draw_glyphs_to_bitmap(bool antialiased); - void draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased); + void draw_glyph_to_bitmap( + py::array_t im, + int x, int 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); @@ -113,7 +118,7 @@ class FT2Font return face; } - FT2Image &get_image() + py::array_t &get_image() { return image; } @@ -141,7 +146,7 @@ class FT2Font private: WarnFunc ft_glyph_warn; bool warn_if_used; - FT2Image image; + py::array_t image; FT_Face face; FT_Vector pen; /* untransformed origin */ std::vector glyphs; diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 18f26ad4e76b..ca2db6aa0e5b 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -968,7 +968,7 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( Parameters ---------- - image : FT2Image + 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. @@ -983,14 +983,16 @@ const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = R"""( )"""; static void -PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, FT2Image &image, +PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, py::buffer &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); + self->x->draw_glyph_to_bitmap( + py::array_t{image}, + xd, yd, glyph->glyphInd, antialiased); } const char *PyFT2Font_get_glyph_name__doc__ = R"""( @@ -1440,12 +1442,7 @@ const char *PyFT2Font_get_image__doc__ = R"""( static py::array PyFT2Font_get_image(PyFT2Font *self) { - FT2Image &im = self->x->get_image(); - py::ssize_t dims[] = { - static_cast(im.get_height()), - static_cast(im.get_width()) - }; - return py::array_t(dims, im.get_buffer()); + return self->x->get_image(); } const char *PyFT2Font__get_type1_encoding_vector__doc__ = R"""( @@ -1565,6 +1562,10 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) PyFT2Image__doc__) .def(py::init( [](double_or_ width, double_or_ height) { + auto warn = + py::module_::import("matplotlib._api").attr("warn_deprecated"); + warn("since"_a="3.11", "name"_a="FT2Image", "obj_type"_a="class", + "alternative"_a="a 2D uint8 ndarray"); return new FT2Image( _double_to_("width", width), _double_to_("height", height) @@ -1604,8 +1605,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def_property_readonly("bbox", &PyGlyph_get_bbox, "The control box of the glyph."); - py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol(), - PyFT2Font__doc__) + auto cls = py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol(), + PyFT2Font__doc__) .def(py::init(&PyFT2Font_init), "filename"_a, "hinting_factor"_a=8, py::kw_only(), "_fallback_list"_a=py::none(), "_kerning_factor"_a=0, @@ -1639,10 +1640,20 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def("get_descent", &PyFT2Font_get_descent, PyFT2Font_get_descent__doc__) .def("draw_glyphs_to_bitmap", &PyFT2Font_draw_glyphs_to_bitmap, py::kw_only(), "antialiased"_a=true, - PyFT2Font_draw_glyphs_to_bitmap__doc__) - .def("draw_glyph_to_bitmap", &PyFT2Font_draw_glyph_to_bitmap, - "image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true, - PyFT2Font_draw_glyph_to_bitmap__doc__) + PyFT2Font_draw_glyphs_to_bitmap__doc__); + // The generated docstring uses an unqualified "Buffer" as type hint, + // which causes an error in sphinx. This is fixed as of pybind11 + // master (since #5566) which now uses "collections.abc.Buffer"; + // restore the signature once that version is released. + { + py::options options{}; + options.disable_function_signatures(); + cls + .def("draw_glyph_to_bitmap", &PyFT2Font_draw_glyph_to_bitmap, + "image"_a, "x"_a, "y"_a, "glyph"_a, py::kw_only(), "antialiased"_a=true, + PyFT2Font_draw_glyph_to_bitmap__doc__); + } + cls .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__) @@ -1760,10 +1771,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) "The original filename for this object.") .def_buffer([](PyFT2Font &self) -> py::buffer_info { - FT2Image &im = self.x->get_image(); - std::vector shape { im.get_height(), im.get_width() }; - std::vector strides { im.get_width(), 1 }; - return py::buffer_info(im.get_buffer(), shape, strides); + return self.x->get_image().request(); }); m.attr("__freetype_version__") = version_string;