🌐 AI搜索 & 代理 主页
Skip to content

Commit c45e5f1

Browse files
committed
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 04c8eef commit c45e5f1

File tree

4 files changed

+154
-43
lines changed

4 files changed

+154
-43
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 62 additions & 42 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
@@ -32,7 +32,7 @@
3232
from matplotlib.backend_bases import (
3333
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
3434
from matplotlib.font_manager import fontManager as _fontManager, get_font
35-
from matplotlib.ft2font import LoadFlags
35+
from matplotlib.ft2font import LoadFlags, RenderMode
3636
from matplotlib.mathtext import MathTextParser
3737
from matplotlib.path import Path
3838
from matplotlib.transforms import Bbox, BboxBase
@@ -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,46 +173,67 @@ 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, 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(
192+
font.get_char_index(char), get_hinting_flag(),
193+
RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO)
194+
# draw_text_image's y is downwards & the bitmap bottom side.
195+
self._renderer.draw_text_image(
196+
bitmap["buffer"],
197+
bitmap["left"],
198+
int(self.height) - bitmap["top"] + bitmap["buffer"].shape[0],
199+
0, gc)
200+
if not angle:
201+
for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side.
202+
self._renderer.draw_text_image(
203+
np.full((round(h), round(w)), np.uint8(0xff)),
204+
round(x + dx), round(y - dy - h),
205+
0, gc)
206+
else:
207+
rgba = gc.get_rgb()
208+
if len(rgba) == 3 or gc.get_forced_alpha():
209+
rgba = rgba[:3] + (gc.get_alpha(),)
210+
gc1 = self.new_gc()
211+
gc1.set_linewidth(0)
212+
for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side.
213+
path = Path._create_closed(
214+
[(dx, dy), (dx + w, dy), (dx + w, dy + h), (dx, dy + h)])
215+
self._renderer.draw_path(
216+
gc1, path,
217+
mpl.transforms.Affine2D()
218+
.rotate_deg(angle).translate(x, self.height - y),
219+
rgba)
220+
gc1.restore()
185221

186222
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
187223
# docstring inherited
188224
if ismath:
189225
return self.draw_mathtext(gc, x, y, s, prop, angle)
190226
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())
194-
font.draw_glyphs_to_bitmap(
195-
antialiased=gc.get_antialiased())
196-
d = font.get_descent() / 64.0
197-
# The descent needs to be adjusted for the angle.
198-
xo, yo = font.get_bitmap_offset()
199-
xo /= 64.0
200-
yo /= 64.0
201-
202-
rad = radians(angle)
203-
xd = d * sin(rad)
204-
yd = d * cos(rad)
205-
# Rotating the offset vector ensures text rotates around the anchor point.
206-
# Without this, rotated text offsets incorrectly, causing a horizontal shift.
207-
# Applying the 2D rotation matrix.
208-
rotated_xo = xo * cos(rad) - yo * sin(rad)
209-
rotated_yo = xo * sin(rad) + yo * cos(rad)
210-
# Subtract rotated_yo to account for the inverted y-axis in computer graphics,
211-
# compared to the mathematical convention.
212-
x = round(x + rotated_xo + xd)
213-
y = round(y - rotated_yo + yd)
214-
215-
self._renderer.draw_text_image(font, x, y + 1, angle, gc)
227+
font.set_text(s, angle, flags=get_hinting_flag())
228+
for bitmap in font._render_glyphs(
229+
x, self.height - y,
230+
RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO,
231+
):
232+
self._renderer.draw_text_image(
233+
bitmap["buffer"],
234+
bitmap["left"],
235+
int(self.height) - bitmap["top"] + bitmap["buffer"].shape[0],
236+
0, gc)
216237

217238
def get_text_width_height_descent(self, s, prop, ismath):
218239
# docstring inherited
@@ -222,9 +243,8 @@ def get_text_width_height_descent(self, s, prop, ismath):
222243
return super().get_text_width_height_descent(s, prop, ismath)
223244

224245
if ismath:
225-
ox, oy, width, height, descent, font_image = \
226-
self.mathtext_parser.parse(s, self.dpi, prop)
227-
return width, height, descent
246+
parse = self.mathtext_parser.parse(s, self.dpi, prop)
247+
return parse.width, parse.height, parse.depth
228248

229249
font = self._prepare_font(prop)
230250
font.set_text(s, 0.0, flags=get_hinting_flag())
@@ -246,8 +266,8 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
246266
Z = np.array(Z * 255.0, np.uint8)
247267

248268
w, h, d = self.get_text_width_height_descent(s, prop, ismath="TeX")
249-
xd = d * sin(radians(angle))
250-
yd = d * cos(radians(angle))
269+
xd = d * math.sin(math.radians(angle))
270+
yd = d * math.cos(math.radians(angle))
251271
x = round(x + xd)
252272
y = round(y + yd)
253273
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
@@ -107,6 +107,8 @@ class FT2Font
107107
void close();
108108
void clear();
109109
void set_size(double ptsize, double dpi);
110+
void _set_transform(
111+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta);
110112
void set_charmap(int i);
111113
void select_charmap(unsigned long i);
112114
void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
@@ -175,7 +177,9 @@ class FT2Font
175177
py::array_t<uint8_t, py::array::c_style> image;
176178
FT_Face face;
177179
FT_Vector pen; /* untransformed origin */
180+
public:
178181
std::vector<FT_Glyph> glyphs;
182+
private:
179183
std::vector<FT2Font *> fallbacks;
180184
std::unordered_map<long, FT2Font *> char_to_font;
181185
FT_BBox bbox;

src/ft2font_wrapper.cpp

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,25 @@ P11X_DECLARE_ENUM(
204204
{"TARGET_LCD_V", LoadFlags::TARGET_LCD_V},
205205
);
206206

207+
const char *RenderMode__doc__ = R"""(
208+
Render modes.
209+
210+
For more information, see `the FreeType documentation
211+
<https://freetype.org/freetype2/docs/reference/ft2-glyph_retrieval.html#ft_render_mode>`_.
212+
213+
.. versionadded:: 3.10
214+
)""";
215+
216+
P11X_DECLARE_ENUM(
217+
"RenderMode", "Enum",
218+
{"NORMAL", FT_RENDER_MODE_NORMAL},
219+
{"LIGHT", FT_RENDER_MODE_LIGHT},
220+
{"MONO", FT_RENDER_MODE_MONO},
221+
{"LCD", FT_RENDER_MODE_LCD},
222+
{"LCD_V", FT_RENDER_MODE_LCD_V},
223+
{"SDF", FT_RENDER_MODE_SDF},
224+
);
225+
207226
const char *StyleFlags__doc__ = R"""(
208227
Flags returned by `FT2Font.style_flags`.
209228
@@ -535,6 +554,19 @@ const char *PyFT2Font_set_size__doc__ = R"""(
535554
The DPI used for rendering the text.
536555
)""";
537556

557+
const char *PyFT2Font__set_transform__doc__ = R"""(
558+
Set the transform of the text.
559+
560+
This is a low-level function, where *matrix* and *delta* are directly in
561+
16.16 and 26.6 formats respectively. Refer to the FreeType docs of
562+
FT_Set_Transform for further description.
563+
564+
Parameters
565+
----------
566+
matrix : (2, 2) array of int
567+
delta : (2,) array of int
568+
)""";
569+
538570
const char *PyFT2Font_set_charmap__doc__ = R"""(
539571
Make the i-th charmap current.
540572
@@ -1467,6 +1499,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
14671499
p11x::bind_enums(m);
14681500
p11x::enums["Kerning"].attr("__doc__") = Kerning__doc__;
14691501
p11x::enums["LoadFlags"].attr("__doc__") = LoadFlags__doc__;
1502+
p11x::enums["RenderMode"].attr("__doc__") = RenderMode__doc__;
14701503
p11x::enums["FaceFlags"].attr("__doc__") = FaceFlags__doc__;
14711504
p11x::enums["StyleFlags"].attr("__doc__") = StyleFlags__doc__;
14721505

@@ -1527,6 +1560,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
15271560
.def("clear", &PyFT2Font::clear, PyFT2Font_clear__doc__)
15281561
.def("set_size", &PyFT2Font::set_size, "ptsize"_a, "dpi"_a,
15291562
PyFT2Font_set_size__doc__)
1563+
.def("_set_transform", &PyFT2Font::_set_transform, "matrix"_a, "delta"_a,
1564+
PyFT2Font__set_transform__doc__)
15301565
.def("set_charmap", &PyFT2Font::set_charmap, "i"_a,
15311566
PyFT2Font_set_charmap__doc__)
15321567
.def("select_charmap", &PyFT2Font::select_charmap, "i"_a,
@@ -1683,10 +1718,51 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16831718
.def_property_readonly(
16841719
"fname", &PyFT2Font_fname,
16851720
"The original filename for this object.")
1721+
.def_property_readonly(
1722+
"_hinting_factor", [](PyFT2Font *self) {
1723+
return self->get_hinting_factor();
1724+
}, "The hinting factor.")
16861725

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

16911767
m.attr("__freetype_version__") = version_string;
16921768
m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE;

0 commit comments

Comments
 (0)