Skip to content

Commit 9a4e4c8

Browse files
committed
Use libraqm for text in vector outputs
1 parent 6a792e2 commit 9a4e4c8

File tree

5 files changed

+161
-49
lines changed

5 files changed

+161
-49
lines changed

lib/matplotlib/_text_helpers.py

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,21 @@
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, GlyphIndexType, Kerning, LoadFlags
10+
from .ft2font import FT2Font, CharacterCodeType, LayoutItem, LoadFlags
1111

1212

13-
@dataclasses.dataclass(frozen=True)
14-
class LayoutItem:
15-
ft_object: FT2Font
16-
char: str
17-
glyph_index: GlyphIndexType
18-
x: float
19-
prev_kern: float
20-
21-
22-
def warn_on_missing_glyph(codepoint, fontnames):
13+
def warn_on_missing_glyph(codepoint: CharacterCodeType, 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, *, features=None, kern_mode=Kerning.DEFAULT, language=None):
20+
def layout(string: str, font: FT2Font, *, features=None,
21+
language=None) -> Iterator[LayoutItem]:
3022
"""
3123
Render *string* with *font*.
3224
@@ -41,8 +33,6 @@ def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=N
4133
The font.
4234
features : tuple of str, optional
4335
The font features to apply to the text.
44-
kern_mode : Kerning
45-
A FreeType kerning mode.
4636
language : str, optional
4737
The language of the text in a format accepted by libraqm, namely `a BCP47
4838
language code <https://www.w3.org/International/articles/language-tags/>`_.
@@ -51,20 +41,8 @@ def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=N
5141
------
5242
LayoutItem
5343
"""
54-
x = 0
55-
prev_glyph_idx = None
56-
char_to_font = font._get_fontmap(string) # TODO: Pass in features and language.
57-
base_font = font
58-
for char in string:
59-
# This has done the fallback logic
60-
font = char_to_font.get(char, base_font)
61-
glyph_idx = font.get_char_index(ord(char))
62-
kern = (
63-
base_font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64
64-
if prev_glyph_idx is not None else 0.
65-
)
66-
x += kern
67-
glyph = font.load_glyph(glyph_idx, flags=LoadFlags.NO_HINTING)
68-
yield LayoutItem(font, char, glyph_idx, x, kern)
69-
x += glyph.linearHoriAdvance / 65536
70-
prev_glyph_idx = glyph_idx
44+
for raqm_item in font._layout(string, LoadFlags.NO_HINTING,
45+
features=features, language=language):
46+
raqm_item.ft_object.load_glyph(raqm_item.glyph_index,
47+
flags=LoadFlags.NO_HINTING)
48+
yield raqm_item

lib/matplotlib/backends/backend_pdf.py

Lines changed: 10 additions & 8 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
@@ -2355,7 +2355,6 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23552355
fonttype = 1
23562356
else:
23572357
font = self._get_font_ttf(prop)
2358-
self.file._character_tracker.track(font, s)
23592358
fonttype = mpl.rcParams['pdf.fonttype']
23602359

23612360
if gc.get_url() is not None:
@@ -2367,6 +2366,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23672366
# If fonttype is neither 3 nor 42, emit the whole string at once
23682367
# without manual kerning.
23692368
if fonttype not in [3, 42]:
2369+
self.file._character_tracker.track(font, s)
23702370
self.file.output(Op.begin_text,
23712371
self.file.fontName(prop), fontsize, Op.selectfont)
23722372
self._setup_textpos(x, y, angle)
@@ -2394,9 +2394,11 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23942394
prev_was_multibyte = True
23952395
prev_font = font
23962396
for item in _text_helpers.layout(s, font, features=features,
2397-
kern_mode=Kerning.UNFITTED,
23982397
language=language):
2399-
if _font_supports_glyph(fonttype, ord(item.char)):
2398+
self.file._character_tracker.track_glyph(item.ft_object,
2399+
item.glyph_index)
2400+
if (len(item.char) == 1 and
2401+
_font_supports_glyph(fonttype, ord(item.char))):
24002402
if prev_was_multibyte or item.ft_object != prev_font:
24012403
singlebyte_chunks.append((item.ft_object, item.x, []))
24022404
prev_font = item.ft_object
@@ -2431,15 +2433,15 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
24312433
prev_start_x = start_x
24322434
self.file.output(Op.end_text)
24332435
# Then emit all the multibyte characters, one at a time.
2434-
for ft_object, start_x, glyph_idx in multibyte_glyphs:
2436+
for ft_object, start_x, glyph_index in multibyte_glyphs:
24352437
self._draw_xobject_glyph(
2436-
ft_object, fontsize, glyph_idx, start_x, 0
2438+
ft_object, fontsize, glyph_index, start_x, 0
24372439
)
24382440
self.file.output(Op.grestore)
24392441

2440-
def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y):
2442+
def _draw_xobject_glyph(self, font, fontsize, glyph_index, x, y):
24412443
"""Draw a multibyte character from a Type 3 font as an XObject."""
2442-
glyph_name = font.get_glyph_name(glyph_idx)
2444+
glyph_name = font.get_glyph_name(glyph_index)
24432445
name = self.file._get_xobject_glyph_name(font.fname, glyph_name)
24442446
self.file.output(
24452447
Op.gsave,

lib/matplotlib/backends/backend_ps.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -797,20 +797,19 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
797797
else:
798798
features = language = None
799799
font = self._get_font_ttf(prop)
800-
self._character_tracker.track(font, s)
801800
for item in _text_helpers.layout(s, font, features=features,
802801
language=language):
802+
self._character_tracker.track_glyph(item.ft_object, item.glyph_index)
803803
ps_name = (item.ft_object.postscript_name
804804
.encode("ascii", "replace").decode("ascii"))
805805
glyph_name = item.ft_object.get_glyph_name(item.glyph_index)
806-
stream.append((ps_name, item.x, glyph_name))
806+
stream.append((ps_name, item.x, item.y, glyph_name))
807807
self.set_color(*gc.get_rgb())
808808

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

lib/matplotlib/textpath.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,16 @@ def get_glyphs_with_font(self, font, s, glyph_map=None,
147147
glyph_map_new = glyph_map
148148

149149
xpositions = []
150+
ypositions = []
150151
glyph_ids = []
151152
for item in _text_helpers.layout(s, font, features=features, language=language):
152153
glyph_id = self._get_glyph_id(item.ft_object, item.glyph_index)
153154
glyph_ids.append(glyph_id)
154155
xpositions.append(item.x)
156+
ypositions.append(item.y)
155157
if glyph_id not in glyph_map:
156158
glyph_map_new[glyph_id] = item.ft_object.get_path()
157159

158-
ypositions = [0] * len(xpositions)
159160
sizes = [1.] * len(xpositions)
160161

161162
rects = []

src/ft2font_wrapper.cpp

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,120 @@ PyFT2Font__get_type1_encoding_vector(PyFT2Font *self)
14061406
return indices;
14071407
}
14081408

1409+
/**********************************************************************
1410+
* Layout items
1411+
* */
1412+
1413+
struct LayoutItem {
1414+
PyFT2Font *ft_object;
1415+
std::u32string character;
1416+
int glyph_index;
1417+
double x;
1418+
double y;
1419+
double prev_kern;
1420+
1421+
LayoutItem(PyFT2Font *f, std::u32string c, int i, double x, double y, double k) :
1422+
ft_object(f), character(c), glyph_index(i), x(x), y(y), prev_kern(k) {}
1423+
1424+
std::string to_string()
1425+
{
1426+
std::ostringstream out;
1427+
out << "LayoutItem(ft_object=" << PyFT2Font_fname(ft_object);
1428+
out << ", char=" << character[0];
1429+
out << ", glyph_index=" << glyph_index;
1430+
out << ", x=" << x;
1431+
out << ", y=" << y;
1432+
out << ", prev_kern=" << prev_kern;
1433+
out << ")";
1434+
return out.str();
1435+
}
1436+
};
1437+
1438+
const char *PyFT2Font_layout__doc__ = R"""(
1439+
Layout a string and yield information about each used glyph.
1440+
1441+
.. warning::
1442+
This API uses the fallback list and is both private and provisional: do not use
1443+
it directly.
1444+
1445+
Parameters
1446+
----------
1447+
text : str
1448+
The characters for which to find fonts.
1449+
1450+
Returns
1451+
-------
1452+
list[LayoutItem]
1453+
)""";
1454+
1455+
static auto
1456+
PyFT2Font_layout(PyFT2Font *self, std::u32string text, LoadFlags flags,
1457+
std::optional<std::vector<std::string>> features = std::nullopt,
1458+
std::variant<FT2Font::LanguageType, std::string> languages_or_str = nullptr)
1459+
{
1460+
const auto hinting_factor = self->get_hinting_factor();
1461+
const auto load_flags = static_cast<FT_Int32>(flags);
1462+
1463+
FT2Font::LanguageType languages;
1464+
if (auto value = std::get_if<FT2Font::LanguageType>(&languages_or_str)) {
1465+
languages = std::move(*value);
1466+
} else if (auto value = std::get_if<std::string>(&languages_or_str)) {
1467+
languages = std::vector<FT2Font::LanguageRange>{
1468+
FT2Font::LanguageRange{*value, 0, text.size()}
1469+
};
1470+
} else {
1471+
// NOTE: this can never happen as pybind11 would have checked the type in the
1472+
// Python wrapper before calling this function, but we need to keep the
1473+
// std::get_if instead of std::get for macOS 10.12 compatibility.
1474+
throw py::type_error("languages must be str or list of tuple");
1475+
}
1476+
1477+
std::set<FT_String*> glyph_seen_fonts;
1478+
auto glyphs = self->layout(text, load_flags, features, languages, glyph_seen_fonts);
1479+
1480+
std::set<decltype(raqm_glyph_t::cluster)> clusters;
1481+
for (auto &glyph : glyphs) {
1482+
clusters.emplace(glyph.cluster);
1483+
}
1484+
1485+
std::vector<LayoutItem> items;
1486+
1487+
double x = 0.0;
1488+
double y = 0.0;
1489+
std::optional<double> prev_advance = std::nullopt;
1490+
double prev_x = 0.0;
1491+
for (auto &glyph : glyphs) {
1492+
auto ft_object = static_cast<PyFT2Font *>(glyph.ftface->generic.data);
1493+
1494+
ft_object->load_glyph(glyph.index, load_flags);
1495+
1496+
double prev_kern = 0.0;
1497+
if (prev_advance.has_value()) {
1498+
double actual_advance = (x + glyph.x_offset) - prev_x;
1499+
prev_kern = actual_advance - prev_advance.value();
1500+
}
1501+
1502+
auto next = clusters.upper_bound(glyph.cluster);
1503+
auto end = (next != clusters.end()) ? *next : text.size();
1504+
auto substr = text.substr(glyph.cluster, end - glyph.cluster);
1505+
1506+
items.emplace_back(ft_object, substr, glyph.index,
1507+
(x + glyph.x_offset) / 64.0, (y + glyph.y_offset) / 64.0,
1508+
prev_kern / 64.0);
1509+
prev_x = x + glyph.x_offset;
1510+
x += glyph.x_advance;
1511+
y += glyph.y_advance;
1512+
// Note, linearHoriAdvance is a 16.16 instead of 26.6 fixed-point value.
1513+
prev_advance = ft_object->get_face()->glyph->linearHoriAdvance / 1024.0 / hinting_factor;
1514+
}
1515+
1516+
return items;
1517+
}
1518+
1519+
/**********************************************************************
1520+
* Deprecations
1521+
* */
1522+
14091523
static py::object
14101524
ft2font__getattr__(std::string name) {
14111525
auto api = py::module_::import("matplotlib._api");
@@ -1540,8 +1654,23 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
15401654
.def_property_readonly("bbox", &PyGlyph_get_bbox,
15411655
"The control box of the glyph.");
15421656

1543-
auto cls = py::class_<PyFT2Font>(m, "FT2Font", py::is_final(), py::buffer_protocol(),
1544-
PyFT2Font__doc__)
1657+
py::class_<LayoutItem>(m, "LayoutItem", py::is_final())
1658+
.def_readonly("ft_object", &LayoutItem::ft_object,
1659+
"The FT_Face of the item.")
1660+
.def_readonly("char", &LayoutItem::character,
1661+
"The character code for the item.")
1662+
.def_readonly("glyph_index", &LayoutItem::glyph_index,
1663+
"The glyph index for the item.")
1664+
.def_readonly("x", &LayoutItem::x,
1665+
"The x position of the item.")
1666+
.def_readonly("y", &LayoutItem::y,
1667+
"The y position of the item.")
1668+
.def_readonly("prev_kern", &LayoutItem::prev_kern,
1669+
"The kerning between this item and the previous one.")
1670+
.def("__str__", &LayoutItem::to_string);
1671+
1672+
auto cls = py::class_<PyFT2Font>(m, "FT2Font", py::is_final(), py::buffer_protocol(),
1673+
PyFT2Font__doc__)
15451674
.def(py::init(&PyFT2Font_init),
15461675
"filename"_a, "hinting_factor"_a=8, py::kw_only(),
15471676
"_fallback_list"_a=py::none(), "_kerning_factor"_a=py::none(),
@@ -1556,6 +1685,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
15561685
PyFT2Font_select_charmap__doc__)
15571686
.def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a,
15581687
PyFT2Font_get_kerning__doc__)
1688+
.def("_layout", &PyFT2Font_layout, "string"_a, "flags"_a, py::kw_only(),
1689+
"features"_a=nullptr, "language"_a=nullptr,
1690+
PyFT2Font_layout__doc__)
15591691
.def("set_text", &PyFT2Font_set_text,
15601692
"string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(),
15611693
"features"_a=nullptr,

0 commit comments

Comments
 (0)