Skip to content

Commit 4df0c5b

Browse files
committed
Merge branch '30059/ft-direct-render' into libraqm-full
2 parents b2aa1f2 + b0a13fa commit 4df0c5b

File tree

6 files changed

+196
-43
lines changed

6 files changed

+196
-43
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 61 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,48 +173,68 @@ 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(
192+
glyph_index, get_hinting_flag(),
193+
RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO)
194+
buffer = np.asarray(bitmap.buffer)
195+
if not gc.get_antialiased():
196+
buffer *= 0xff
197+
# draw_text_image's y is downwards & the bitmap bottom side.
198+
self._renderer.draw_text_image(
199+
buffer,
200+
bitmap.left,
201+
int(self.height) - bitmap.top + bitmap.buffer.shape[0],
202+
0, gc)
203+
rgba = gc.get_rgb()
204+
if len(rgba) == 3 or gc.get_forced_alpha():
205+
rgba = rgba[:3] + (gc.get_alpha(),)
206+
gc1 = self.new_gc()
207+
gc1.set_linewidth(0)
208+
gc1.set_snap(gc.get_snap())
209+
for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side.
210+
path = Path._create_closed(
211+
[(dx, dy), (dx + w, dy), (dx + w, dy + h), (dx, dy + h)])
212+
self._renderer.draw_path(
213+
gc1, path,
214+
mpl.transforms.Affine2D()
215+
.rotate_deg(angle).translate(x, self.height - y),
216+
rgba)
217+
gc1.restore()
185218

186219
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
187220
# docstring inherited
188221
if ismath:
189222
return self.draw_mathtext(gc, x, y, s, prop, angle)
190223
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(),
224+
font.set_text(s, angle, flags=get_hinting_flag(),
194225
features=mtext.get_fontfeatures() if mtext is not None else None,
195226
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)
227+
for bitmap in font._render_glyphs(
228+
x, self.height - y,
229+
RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO,
230+
):
231+
buffer = bitmap.buffer
232+
if not gc.get_antialiased():
233+
buffer *= 0xff
234+
self._renderer.draw_text_image(
235+
buffer,
236+
bitmap.left, int(self.height) - bitmap.top + buffer.shape[0],
237+
0, gc)
218238

219239
def get_text_width_height_descent(self, s, prop, ismath):
220240
# docstring inherited
@@ -224,9 +244,8 @@ def get_text_width_height_descent(self, s, prop, ismath):
224244
return super().get_text_width_height_descent(s, prop, ismath)
225245

226246
if ismath:
227-
ox, oy, width, height, descent, font_image = \
228-
self.mathtext_parser.parse(s, self.dpi, prop)
229-
return width, height, descent
247+
parse = self.mathtext_parser.parse(s, self.dpi, prop)
248+
return parse.width, parse.height, parse.depth
230249

231250
font = self._prepare_font(prop)
232251
font.set_text(s, 0.0, flags=get_hinting_flag())
@@ -248,8 +267,8 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
248267
Z = np.array(Z * 255.0, np.uint8)
249268

250269
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))
270+
xd = d * math.sin(math.radians(angle))
271+
yd = d * math.cos(math.radians(angle))
253272
x = round(x + xd)
254273
y = round(y + yd)
255274
self._renderer.draw_text_image(Z, x, y, angle, gc)

lib/matplotlib/ft2font.pyi

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ class LoadFlags(Flag):
7070
TARGET_LCD = cast(int, ...)
7171
TARGET_LCD_V = cast(int, ...)
7272

73+
class RenderMode(Enum):
74+
NORMAL = cast(int, ...)
75+
LIGHT = cast(int, ...)
76+
MONO = cast(int, ...)
77+
LCD = cast(int, ...)
78+
LCD_V = cast(int, ...)
79+
SDF = cast(int, ...)
80+
7381
class StyleFlags(Flag):
7482
NORMAL = cast(int, ...)
7583
ITALIC = cast(int, ...)

lib/matplotlib/text.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,7 @@ def draw(self, renderer):
871871
gc.set_alpha(self.get_alpha())
872872
gc.set_url(self._url)
873873
gc.set_antialiased(self._antialiased)
874+
gc.set_snap(self.get_snap())
874875
self._set_gc_clip(gc)
875876

876877
angle = self.get_rotation()

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
extern "C" {
2121
#include <ft2build.h>
22+
#include FT_BITMAP_H
2223
#include FT_FREETYPE_H
2324
#include FT_GLYPH_H
2425
#include FT_OUTLINE_H
@@ -111,6 +112,8 @@ class FT2Font
111112
void close();
112113
void clear();
113114
void set_size(double ptsize, double dpi);
115+
void _set_transform(
116+
std::array<std::array<FT_Fixed, 2>, 2> matrix, std::array<FT_Fixed, 2> delta);
114117
void set_charmap(int i);
115118
void select_charmap(unsigned long i);
116119
std::vector<raqm_glyph_t> layout(std::u32string_view text, FT_Int32 flags,
@@ -155,6 +158,10 @@ class FT2Font
155158
{
156159
return image;
157160
}
161+
std::vector<FT_Glyph> &get_glyphs()
162+
{
163+
return glyphs;
164+
}
158165
FT_Glyph const &get_last_glyph() const
159166
{
160167
return glyphs.back();

src/ft2font_wrapper.cpp

Lines changed: 108 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
@@ -265,6 +284,45 @@ PyFT2Image_draw_rect_filled(FT2Image *self,
265284
self->draw_rect_filled(x0, y0, x1, y1);
266285
}
267286

287+
/**********************************************************************
288+
* Positioned Bitmap; owns the FT_Bitmap!
289+
* */
290+
291+
struct PyPositionedBitmap {
292+
FT_Int left, top;
293+
bool owning;
294+
FT_Bitmap bitmap;
295+
296+
PyPositionedBitmap(FT_GlyphSlot slot) :
297+
left{slot->bitmap_left}, top{slot->bitmap_top}, owning{true}
298+
{
299+
FT_Bitmap_Init(&bitmap);
300+
FT_CHECK(FT_Bitmap_Convert, _ft2Library, &slot->bitmap, &bitmap, 1);
301+
}
302+
303+
PyPositionedBitmap(FT_BitmapGlyph bg) :
304+
left{bg->left}, top{bg->top}, owning{true}
305+
{
306+
FT_Bitmap_Init(&bitmap);
307+
FT_CHECK(FT_Bitmap_Convert, _ft2Library, &bg->bitmap, &bitmap, 1);
308+
}
309+
310+
PyPositionedBitmap(PyPositionedBitmap& other) = delete; // Non-copyable.
311+
312+
PyPositionedBitmap(PyPositionedBitmap&& other) :
313+
left{other.left}, top{other.top}, owning{true}, bitmap{other.bitmap}
314+
{
315+
other.owning = false; // Prevent double deletion.
316+
}
317+
318+
~PyPositionedBitmap()
319+
{
320+
if (owning) {
321+
FT_Bitmap_Done(_ft2Library, &bitmap);
322+
}
323+
}
324+
};
325+
268326
/**********************************************************************
269327
* Glyph
270328
* */
@@ -545,6 +603,19 @@ const char *PyFT2Font_set_size__doc__ = R"""(
545603
The DPI used for rendering the text.
546604
)""";
547605

606+
const char *PyFT2Font__set_transform__doc__ = R"""(
607+
Set the transform of the text.
608+
609+
This is a low-level function, where *matrix* and *delta* are directly in
610+
16.16 and 26.6 formats respectively. Refer to the FreeType docs of
611+
FT_Set_Transform for further description.
612+
613+
Parameters
614+
----------
615+
matrix : (2, 2) array of int
616+
delta : (2,) array of int
617+
)""";
618+
548619
const char *PyFT2Font_set_charmap__doc__ = R"""(
549620
Make the i-th charmap current.
550621
@@ -1565,6 +1636,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
15651636
p11x::bind_enums(m);
15661637
p11x::enums["Kerning"].attr("__doc__") = Kerning__doc__;
15671638
p11x::enums["LoadFlags"].attr("__doc__") = LoadFlags__doc__;
1639+
p11x::enums["RenderMode"].attr("__doc__") = RenderMode__doc__;
15681640
p11x::enums["FaceFlags"].attr("__doc__") = FaceFlags__doc__;
15691641
p11x::enums["StyleFlags"].attr("__doc__") = StyleFlags__doc__;
15701642

@@ -1591,6 +1663,17 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
15911663
return py::buffer_info(self.get_buffer(), shape, strides);
15921664
});
15931665

1666+
py::class_<PyPositionedBitmap>(m, "_PositionedBitmap", py::is_final())
1667+
.def_readonly("left", &PyPositionedBitmap::left)
1668+
.def_readonly("top", &PyPositionedBitmap::top)
1669+
.def_property_readonly(
1670+
"buffer", [](PyPositionedBitmap &self) -> py::array {
1671+
return {{self.bitmap.rows, self.bitmap.width},
1672+
{self.bitmap.pitch, 1},
1673+
self.bitmap.buffer};
1674+
})
1675+
;
1676+
15941677
py::class_<PyGlyph>(m, "Glyph", py::is_final(), PyGlyph__doc__)
15951678
.def(py::init<>([]() -> PyGlyph {
15961679
// Glyph is not useful from Python, so mark it as not constructible.
@@ -1651,6 +1734,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16511734
.def("clear", &PyFT2Font::clear, PyFT2Font_clear__doc__)
16521735
.def("set_size", &PyFT2Font::set_size, "ptsize"_a, "dpi"_a,
16531736
PyFT2Font_set_size__doc__)
1737+
.def("_set_transform", &PyFT2Font::_set_transform, "matrix"_a, "delta"_a,
1738+
PyFT2Font__set_transform__doc__)
16541739
.def("set_charmap", &PyFT2Font::set_charmap, "i"_a,
16551740
PyFT2Font_set_charmap__doc__)
16561741
.def("select_charmap", &PyFT2Font::select_charmap, "i"_a,
@@ -1813,10 +1898,32 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
18131898
.def_property_readonly(
18141899
"fname", &PyFT2Font_fname,
18151900
"The original filename for this object.")
1901+
.def_property_readonly(
1902+
"_hinting_factor", &PyFT2Font::get_hinting_factor,
1903+
"The hinting factor.")
18161904

18171905
.def_buffer([](PyFT2Font &self) -> py::buffer_info {
18181906
return self.get_image().request();
1819-
});
1907+
})
1908+
1909+
.def("_render_glyph",
1910+
[](PyFT2Font *self, FT_UInt idx, LoadFlags flags, FT_Render_Mode render_mode) {
1911+
auto face = self->get_face();
1912+
FT_CHECK(FT_Load_Glyph, face, idx, static_cast<FT_Int32>(flags));
1913+
FT_CHECK(FT_Render_Glyph, face->glyph, render_mode);
1914+
return PyPositionedBitmap{face->glyph};
1915+
})
1916+
.def("_render_glyphs",
1917+
[](PyFT2Font *self, double x, double y, FT_Render_Mode render_mode) {
1918+
auto origin = FT_Vector{std::lround(x * 64), std::lround(y * 64)};
1919+
auto pbs = std::vector<PyPositionedBitmap>{};
1920+
for (auto &g: self->get_glyphs()) {
1921+
FT_CHECK(FT_Glyph_To_Bitmap, &g, render_mode, &origin, 1);
1922+
pbs.emplace_back(reinterpret_cast<FT_BitmapGlyph>(g));
1923+
}
1924+
return pbs;
1925+
})
1926+
;
18201927

18211928
m.attr("__freetype_version__") = version_string;
18221929
m.attr("__freetype_build_type__") = FREETYPE_BUILD_TYPE;

0 commit comments

Comments
 (0)