Skip to content

Commit df28887

Browse files
anntzerQuLogic
authored andcommitted
Drop the FT2Font intermediate buffer
Directly render FT glyphs to the Agg buffer. In particular, this naturally provides, with no extra work, subpixel positioning of glyphs (which could also have been implemented in the old framework, but would have required careful tracking of subpixel offets). Note that all baseline images should be regenerated. The new APIs added to FT2Font are also up to bikeshedding (but they are all private).
1 parent 44c17bc commit df28887

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,47 +173,63 @@ 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
language=mtext.get_language() if mtext is not None else None)
195-
font.draw_glyphs_to_bitmap(
196-
antialiased=gc.get_antialiased())
197-
d = font.get_descent() / 64.0
198-
# The descent needs to be adjusted for the angle.
199-
xo, yo = font.get_bitmap_offset()
200-
xo /= 64.0
201-
yo /= 64.0
202-
203-
rad = radians(angle)
204-
xd = d * sin(rad)
205-
yd = d * cos(rad)
206-
# Rotating the offset vector ensures text rotates around the anchor point.
207-
# Without this, rotated text offsets incorrectly, causing a horizontal shift.
208-
# Applying the 2D rotation matrix.
209-
rotated_xo = xo * cos(rad) - yo * sin(rad)
210-
rotated_yo = xo * sin(rad) + yo * cos(rad)
211-
# Subtract rotated_yo to account for the inverted y-axis in computer graphics,
212-
# compared to the mathematical convention.
213-
x = round(x + rotated_xo + xd)
214-
y = round(y - rotated_yo + yd)
215-
216-
self._renderer.draw_text_image(font, x, y + 1, angle, gc)
227+
for bitmap in font._render_glyphs(x, self.height - y):
228+
self._renderer.draw_text_image(
229+
bitmap["buffer"],
230+
bitmap["left"],
231+
int(self.height) - bitmap["top"] + bitmap["buffer"].shape[0],
232+
0, gc)
217233

218234
def get_text_width_height_descent(self, s, prop, ismath):
219235
# docstring inherited
@@ -223,9 +239,8 @@ def get_text_width_height_descent(self, s, prop, ismath):
223239
return super().get_text_width_height_descent(s, prop, ismath)
224240

225241
if ismath:
226-
ox, oy, width, height, descent, font_image = \
227-
self.mathtext_parser.parse(s, self.dpi, prop)
228-
return width, height, descent
242+
parse = self.mathtext_parser.parse(s, self.dpi, prop)
243+
return parse.width, parse.height, parse.depth
229244

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

249264
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
250-
xd = d * sin(radians(angle))
251-
yd = d * cos(radians(angle))
265+
xd = d * math.sin(math.radians(angle))
266+
yd = d * math.cos(math.radians(angle))
252267
x = round(x + xd)
253268
y = round(y + yd)
254269
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
void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
@@ -179,7 +181,9 @@ class FT2Font
179181
py::array_t<uint8_t, py::array::c_style> image;
180182
FT_Face face;
181183
FT_Vector pen; /* untransformed origin */
184+
public:
182185
std::vector<FT_Glyph> glyphs;
186+
private:
183187
std::vector<FT2Font *> fallbacks;
184188
std::unordered_map<long, FT2Font *> char_to_font;
185189
FT_BBox bbox;

src/ft2font_wrapper.cpp

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

538+
const char *PyFT2Font__set_transform__doc__ = R"""(
539+
Set the transform of the text.
540+
541+
This is a low-level function, where *matrix* and *delta* are directly in
542+
16.16 and 26.6 formats respectively. Refer to the FreeType docs of
543+
FT_Set_Transform for further description.
544+
545+
Parameters
546+
----------
547+
matrix : (2, 2) array of int
548+
delta : (2,) array of int
549+
)""";
550+
538551
const char *PyFT2Font_set_charmap__doc__ = R"""(
539552
Make the i-th charmap current.
540553
@@ -1542,6 +1555,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
15421555
.def("clear", &PyFT2Font::clear, PyFT2Font_clear__doc__)
15431556
.def("set_size", &PyFT2Font::set_size, "ptsize"_a, "dpi"_a,
15441557
PyFT2Font_set_size__doc__)
1558+
.def("_set_transform", &PyFT2Font::_set_transform, "matrix"_a, "delta"_a,
1559+
PyFT2Font__set_transform__doc__)
15451560
.def("set_charmap", &PyFT2Font::set_charmap, "i"_a,
15461561
PyFT2Font_set_charmap__doc__)
15471562
.def("select_charmap", &PyFT2Font::select_charmap, "i"_a,
@@ -1699,10 +1714,49 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16991714
.def_property_readonly(
17001715
"fname", &PyFT2Font_fname,
17011716
"The original filename for this object.")
1717+
.def_property_readonly(
1718+
"_hinting_factor", [](PyFT2Font *self) {
1719+
return self->get_hinting_factor();
1720+
}, "The hinting factor.")
17021721

17031722
.def_buffer([](PyFT2Font &self) -> py::buffer_info {
17041723
return self.get_image().request();
1705-
});
1724+
})
1725+
1726+
// TODO: Return a nicer structure than dicts.
1727+
// NOTE: The lifetime of the buffers is limited and could get invalidated...
1728+
// TODO: Real antialiasing flag.
1729+
.def("_render_glyph", [](PyFT2Font *self, FT_UInt idx, LoadFlags flags) {
1730+
auto face = self->get_face();
1731+
FT_CHECK(FT_Load_Glyph, face, idx, static_cast<FT_Int32>(flags));
1732+
FT_CHECK(FT_Render_Glyph, face->glyph, FT_RENDER_MODE_NORMAL);
1733+
py::dict d;
1734+
d["left"] = face->glyph->bitmap_left;
1735+
d["top"] = face->glyph->bitmap_top;
1736+
d["buffer"] = py::array_t<uint8_t>{
1737+
{face->glyph->bitmap.rows, face->glyph->bitmap.width},
1738+
{face->glyph->bitmap.pitch, 1},
1739+
face->glyph->bitmap.buffer};
1740+
return d;
1741+
})
1742+
.def("_render_glyphs", [](PyFT2Font *self, double x, double y) {
1743+
auto origin = FT_Vector{std::lround(x * 64), std::lround(y * 64)};
1744+
py::list gs;
1745+
for (auto &g: self->glyphs) {
1746+
FT_CHECK(FT_Glyph_To_Bitmap, &g, FT_RENDER_MODE_NORMAL, &origin, 1);
1747+
auto bg = reinterpret_cast<FT_BitmapGlyph>(g);
1748+
py::dict d;
1749+
d["left"] = bg->left;
1750+
d["top"] = bg->top;
1751+
d["buffer"] = py::array_t<uint8_t>{
1752+
{bg->bitmap.rows, bg->bitmap.width},
1753+
{bg->bitmap.pitch, 1},
1754+
bg->bitmap.buffer};
1755+
gs.append(d);
1756+
}
1757+
return gs;
1758+
})
1759+
;
17061760

17071761
m.attr("__freetype_version__") = version_string;
17081762
m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE;

0 commit comments

Comments
 (0)