Skip to content

Commit f13fe82

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 f13fe82

File tree

7 files changed

+203
-44
lines changed

7 files changed

+203
-44
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 60 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,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, 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+
path = Path._create_closed(
210+
[(dx, dy), (dx + w, dy), (dx + w, dy + h), (dx, dy + h)])
211+
self._renderer.draw_path(
212+
gc1, path,
213+
mpl.transforms.Affine2D()
214+
.rotate_deg(angle).translate(x, self.height - y),
215+
rgba)
216+
gc1.restore()
185217

186218
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
187219
# docstring inherited
188220
if ismath:
189221
return self.draw_mathtext(gc, x, y, s, prop, angle)
190222
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(),
223+
font.set_text(s, angle, flags=get_hinting_flag(),
194224
features=mtext.get_fontfeatures() if mtext is not None else None,
195225
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)
226+
for bitmap in font._render_glyphs(
227+
x, self.height - y,
228+
RenderMode.NORMAL if gc.get_antialiased() else RenderMode.MONO,
229+
):
230+
buffer = bitmap.buffer
231+
if not gc.get_antialiased():
232+
buffer *= 0xff
233+
self._renderer.draw_text_image(
234+
buffer,
235+
bitmap.left, int(self.height) - bitmap.top + buffer.shape[0],
236+
0, gc)
218237

219238
def get_text_width_height_descent(self, s, prop, ismath):
220239
# docstring inherited
@@ -224,9 +243,8 @@ def get_text_width_height_descent(self, s, prop, ismath):
224243
return super().get_text_width_height_descent(s, prop, ismath)
225244

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

231249
font = self._prepare_font(prop)
232250
font.set_text(s, 0.0, flags=get_hinting_flag())
@@ -248,8 +266,8 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
248266
Z = np.array(Z * 255.0, np.uint8)
249267

250268
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))
269+
xd = d * math.sin(math.radians(angle))
270+
yd = d * math.cos(math.radians(angle))
253271
x = round(x + xd)
254272
y = round(y + yd)
255273
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/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)