Skip to content

Commit d0df260

Browse files
authored
Merge pull request #30059 from anntzer/ft-direct-render
Drop the FT2Font intermediate buffer.
2 parents 5e56525 + 9d7d7b4 commit d0df260

File tree

8 files changed

+212
-45
lines changed

8 files changed

+212
-45
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 68 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,75 @@ 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, int(self.height) - bitmap.top + buffer.shape[0],
201+
0, gc)
202+
rgba = gc.get_rgb()
203+
if len(rgba) == 3 or gc.get_forced_alpha():
204+
rgba = rgba[:3] + (gc.get_alpha(),)
205+
gc1 = self.new_gc()
206+
gc1.set_linewidth(0)
207+
gc1.set_snap(gc.get_snap())
208+
for dx, dy, w, h in parse.rects: # dy is upwards & the rect top side.
209+
if gc1.get_snap() in [None, True]:
210+
# Prevent thin bars from disappearing by growing symmetrically.
211+
if w < 1:
212+
dx -= (1 - w) / 2
213+
w = 1
214+
if h < 1:
215+
dy -= (1 - h) / 2
216+
h = 1
217+
path = Path._create_closed(
218+
[(dx, dy), (dx + w, dy), (dx + w, dy + h), (dx, dy + h)])
219+
self._renderer.draw_path(
220+
gc1, path,
221+
mpl.transforms.Affine2D()
222+
.rotate_deg(angle).translate(x, self.height - y),
223+
rgba)
224+
gc1.restore()
185225

186226
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
187227
# docstring inherited
188228
if ismath:
189229
return self.draw_mathtext(gc, x, y, s, prop, angle)
190230
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(),
231+
font.set_text(s, angle, flags=get_hinting_flag(),
194232
features=mtext.get_fontfeatures() if mtext is not None else None,
195233
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)
234+
for bitmap in font._render_glyphs(
235+
x, self.height - y,
236+
RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO,
237+
):
238+
buffer = bitmap.buffer
239+
if not gc.get_antialiased():
240+
buffer *= 0xff
241+
self._renderer.draw_text_image(
242+
buffer,
243+
bitmap.left, int(self.height) - bitmap.top + buffer.shape[0],
244+
0, gc)
218245

219246
def get_text_width_height_descent(self, s, prop, ismath):
220247
# docstring inherited
@@ -224,9 +251,8 @@ def get_text_width_height_descent(self, s, prop, ismath):
224251
return super().get_text_width_height_descent(s, prop, ismath)
225252

226253
if ismath:
227-
ox, oy, width, height, descent, font_image = \
228-
self.mathtext_parser.parse(s, self.dpi, prop)
229-
return width, height, descent
254+
parse = self.mathtext_parser.parse(s, self.dpi, prop)
255+
return parse.width, parse.height, parse.depth
230256

231257
font = self._prepare_font(prop)
232258
font.set_text(s, 0.0, flags=get_hinting_flag())
@@ -248,8 +274,8 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
248274
Z = np.array(Z * 255.0, np.uint8)
249275

250276
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))
277+
xd = d * math.sin(math.radians(angle))
278+
yd = d * math.cos(math.radians(angle))
253279
x = round(x + xd)
254280
y = round(y + yd)
255281
self._renderer.draw_text_image(Z, x, y, angle, gc)

lib/matplotlib/font_manager.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1712,7 +1712,7 @@ def get_font(font_filepaths, hinting_factor=None):
17121712

17131713
hinting_factor = mpl._val_or_rc(hinting_factor, 'text.hinting_factor')
17141714

1715-
return _get_font(
1715+
font = _get_font(
17161716
# must be a tuple to be cached
17171717
paths,
17181718
hinting_factor,
@@ -1721,6 +1721,10 @@ def get_font(font_filepaths, hinting_factor=None):
17211721
thread_id=threading.get_ident(),
17221722
enable_last_resort=mpl.rcParams['font.enable_last_resort'],
17231723
)
1724+
# Ensure the transform is always consistent.
1725+
font._set_transform([[round(0x10000 / font._hinting_factor), 0], [0, 0x10000]],
1726+
[0, 0])
1727+
return font
17241728

17251729

17261730
def _load_fontmanager(*, try_read_cache=True):

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/tests/test_axes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6446,7 +6446,7 @@ def test_pie_linewidth_0():
64466446
plt.axis('equal')
64476447

64486448

6449-
@image_comparison(['pie_center_radius.png'], style='mpl20', tol=0.01)
6449+
@image_comparison(['pie_center_radius.png'], style='mpl20', tol=0.011)
64506450
def test_pie_center_radius():
64516451
# The slices will be ordered and plotted counter-clockwise.
64526452
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'

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();

0 commit comments

Comments
 (0)