Skip to content

Commit 9d7d7b4

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 5e56525 commit 9d7d7b4

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)