diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index b435ae565ce4..0653a875e2a8 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -70,7 +70,7 @@ def __init__(self, width, height, dpi): self._filter_renderers = [] self._update_methods() - self.mathtext_parser = MathTextParser('agg') + self.mathtext_parser = MathTextParser('path') self.bbox = Bbox.from_bounds(0, 0, self.width, self.height) @@ -172,36 +172,58 @@ def draw_path(self, gc, path, transform, rgbFace=None): def draw_mathtext(self, gc, x, y, s, prop, angle): """Draw mathtext using :mod:`matplotlib.mathtext`.""" - ox, oy, width, height, descent, font_image = \ - self.mathtext_parser.parse(s, self.dpi, prop, - antialiased=gc.get_antialiased()) - - xd = descent * sin(radians(angle)) - yd = descent * cos(radians(angle)) - x = round(x + ox + xd) - y = round(y - oy + yd) - self._renderer.draw_text_image(font_image, x, y + 1, angle, gc) + # y is downwards. + parse = self.mathtext_parser.parse( + s, self.dpi, prop, antialiased=gc.get_antialiased()) + c = cos(radians(angle)) + s = sin(radians(angle)) + for font, size, char, dx, dy in parse.glyphs: # dy is upwards. + font.set_size(size, self.dpi) + bitmap = font._render_glyph( + font.get_char_index(char), + # The "y" parameter is upwards (per FreeType). + x + dx * c - dy * s, self.height - y + dx * s + dy * c, angle, + get_hinting_flag()) + # draw_text_image's y is downwards & the bitmap bottom side. + self._renderer.draw_text_image( + bitmap["buffer"], + bitmap["left"], + int(self.height) - bitmap["top"] + bitmap["buffer"].shape[0], + 0, gc) + if not angle: + for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side. + self._renderer.draw_text_image( + np.full((round(h), round(w)), np.uint8(0xff)), + round(x + dx), round(y - dy - h), + 0, gc) + else: + rgba = gc.get_rgb() + if len(rgba) == 3 or gc.get_forced_alpha(): + rgba = rgba[:3] + (gc.get_alpha(),) + gc1 = self.new_gc() + gc1.set_linewidth(0) + for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side. + path = Path._create_closed( + [(dx, dy), (dx + w, dy), (dx + w, dy + h), (dx, dy + h)]) + self._renderer.draw_path( + gc1, path, + mpl.transforms.Affine2D() + .rotate_deg(angle).translate(x, self.height - y), + rgba) + gc1.restore() def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) font = self._prepare_font(prop) - # We pass '0' for angle here, since it will be rotated (in raster - # space) in the following call to draw_text_image). - font.set_text(s, 0, flags=get_hinting_flag()) - font.draw_glyphs_to_bitmap( - antialiased=gc.get_antialiased()) - d = font.get_descent() / 64.0 - # The descent needs to be adjusted for the angle. - xo, yo = font.get_bitmap_offset() - xo /= 64.0 - yo /= 64.0 - xd = d * sin(radians(angle)) - yd = d * cos(radians(angle)) - x = round(x + xo + xd) - y = round(y + yo + yd) - self._renderer.draw_text_image(font, x, y + 1, angle, gc) + font.set_text(s, angle, flags=get_hinting_flag()) + for bitmap in font._render_glyphs(x, self.height - y): + self._renderer.draw_text_image( + bitmap["buffer"], + bitmap["left"], + int(self.height) - bitmap["top"] + bitmap["buffer"].shape[0], + 0, gc) def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited @@ -211,9 +233,8 @@ def get_text_width_height_descent(self, s, prop, ismath): return super().get_text_width_height_descent(s, prop, ismath) if ismath: - ox, oy, width, height, descent, font_image = \ - self.mathtext_parser.parse(s, self.dpi, prop) - return width, height, descent + parse = self.mathtext_parser.parse(s, self.dpi, prop) + return parse.width, parse.height, parse.depth font = self._prepare_font(prop) font.set_text(s, 0.0, flags=get_hinting_flag()) diff --git a/src/ft2font.h b/src/ft2font.h index cb38e337157a..e52ac94dcd62 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -144,7 +144,9 @@ class FT2Font FT2Image image; FT_Face face; FT_Vector pen; /* untransformed origin */ + public: std::vector glyphs; + private: std::vector fallbacks; std::unordered_map glyph_to_font; std::unordered_map char_to_font; diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 18f26ad4e76b..e6f3f7d57323 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1764,7 +1764,59 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) 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); - }); + }) + + // TODO: Return a nicer structure than dicts. + // NOTE: The lifetime of the buffers is limited and could get invalidated... + // TODO: Real antialiasing flag. + // TODO: throw_ft_error. + // x, y are upwards here + .def("_render_glyph", [](PyFT2Font *self, FT_UInt idx, + double x, double y, double angle, + LoadFlags flags) { + auto face = self->x->get_face(); + auto hf = self->x->get_hinting_factor(); + auto c = std::cos(angle * M_PI / 180) * 0x10000L, + s = std::sin(angle * M_PI / 180) * 0x10000L; + auto matrix = FT_Matrix{ + std::lround(c / hf), std::lround(-s), std::lround(s / hf), std::lround(c)}; + auto delta = FT_Vector{std::lround(x * 64), std::lround(y * 64)}; + FT_Set_Transform(face, &matrix, &delta); + if (auto error = FT_Load_Glyph(face, idx, static_cast(flags))) { + throw std::runtime_error("Could not load glyph"); + } + if (auto error = FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL)) { + throw std::runtime_error("Could not convert glyph to bitmap"); + } + py::dict d; + d["left"] = face->glyph->bitmap_left; + d["top"] = face->glyph->bitmap_top; + d["buffer"] = py::array_t{ + {face->glyph->bitmap.rows, face->glyph->bitmap.width}, + {face->glyph->bitmap.pitch, 1}, + face->glyph->bitmap.buffer}; + return d; + }) + .def("_render_glyphs", [](PyFT2Font *self, double x, double y) { + auto origin = FT_Vector{std::lround(x * 64), std::lround(y * 64)}; + py::list gs; + for (auto &g: self->x->glyphs) { + if (auto error = FT_Glyph_To_Bitmap(&g, FT_RENDER_MODE_NORMAL, &origin, 1)) { + throw std::runtime_error("Could not convert glyph to bitmap"); + } + auto bg = reinterpret_cast(g); + py::dict d; + d["left"] = bg->left; + d["top"] = bg->top; + d["buffer"] = py::array_t{ + {bg->bitmap.rows, bg->bitmap.width}, + {bg->bitmap.pitch, 1}, + bg->bitmap.buffer}; + gs.append(d); + } + return gs; + }) + ; m.attr("__freetype_version__") = version_string; m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE;