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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy