Skip to content

Commit 4c45ae2

Browse files
committed
Use libraqm for text in vector outputs
1 parent e771cc4 commit 4c45ae2

File tree

6 files changed

+143
-45
lines changed

6 files changed

+143
-45
lines changed

lib/matplotlib/_text_helpers.py

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,20 @@
44

55
from __future__ import annotations
66

7-
import dataclasses
7+
from collections.abc import Iterator
88

99
from . import _api
10-
from .ft2font import FT2Font, Kerning, LoadFlags
10+
from .ft2font import FT2Font, LayoutItem, LoadFlags
1111

1212

13-
@dataclasses.dataclass(frozen=True)
14-
class LayoutItem:
15-
ft_object: FT2Font
16-
char: str
17-
glyph_idx: int
18-
x: float
19-
prev_kern: float
20-
21-
22-
def warn_on_missing_glyph(codepoint, fontnames):
13+
def warn_on_missing_glyph(codepoint: int, fontnames: str):
2314
_api.warn_external(
2415
f"Glyph {codepoint} "
2516
f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) "
2617
f"missing from font(s) {fontnames}.")
2718

2819

29-
def layout(string, font, *, kern_mode=Kerning.DEFAULT):
20+
def layout(string: str, font: FT2Font) -> Iterator[LayoutItem]:
3021
"""
3122
Render *string* with *font*.
3223
@@ -39,27 +30,11 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT):
3930
The string to be rendered.
4031
font : FT2Font
4132
The font.
42-
kern_mode : Kerning
43-
A FreeType kerning mode.
4433
4534
Yields
4635
------
4736
LayoutItem
4837
"""
49-
x = 0
50-
prev_glyph_idx = None
51-
char_to_font = font._get_fontmap(string)
52-
base_font = font
53-
for char in string:
54-
# This has done the fallback logic
55-
font = char_to_font.get(char, base_font)
56-
glyph_idx = font.get_char_index(ord(char))
57-
kern = (
58-
base_font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
59-
if prev_glyph_idx is not None else 0.
60-
)
61-
x += kern
62-
glyph = font.load_glyph(glyph_idx, flags=LoadFlags.NO_HINTING)
63-
yield LayoutItem(font, char, glyph_idx, x, kern)
64-
x += glyph.linearHoriAdvance / 65536
65-
prev_glyph_idx = glyph_idx
38+
for raqm_item in font._layout(string, LoadFlags.NO_HINTING):
39+
raqm_item.ft_object.load_glyph(raqm_item.glyph_idx, flags=LoadFlags.NO_HINTING)
40+
yield raqm_item

lib/matplotlib/backends/backend_pdf.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from matplotlib.figure import Figure
3636
from matplotlib.font_manager import get_font, fontManager as _fontManager
3737
from matplotlib._afm import AFM
38-
from matplotlib.ft2font import FT2Font, FaceFlags, Kerning, LoadFlags, StyleFlags
38+
from matplotlib.ft2font import FT2Font, FaceFlags, LoadFlags, StyleFlags
3939
from matplotlib.transforms import Affine2D, BboxBase
4040
from matplotlib.path import Path
4141
from matplotlib.dates import UTC
@@ -2351,7 +2351,6 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23512351
fonttype = 1
23522352
else:
23532353
font = self._get_font_ttf(prop)
2354-
self.file._character_tracker.track(font, s)
23552354
fonttype = mpl.rcParams['pdf.fonttype']
23562355

23572356
if gc.get_url() is not None:
@@ -2363,6 +2362,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23632362
# If fonttype is neither 3 nor 42, emit the whole string at once
23642363
# without manual kerning.
23652364
if fonttype not in [3, 42]:
2365+
self.file._character_tracker.track(font, s)
23662366
self.file.output(Op.begin_text,
23672367
self.file.fontName(prop), fontsize, Op.selectfont)
23682368
self._setup_textpos(x, y, angle)
@@ -2389,7 +2389,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23892389
multibyte_glyphs = []
23902390
prev_was_multibyte = True
23912391
prev_font = font
2392-
for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED):
2392+
for item in _text_helpers.layout(s, font):
2393+
self.file._character_tracker.track_glyph(font, item.glyph_idx)
23932394
if _font_supports_glyph(fonttype, ord(item.char)):
23942395
if prev_was_multibyte or item.ft_object != prev_font:
23952396
singlebyte_chunks.append((item.ft_object, item.x, []))

lib/matplotlib/backends/backend_ps.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -795,19 +795,18 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
795795

796796
else:
797797
font = self._get_font_ttf(prop)
798-
self._character_tracker.track(font, s)
799798
for item in _text_helpers.layout(s, font):
799+
self._character_tracker.track_glyph(item.ft_object, item.glyph_idx)
800800
ps_name = (item.ft_object.postscript_name
801801
.encode("ascii", "replace").decode("ascii"))
802802
glyph_name = item.ft_object.get_glyph_name(item.glyph_idx)
803-
stream.append((ps_name, item.x, glyph_name))
803+
stream.append((ps_name, item.x, item.y, glyph_name))
804804
self.set_color(*gc.get_rgb())
805805

806-
for ps_name, group in itertools. \
807-
groupby(stream, lambda entry: entry[0]):
806+
for ps_name, group in itertools.groupby(stream, lambda entry: entry[0]):
808807
self.set_font(ps_name, prop.get_size_in_points(), False)
809-
thetext = "\n".join(f"{x:g} 0 m /{name:s} glyphshow"
810-
for _, x, name in group)
808+
thetext = "\n".join(f"{x:g} {y:g} m /{name:s} glyphshow"
809+
for _, x, y, name in group)
811810
self._pswriter.write(f"""\
812811
gsave
813812
{self._get_clip_cmd(gc)}

lib/matplotlib/textpath.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,16 @@ def get_glyphs_with_font(self, font, s, glyph_map=None,
144144
glyph_map_new = glyph_map
145145

146146
xpositions = []
147+
ypositions = []
147148
glyph_ids = []
148149
for item in _text_helpers.layout(s, font):
149-
char_id = self._get_char_id(item.ft_object, ord(item.char))
150+
char_id = self._get_char_id(item.ft_object, item.glyph_idx)
150151
glyph_ids.append(char_id)
151152
xpositions.append(item.x)
153+
ypositions.append(item.y)
152154
if char_id not in glyph_map:
153155
glyph_map_new[char_id] = item.ft_object.get_path()
154156

155-
ypositions = [0] * len(xpositions)
156157
sizes = [1.] * len(xpositions)
157158

158159
rects = []

src/ft2font.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ class FT2Font
171171
return FT_HAS_KERNING(face);
172172
}
173173

174+
void set_parent(void *parent)
175+
{
176+
_parent = parent;
177+
}
178+
179+
void *get_parent() const
180+
{
181+
return _parent;
182+
}
183+
174184
private:
175185
WarnFunc ft_glyph_warn;
176186
bool warn_if_used;
@@ -185,6 +195,9 @@ class FT2Font
185195
long hinting_factor;
186196
int kerning_factor;
187197

198+
// Holds the parent PyFT2Font object.
199+
void *_parent;
200+
188201
// prevent copying
189202
FT2Font(const FT2Font &);
190203
FT2Font &operator=(const FT2Font &);

src/ft2font_wrapper.cpp

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ PyFT2Font_init(py::object filename, long hinting_factor = 8,
499499

500500
self->x = new FT2Font(open_args, hinting_factor, fallback_fonts, ft_glyph_warn,
501501
warn_if_used);
502+
self->x->set_parent(self);
502503

503504
self->x->set_kerning_factor(kerning_factor);
504505

@@ -1467,6 +1468,97 @@ PyFT2Font__get_type1_encoding_vector(PyFT2Font *self)
14671468
return indices;
14681469
}
14691470

1471+
/**********************************************************************
1472+
* Layout items
1473+
* */
1474+
1475+
struct LayoutItem {
1476+
PyFT2Font *ft_object;
1477+
std::u32string character;
1478+
int glyph_idx;
1479+
double x;
1480+
double y;
1481+
double prev_kern;
1482+
1483+
LayoutItem(PyFT2Font *f, std::u32string c, int i, double x, double y, double k) :
1484+
ft_object(f), character(c), glyph_idx(i), x(x), y(y), prev_kern(k) {}
1485+
1486+
std::string to_string()
1487+
{
1488+
std::ostringstream out;
1489+
out << "LayoutItem(ft_object=" << PyFT2Font_fname(ft_object);
1490+
out << ", char=" << character[0];
1491+
out << ", glyph_idx=" << glyph_idx;
1492+
out << ", x=" << x;
1493+
out << ", y=" << y;
1494+
out << ", prev_kern=" << prev_kern;
1495+
out << ")";
1496+
return out.str();
1497+
}
1498+
};
1499+
1500+
const char *PyFT2Font_layout__doc__ = R"""(
1501+
Layout a string and yield information about each used glyph.
1502+
1503+
.. warning::
1504+
This API uses the fallback list and is both private and provisional: do not use
1505+
it directly.
1506+
1507+
Parameters
1508+
----------
1509+
text : str
1510+
The characters for which to find fonts.
1511+
1512+
Returns
1513+
-------
1514+
list[LayoutItem]
1515+
)""";
1516+
1517+
static auto
1518+
PyFT2Font_layout(PyFT2Font *self, std::u32string text, LoadFlags flags)
1519+
{
1520+
const auto hinting_factor = self->x->get_hinting_factor();
1521+
const auto load_flags = static_cast<FT_Int32>(flags);
1522+
1523+
std::set<FT_String*> glyph_seen_fonts;
1524+
std::vector<raqm_glyph_t> glyphs;
1525+
self->x->layout(text, load_flags, glyph_seen_fonts, glyphs);
1526+
1527+
std::vector<LayoutItem> items;
1528+
1529+
double x = 0.0;
1530+
double y = 0.0;
1531+
std::optional<double> prev_advance = std::nullopt;
1532+
double prev_x = 0.0;
1533+
for (auto &glyph : glyphs) {
1534+
auto ft_object = static_cast<FT2Font *>(glyph.ftface->generic.data);
1535+
auto pyft_object = static_cast<PyFT2Font *>(ft_object->get_parent());
1536+
1537+
ft_object->load_glyph(glyph.index, load_flags);
1538+
1539+
double prev_kern = 0.0;
1540+
if (prev_advance.has_value()) {
1541+
double actual_advance = (x + glyph.x_offset) - prev_x;
1542+
prev_kern = actual_advance - prev_advance.value();
1543+
}
1544+
1545+
items.emplace_back(pyft_object, text.substr(glyph.cluster, 1), glyph.index,
1546+
(x + glyph.x_offset) / 64.0, (y + glyph.y_offset) / 64.0,
1547+
prev_kern / 64.0);
1548+
prev_x = x + glyph.x_offset;
1549+
x += glyph.x_advance;
1550+
y += glyph.y_advance;
1551+
// Note, linearHoriAdvance is a 16.16 instead of 26.6 fixed-point value.
1552+
prev_advance = ft_object->get_face()->glyph->linearHoriAdvance / 1024.0 / hinting_factor;
1553+
}
1554+
1555+
return items;
1556+
}
1557+
1558+
/**********************************************************************
1559+
* Deprecations
1560+
* */
1561+
14701562
static py::object
14711563
ft2font__getattr__(std::string name) {
14721564
auto api = py::module_::import("matplotlib._api");
@@ -1601,8 +1693,23 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16011693
.def_property_readonly("bbox", &PyGlyph_get_bbox,
16021694
"The control box of the glyph.");
16031695

1604-
auto cls = py::class_<PyFT2Font>(m, "FT2Font", py::is_final(), py::buffer_protocol(),
1605-
PyFT2Font__doc__)
1696+
py::class_<LayoutItem>(m, "LayoutItem", py::is_final())
1697+
.def_readonly("ft_object", &LayoutItem::ft_object,
1698+
"The FT_Face of the item.")
1699+
.def_readonly("char", &LayoutItem::character,
1700+
"The character code for the item.")
1701+
.def_readonly("glyph_idx", &LayoutItem::glyph_idx,
1702+
"The glyph index for the item.")
1703+
.def_readonly("x", &LayoutItem::x,
1704+
"The x position of the item.")
1705+
.def_readonly("y", &LayoutItem::y,
1706+
"The y position of the item.")
1707+
.def_readonly("prev_kern", &LayoutItem::prev_kern,
1708+
"The kerning between this item and the previous one.")
1709+
.def("__str__", &LayoutItem::to_string);
1710+
1711+
auto cls = py::class_<PyFT2Font>(m, "FT2Font", py::is_final(), py::buffer_protocol(),
1712+
PyFT2Font__doc__)
16061713
.def(py::init(&PyFT2Font_init),
16071714
"filename"_a, "hinting_factor"_a=8, py::kw_only(),
16081715
"_fallback_list"_a=py::none(), "_kerning_factor"_a=0,
@@ -1617,6 +1724,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
16171724
PyFT2Font_select_charmap__doc__)
16181725
.def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a,
16191726
PyFT2Font_get_kerning__doc__)
1727+
.def("_layout", &PyFT2Font_layout, "string"_a, "flags"_a,
1728+
PyFT2Font_layout__doc__)
16201729
.def("set_text", &PyFT2Font_set_text,
16211730
"string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT,
16221731
PyFT2Font_set_text__doc__)

0 commit comments

Comments
 (0)