Skip to content

Commit fb7cbf5

Browse files
committed
ft2font: Add a wrapper around layouting for vector usage
1 parent 8da3099 commit fb7cbf5

File tree

3 files changed

+166
-0
lines changed

3 files changed

+166
-0
lines changed

ci/mypy-stubtest-allowlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ matplotlib\._.*
66
matplotlib\.rcsetup\._listify_validator
77
matplotlib\.rcsetup\._validate_linestyle
88
matplotlib\.ft2font\.Glyph
9+
matplotlib\.ft2font\.LayoutItem
910
matplotlib\.testing\.jpl_units\..*
1011
matplotlib\.sphinxext(\..*)?
1112

lib/matplotlib/ft2font.pyi

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,22 @@ class _SfntPcltDict(TypedDict):
191191
widthType: int
192192
serifStyle: int
193193

194+
@final
195+
class LayoutItem:
196+
@property
197+
def ft_object(self) -> FT2Font: ...
198+
@property
199+
def char(self) -> str: ...
200+
@property
201+
def glyph_index(self) -> GlyphIndexType: ...
202+
@property
203+
def x(self) -> float: ...
204+
@property
205+
def y(self) -> float: ...
206+
@property
207+
def prev_kern(self) -> float: ...
208+
def __str__(self) -> str: ...
209+
194210
@final
195211
class FT2Font(Buffer):
196212
def __init__(
@@ -204,6 +220,13 @@ class FT2Font(Buffer):
204220
if sys.version_info[:2] >= (3, 12):
205221
def __buffer__(self, flags: int) -> memoryview: ...
206222
def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ...
223+
def _layout(
224+
self,
225+
text: str,
226+
flags: LoadFlags,
227+
features: tuple[str, ...] | None = ...,
228+
language: str | tuple[tuple[str, int, int], ...] | None = ...,
229+
) -> list[LayoutItem]: ...
207230
def clear(self) -> None: ...
208231
def draw_glyph_to_bitmap(
209232
self, image: NDArray[np.uint8], x: int, y: int, glyph: Glyph, antialiased: bool = ...

src/ft2font_wrapper.cpp

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,119 @@ PyFT2Font__get_type1_encoding_vector(PyFT2Font *self)
14091409
return indices;
14101410
}
14111411

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

1659+
py::class_<LayoutItem>(m, "LayoutItem", py::is_final())
1660+
.def(py::init<>([]() -> LayoutItem {
1661+
// LayoutItem is not useful from Python, so mark it as not constructible.
1662+
throw std::runtime_error("LayoutItem is not constructible");
1663+
}))
1664+
.def_readonly("ft_object", &LayoutItem::ft_object,
1665+
"The FT_Face of the item.")
1666+
.def_readonly("char", &LayoutItem::character,
1667+
"The character code for the item.")
1668+
.def_readonly("glyph_index", &LayoutItem::glyph_index,
1669+
"The glyph index for the item.")
1670+
.def_readonly("x", &LayoutItem::x,
1671+
"The x position of the item.")
1672+
.def_readonly("y", &LayoutItem::y,
1673+
"The y position of the item.")
1674+
.def_readonly("prev_kern", &LayoutItem::prev_kern,
1675+
"The kerning between this item and the previous one.")
1676+
.def("__str__",
1677+
[](const LayoutItem& item) {
1678+
return
1679+
"LayoutItem(ft_object={}, char={!r}, glyph_index={}, "_s
1680+
"x={}, y={}, prev_kern={})"_s.format(
1681+
PyFT2Font_fname(item.ft_object), item.character,
1682+
item.glyph_index, item.x, item.y, item.prev_kern);
1683+
});
1684+
15461685
auto cls = py::class_<PyFT2Font>(m, "FT2Font", py::is_final(), py::buffer_protocol(),
15471686
PyFT2Font__doc__)
15481687
.def(py::init(&PyFT2Font_init),
@@ -1559,6 +1698,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
15591698
PyFT2Font_select_charmap__doc__)
15601699
.def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a,
15611700
PyFT2Font_get_kerning__doc__)
1701+
.def("_layout", &PyFT2Font_layout, "string"_a, "flags"_a, py::kw_only(),
1702+
"features"_a=nullptr, "language"_a=nullptr,
1703+
PyFT2Font_layout__doc__)
15621704
.def("set_text", &PyFT2Font_set_text,
15631705
"string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(),
15641706
"features"_a=nullptr, "language"_a=nullptr,

0 commit comments

Comments
 (0)