Skip to content

Commit 6c6ec62

Browse files
committed
Merge branch '30059/ft-direct-render' into libraqm-full
2 parents 23a1cc5 + 4942861 commit 6c6ec62

File tree

3 files changed

+97
-38
lines changed

3 files changed

+97
-38
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -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,60 @@ 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+
c = cos(radians(angle))
180+
s = sin(radians(angle))
181+
for font, size, char, dx, dy in parse.glyphs: # dy is upwards.
182+
font.set_size(size, self.dpi)
183+
bitmap = font._render_glyph(
184+
font.get_char_index(char),
185+
# The "y" parameter is upwards (per FreeType).
186+
x + dx * c - dy * s, self.height - y + dx * s + dy * c, angle,
187+
get_hinting_flag())
188+
# draw_text_image's y is downwards & the bitmap bottom side.
189+
self._renderer.draw_text_image(
190+
bitmap["buffer"],
191+
bitmap["left"],
192+
int(self.height) - bitmap["top"] + bitmap["buffer"].shape[0],
193+
0, gc)
194+
if not angle:
195+
for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side.
196+
self._renderer.draw_text_image(
197+
np.full((round(h), round(w)), np.uint8(0xff)),
198+
round(x + dx), round(y - dy - h),
199+
0, gc)
200+
else:
201+
rgba = gc.get_rgb()
202+
if len(rgba) == 3 or gc.get_forced_alpha():
203+
rgba = rgba[:3] + (gc.get_alpha(),)
204+
gc1 = self.new_gc()
205+
gc1.set_linewidth(0)
206+
for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side.
207+
path = Path._create_closed(
208+
[(dx, dy), (dx + w, dy), (dx + w, dy + h), (dx, dy + h)])
209+
self._renderer.draw_path(
210+
gc1, path,
211+
mpl.transforms.Affine2D()
212+
.rotate_deg(angle).translate(x, self.height - y),
213+
rgba)
214+
gc1.restore()
185215

186216
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
187217
# docstring inherited
188218
if ismath:
189219
return self.draw_mathtext(gc, x, y, s, prop, angle)
190220
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).
193221
font.set_text(s, 0, flags=get_hinting_flag(),
194222
features=mtext.get_fontfeatures() if mtext is not None else None,
195223
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)
224+
for bitmap in font._render_glyphs(x, self.height - y):
225+
self._renderer.draw_text_image(
226+
bitmap["buffer"],
227+
bitmap["left"],
228+
int(self.height) - bitmap["top"] + bitmap["buffer"].shape[0],
229+
0, gc)
218230

219231
def get_text_width_height_descent(self, s, prop, ismath):
220232
# docstring inherited
@@ -224,9 +236,8 @@ def get_text_width_height_descent(self, s, prop, ismath):
224236
return super().get_text_width_height_descent(s, prop, ismath)
225237

226238
if ismath:
227-
ox, oy, width, height, descent, font_image = \
228-
self.mathtext_parser.parse(s, self.dpi, prop)
229-
return width, height, descent
239+
parse = self.mathtext_parser.parse(s, self.dpi, prop)
240+
return parse.width, parse.height, parse.depth
230241

231242
font = self._prepare_font(prop)
232243
font.set_text(s, 0.0, flags=get_hinting_flag())

src/ft2font.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ class FT2Font
184184
py::array_t<uint8_t, py::array::c_style> image;
185185
FT_Face face;
186186
FT_Vector pen; /* untransformed origin */
187+
public:
187188
std::vector<FT_Glyph> glyphs;
189+
private:
188190
std::vector<FT2Font *> fallbacks;
189191
std::unordered_map<long, FT2Font *> char_to_font;
190192
FT_BBox bbox;

src/ft2font_wrapper.cpp

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1834,7 +1834,53 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
18341834

18351835
.def_buffer([](PyFT2Font &self) -> py::buffer_info {
18361836
return self.get_image().request();
1837-
});
1837+
})
1838+
1839+
// TODO: Return a nicer structure than dicts.
1840+
// NOTE: The lifetime of the buffers is limited and could get invalidated...
1841+
// TODO: Real antialiasing flag.
1842+
// x, y are upwards here
1843+
.def("_render_glyph", [](PyFT2Font *self, FT_UInt idx,
1844+
double x, double y, double angle,
1845+
LoadFlags flags) {
1846+
auto face = self->get_face();
1847+
auto hf = self->get_hinting_factor();
1848+
auto c = std::cos(angle * M_PI / 180) * 0x10000L,
1849+
s = std::sin(angle * M_PI / 180) * 0x10000L;
1850+
auto matrix = FT_Matrix{
1851+
std::lround(c / hf), std::lround(-s),
1852+
std::lround(s / hf), std::lround(c)};
1853+
auto delta = FT_Vector{std::lround(x * 64), std::lround(y * 64)};
1854+
FT_Set_Transform(face, &matrix, &delta);
1855+
FT_CHECK(FT_Load_Glyph, face, idx, static_cast<FT_Int32>(flags));
1856+
FT_CHECK(FT_Render_Glyph, face->glyph, FT_RENDER_MODE_NORMAL);
1857+
py::dict d;
1858+
d["left"] = face->glyph->bitmap_left;
1859+
d["top"] = face->glyph->bitmap_top;
1860+
d["buffer"] = py::array_t<uint8_t>{
1861+
{face->glyph->bitmap.rows, face->glyph->bitmap.width},
1862+
{face->glyph->bitmap.pitch, 1},
1863+
face->glyph->bitmap.buffer};
1864+
return d;
1865+
})
1866+
.def("_render_glyphs", [](PyFT2Font *self, double x, double y) {
1867+
auto origin = FT_Vector{std::lround(x * 64), std::lround(y * 64)};
1868+
py::list gs;
1869+
for (auto &g: self->glyphs) {
1870+
FT_CHECK(FT_Glyph_To_Bitmap, &g, FT_RENDER_MODE_NORMAL, &origin, 1);
1871+
auto bg = reinterpret_cast<FT_BitmapGlyph>(g);
1872+
py::dict d;
1873+
d["left"] = bg->left;
1874+
d["top"] = bg->top;
1875+
d["buffer"] = py::array_t<uint8_t>{
1876+
{bg->bitmap.rows, bg->bitmap.width},
1877+
{bg->bitmap.pitch, 1},
1878+
bg->bitmap.buffer};
1879+
gs.append(d);
1880+
}
1881+
return gs;
1882+
})
1883+
;
18381884

18391885
m.attr("__freetype_version__") = version_string;
18401886
m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE;

0 commit comments

Comments
 (0)