Skip to content

Commit f317660

Browse files
committed
Merge branch '30059/ft-direct-render' (early part) into HEAD
2 parents 77b8271 + df28887 commit f317660

File tree

4 files changed

+126
-42
lines changed

4 files changed

+126
-42
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"""
2323

2424
from contextlib import nullcontext
25-
from math import radians, cos, sin
25+
import math
2626

2727
import numpy as np
2828
from PIL import features
@@ -71,7 +71,7 @@ def __init__(self, width, height, dpi):
7171
self._filter_renderers = []
7272

7373
self._update_methods()
74-
self.mathtext_parser = MathTextParser('agg')
74+
self.mathtext_parser = MathTextParser('path')
7575

7676
self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
7777

@@ -173,48 +173,64 @@ def draw_path(self, gc, path, transform, rgbFace=None):
173173

174174
def draw_mathtext(self, gc, x, y, s, prop, angle):
175175
"""Draw mathtext using :mod:`matplotlib.mathtext`."""
176-
ox, oy, width, height, descent, font_image = \
177-
self.mathtext_parser.parse(s, self.dpi, prop,
178-
antialiased=gc.get_antialiased())
179-
180-
xd = descent * sin(radians(angle))
181-
yd = descent * cos(radians(angle))
182-
x = round(x + ox + xd)
183-
y = round(y - oy + yd)
184-
self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)
176+
# y is downwards.
177+
parse = self.mathtext_parser.parse(
178+
s, self.dpi, prop, antialiased=gc.get_antialiased())
179+
cos = math.cos(math.radians(angle))
180+
sin = math.sin(math.radians(angle))
181+
for font, size, char, glyph_index, dx, dy in parse.glyphs: # dy is upwards.
182+
font.set_size(size, self.dpi)
183+
hf = font._hinting_factor
184+
font._set_transform(
185+
[[round(0x10000 * cos / hf), round(0x10000 * -sin)],
186+
[round(0x10000 * sin / hf), round(0x10000 * cos)]],
187+
[round(0x40 * (x + dx * cos - dy * sin)),
188+
# FreeType's y is upwards.
189+
round(0x40 * (self.height - y + dx * sin + dy * cos))]
190+
)
191+
bitmap = font._render_glyph(glyph_index, get_hinting_flag())
192+
# draw_text_image's y is downwards & the bitmap bottom side.
193+
self._renderer.draw_text_image(
194+
bitmap["buffer"],
195+
bitmap["left"],
196+
int(self.height) - bitmap["top"] + bitmap["buffer"].shape[0],
197+
0, gc)
198+
if not angle:
199+
for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side.
200+
self._renderer.draw_text_image(
201+
np.full((round(h), round(w)), np.uint8(0xff)),
202+
round(x + dx), round(y - dy - h),
203+
0, gc)
204+
else:
205+
rgba = gc.get_rgb()
206+
if len(rgba) == 3 or gc.get_forced_alpha():
207+
rgba = rgba[:3] + (gc.get_alpha(),)
208+
gc1 = self.new_gc()
209+
gc1.set_linewidth(0)
210+
for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side.
211+
path = Path._create_closed(
212+
[(dx, dy), (dx + w, dy), (dx + w, dy + h), (dx, dy + h)])
213+
self._renderer.draw_path(
214+
gc1, path,
215+
mpl.transforms.Affine2D()
216+
.rotate_deg(angle).translate(x, self.height - y),
217+
rgba)
218+
gc1.restore()
185219

186220
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
187221
# docstring inherited
188222
if ismath:
189223
return self.draw_mathtext(gc, x, y, s, prop, angle)
190224
font = self._prepare_font(prop)
191-
# We pass '0' for angle here, since it will be rotated (in raster
192-
# space) in the following call to draw_text_image).
193-
font.set_text(s, 0, flags=get_hinting_flag(),
225+
font.set_text(s, angle, flags=get_hinting_flag(),
194226
features=mtext.get_fontfeatures() if mtext is not None else None,
195227
language=mtext.get_language() if mtext is not None else None)
196-
font.draw_glyphs_to_bitmap(
197-
antialiased=gc.get_antialiased())
198-
d = font.get_descent() / 64.0
199-
# The descent needs to be adjusted for the angle.
200-
xo, yo = font.get_bitmap_offset()
201-
xo /= 64.0
202-
yo /= 64.0
203-
204-
rad = radians(angle)
205-
xd = d * sin(rad)
206-
yd = d * cos(rad)
207-
# Rotating the offset vector ensures text rotates around the anchor point.
208-
# Without this, rotated text offsets incorrectly, causing a horizontal shift.
209-
# Applying the 2D rotation matrix.
210-
rotated_xo = xo * cos(rad) - yo * sin(rad)
211-
rotated_yo = xo * sin(rad) + yo * cos(rad)
212-
# Subtract rotated_yo to account for the inverted y-axis in computer graphics,
213-
# compared to the mathematical convention.
214-
x = round(x + rotated_xo + xd)
215-
y = round(y - rotated_yo + yd)
216-
217-
self._renderer.draw_text_image(font, x, y + 1, angle, gc)
228+
for bitmap in font._render_glyphs(x, self.height - y):
229+
self._renderer.draw_text_image(
230+
bitmap["buffer"],
231+
bitmap["left"],
232+
int(self.height) - bitmap["top"] + bitmap["buffer"].shape[0],
233+
0, gc)
218234

219235
def get_text_width_height_descent(self, s, prop, ismath):
220236
# docstring inherited
@@ -224,9 +240,8 @@ def get_text_width_height_descent(self, s, prop, ismath):
224240
return super().get_text_width_height_descent(s, prop, ismath)
225241

226242
if ismath:
227-
ox, oy, width, height, descent, font_image = \
228-
self.mathtext_parser.parse(s, self.dpi, prop)
229-
return width, height, descent
243+
parse = self.mathtext_parser.parse(s, self.dpi, prop)
244+
return parse.width, parse.height, parse.depth
230245

231246
font = self._prepare_font(prop)
232247
font.set_text(s, 0.0, flags=get_hinting_flag())
@@ -248,8 +263,8 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
248263
Z = np.array(Z * 255.0, np.uint8)
249264

250265
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
251-
xd = d * sin(radians(angle))
252-
yd = d * cos(radians(angle))
266+
xd = d * math.sin(math.radians(angle))
267+
yd = d * math.cos(math.radians(angle))
253268
x = round(x + xd)
254269
y = round(y + yd)
255270
self._renderer.draw_text_image(Z, x, y, angle, gc)

src/ft2font.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,17 @@ void FT2Font::set_size(double ptsize, double dpi)
283283
}
284284
}
285285

286+
void FT2Font::_set_transform(
287+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta)
288+
{
289+
FT_Matrix m = {matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]};
290+
FT_Vector d = {delta[0], delta[1]};
291+
FT_Set_Transform(face, &m, &d);
292+
for (auto & fallback : fallbacks) {
293+
fallback->_set_transform(matrix, delta);
294+
}
295+
}
296+
286297
void FT2Font::set_charmap(int i)
287298
{
288299
if (i >= face->num_charmaps) {

src/ft2font.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ class FT2Font
111111
void close();
112112
void clear();
113113
void set_size(double ptsize, double dpi);
114+
void _set_transform(
115+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta);
114116
void set_charmap(int i);
115117
void select_charmap(unsigned long i);
116118
std::vector<raqm_glyph_t> layout(std::u32string_view text, FT_Int32 flags,
@@ -184,7 +186,9 @@ class FT2Font
184186
py::array_t<uint8_t, py::array::c_style> image;
185187
FT_Face face;
186188
FT_Vector pen; /* untransformed origin */
189+
public:
187190
std::vector<FT_Glyph> glyphs;
191+
private:
188192
std::vector<FT2Font *> fallbacks;
189193
std::unordered_map<long, FT2Font *> char_to_font;
190194
FT_BBox bbox;

src/ft2font_wrapper.cpp

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,19 @@ const char *PyFT2Font_set_size__doc__ = R"""(
538538
The DPI used for rendering the text.
539539
)""";
540540

541+
const char *PyFT2Font__set_transform__doc__ = R"""(
542+
Set the transform of the text.
543+
544+
This is a low-level function, where *matrix* and *delta* are directly in
545+
16.16 and 26.6 formats respectively. Refer to the FreeType docs of
546+
FT_Set_Transform for further description.
547+
548+
Parameters
549+
----------
550+
matrix : (2, 2) array of int
551+
delta : (2,) array of int
552+
)""";
553+
541554
const char *PyFT2Font_set_charmap__doc__ = R"""(
542555
Make the i-th charmap current.
543556
@@ -1644,6 +1657,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16441657
.def("clear", &PyFT2Font::clear, PyFT2Font_clear__doc__)
16451658
.def("set_size", &PyFT2Font::set_size, "ptsize"_a, "dpi"_a,
16461659
PyFT2Font_set_size__doc__)
1660+
.def("_set_transform", &PyFT2Font::_set_transform, "matrix"_a, "delta"_a,
1661+
PyFT2Font__set_transform__doc__)
16471662
.def("set_charmap", &PyFT2Font::set_charmap, "i"_a,
16481663
PyFT2Font_set_charmap__doc__)
16491664
.def("select_charmap", &PyFT2Font::select_charmap, "i"_a,
@@ -1802,10 +1817,49 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
18021817
.def_property_readonly(
18031818
"fname", &PyFT2Font_fname,
18041819
"The original filename for this object.")
1820+
.def_property_readonly(
1821+
"_hinting_factor", [](PyFT2Font *self) {
1822+
return self->get_hinting_factor();
1823+
}, "The hinting factor.")
18051824

18061825
.def_buffer([](PyFT2Font &self) -> py::buffer_info {
18071826
return self.get_image().request();
1808-
});
1827+
})
1828+
1829+
// TODO: Return a nicer structure than dicts.
1830+
// NOTE: The lifetime of the buffers is limited and could get invalidated...
1831+
// TODO: Real antialiasing flag.
1832+
.def("_render_glyph", [](PyFT2Font *self, FT_UInt idx, LoadFlags flags) {
1833+
auto face = self->get_face();
1834+
FT_CHECK(FT_Load_Glyph, face, idx, static_cast<FT_Int32>(flags));
1835+
FT_CHECK(FT_Render_Glyph, face->glyph, FT_RENDER_MODE_NORMAL);
1836+
py::dict d;
1837+
d["left"] = face->glyph->bitmap_left;
1838+
d["top"] = face->glyph->bitmap_top;
1839+
d["buffer"] = py::array_t<uint8_t>{
1840+
{face->glyph->bitmap.rows, face->glyph->bitmap.width},
1841+
{face->glyph->bitmap.pitch, 1},
1842+
face->glyph->bitmap.buffer};
1843+
return d;
1844+
})
1845+
.def("_render_glyphs", [](PyFT2Font *self, double x, double y) {
1846+
auto origin = FT_Vector{std::lround(x * 64), std::lround(y * 64)};
1847+
py::list gs;
1848+
for (auto &g: self->glyphs) {
1849+
FT_CHECK(FT_Glyph_To_Bitmap, &g, FT_RENDER_MODE_NORMAL, &origin, 1);
1850+
auto bg = reinterpret_cast<FT_BitmapGlyph>(g);
1851+
py::dict d;
1852+
d["left"] = bg->left;
1853+
d["top"] = bg->top;
1854+
d["buffer"] = py::array_t<uint8_t>{
1855+
{bg->bitmap.rows, bg->bitmap.width},
1856+
{bg->bitmap.pitch, 1},
1857+
bg->bitmap.buffer};
1858+
gs.append(d);
1859+
}
1860+
return gs;
1861+
})
1862+
;
18091863

18101864
m.attr("__freetype_version__") = version_string;
18111865
m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE;

0 commit comments

Comments
 (0)