From 959799138e6b53bea788fb647f5315659f7c4e79 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 24 Sep 2025 01:13:22 -0400 Subject: [PATCH 1/2] ft2font: Add a wrapper around layouting for vector usage --- ci/mypy-stubtest-allowlist.txt | 1 + lib/matplotlib/ft2font.pyi | 23 ++++++ src/ft2font_wrapper.cpp | 142 +++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 6e3e487d934f..1a9a888a1896 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -6,6 +6,7 @@ matplotlib\._.* matplotlib\.rcsetup\._listify_validator matplotlib\.rcsetup\._validate_linestyle matplotlib\.ft2font\.Glyph +matplotlib\.ft2font\.LayoutItem matplotlib\.testing\.jpl_units\..* matplotlib\.sphinxext(\..*)? diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index a4d1c77061be..88745e5e5cc9 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -191,6 +191,22 @@ class _SfntPcltDict(TypedDict): widthType: int serifStyle: int +@final +class LayoutItem: + @property + def ft_object(self) -> FT2Font: ... + @property + def char(self) -> str: ... + @property + def glyph_index(self) -> GlyphIndexType: ... + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + @property + def prev_kern(self) -> float: ... + def __str__(self) -> str: ... + @final class FT2Font(Buffer): def __init__( @@ -204,6 +220,13 @@ class FT2Font(Buffer): if sys.version_info[:2] >= (3, 12): def __buffer__(self, flags: int) -> memoryview: ... def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... + def _layout( + self, + text: str, + flags: LoadFlags, + features: tuple[str, ...] | None = ..., + language: str | tuple[tuple[str, int, int], ...] | None = ..., + ) -> list[LayoutItem]: ... def clear(self) -> None: ... def draw_glyph_to_bitmap( self, image: NDArray[np.uint8], x: int, y: int, glyph: Glyph, antialiased: bool = ... diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index a348f0d312b6..6aa9188317fa 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -1409,6 +1409,119 @@ PyFT2Font__get_type1_encoding_vector(PyFT2Font *self) return indices; } +/********************************************************************** + * Layout items + * */ + +struct LayoutItem { + PyFT2Font *ft_object; + std::u32string character; + int glyph_index; + double x; + double y; + double prev_kern; + + LayoutItem(PyFT2Font *f, std::u32string c, int i, double x, double y, double k) : + ft_object(f), character(c), glyph_index(i), x(x), y(y), prev_kern(k) {} +}; + +const char *PyFT2Font_layout__doc__ = R"""( + Layout a string and yield information about each used glyph. + + .. warning:: + This API uses the fallback list and is both private and provisional: do not use + it directly. + + .. versionadded:: 3.11 + + Parameters + ---------- + text : str + The characters for which to find fonts. + flags : LoadFlags, default: `.LoadFlags.FORCE_AUTOHINT` + Any bitwise-OR combination of the `.LoadFlags` flags. + features : tuple[str, ...], optional + The font feature tags to use for the font. + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. + + Returns + ------- + list[LayoutItem] +)"""; + +static auto +PyFT2Font_layout(PyFT2Font *self, std::u32string text, LoadFlags flags, + std::optional> features = std::nullopt, + std::variant languages_or_str = nullptr) +{ + const auto hinting_factor = self->get_hinting_factor(); + const auto load_flags = static_cast(flags); + + FT2Font::LanguageType languages; + if (auto value = std::get_if(&languages_or_str)) { + languages = std::move(*value); + } else if (auto value = std::get_if(&languages_or_str)) { + languages = std::vector{ + FT2Font::LanguageRange{*value, 0, text.size()} + }; + } else { + // NOTE: this can never happen as pybind11 would have checked the type in the + // Python wrapper before calling this function, but we need to keep the + // std::get_if instead of std::get for macOS 10.12 compatibility. + throw py::type_error("languages must be str or list of tuple"); + } + + std::set glyph_seen_fonts; + auto glyphs = self->layout(text, load_flags, features, languages, glyph_seen_fonts); + + std::set clusters; + for (auto &glyph : glyphs) { + clusters.emplace(glyph.cluster); + } + + std::vector items; + + double x = 0.0; + double y = 0.0; + std::optional prev_advance = std::nullopt; + double prev_x = 0.0; + for (auto &glyph : glyphs) { + auto ft_object = static_cast(glyph.ftface->generic.data); + + ft_object->load_glyph(glyph.index, load_flags); + + double prev_kern = 0.0; + if (prev_advance) { + double actual_advance = (x + glyph.x_offset) - prev_x; + prev_kern = actual_advance - *prev_advance; + } + + auto next = clusters.upper_bound(glyph.cluster); + auto end = (next != clusters.end()) ? *next : text.size(); + auto substr = text.substr(glyph.cluster, end - glyph.cluster); + + items.emplace_back(ft_object, substr, glyph.index, + (x + glyph.x_offset) / 64.0, (y + glyph.y_offset) / 64.0, + prev_kern / 64.0); + prev_x = x + glyph.x_offset; + x += glyph.x_advance; + y += glyph.y_advance; + // Note, linearHoriAdvance is a 16.16 instead of 26.6 fixed-point value. + prev_advance = ft_object->get_face()->glyph->linearHoriAdvance / 1024.0 / hinting_factor; + } + + return items; +} + +/********************************************************************** + * Deprecations + * */ + static py::object ft2font__getattr__(std::string name) { auto api = py::module_::import("matplotlib._api"); @@ -1543,6 +1656,32 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def_property_readonly("bbox", &PyGlyph_get_bbox, "The control box of the glyph."); + py::class_(m, "LayoutItem", py::is_final()) + .def(py::init<>([]() -> LayoutItem { + // LayoutItem is not useful from Python, so mark it as not constructible. + throw std::runtime_error("LayoutItem is not constructible"); + })) + .def_readonly("ft_object", &LayoutItem::ft_object, + "The FT_Face of the item.") + .def_readonly("char", &LayoutItem::character, + "The character code for the item.") + .def_readonly("glyph_index", &LayoutItem::glyph_index, + "The glyph index for the item.") + .def_readonly("x", &LayoutItem::x, + "The x position of the item.") + .def_readonly("y", &LayoutItem::y, + "The y position of the item.") + .def_readonly("prev_kern", &LayoutItem::prev_kern, + "The kerning between this item and the previous one.") + .def("__str__", + [](const LayoutItem& item) { + return + "LayoutItem(ft_object={}, char={!r}, glyph_index={}, "_s + "x={}, y={}, prev_kern={})"_s.format( + PyFT2Font_fname(item.ft_object), item.character, + item.glyph_index, item.x, item.y, item.prev_kern); + }); + auto cls = py::class_(m, "FT2Font", py::is_final(), py::buffer_protocol(), PyFT2Font__doc__) .def(py::init(&PyFT2Font_init), @@ -1559,6 +1698,9 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) PyFT2Font_select_charmap__doc__) .def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a, PyFT2Font_get_kerning__doc__) + .def("_layout", &PyFT2Font_layout, "string"_a, "flags"_a, py::kw_only(), + "features"_a=nullptr, "language"_a=nullptr, + PyFT2Font_layout__doc__) .def("set_text", &PyFT2Font_set_text, "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(), "features"_a=nullptr, "language"_a=nullptr, From bd17cd43dd71b879928828c899cb2f9087ce745e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 24 May 2025 05:02:40 -0400 Subject: [PATCH 2/2] Use libraqm for text in vector outputs --- lib/matplotlib/_text_helpers.py | 44 ++++++------------- lib/matplotlib/backends/_backend_pdf_ps.py | 18 ++++++-- lib/matplotlib/backends/backend_pdf.py | 23 +++++++--- lib/matplotlib/backends/backend_ps.py | 13 +++--- lib/matplotlib/backends/backend_svg.py | 8 +++- lib/matplotlib/ft2font.pyi | 1 - lib/matplotlib/tests/test_ft2font.py | 18 ++++---- lib/matplotlib/tests/test_text.py | 8 ++-- lib/matplotlib/textpath.py | 3 +- src/ft2font_wrapper.cpp | 50 ---------------------- 10 files changed, 72 insertions(+), 114 deletions(-) diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index e4e6bb03a145..0ebbf3ac139d 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -4,29 +4,23 @@ from __future__ import annotations -import dataclasses +from collections.abc import Iterator from . import _api -from .ft2font import FT2Font, GlyphIndexType, Kerning, LoadFlags +from .ft2font import FT2Font, CharacterCodeType, LayoutItem, LoadFlags -@dataclasses.dataclass(frozen=True) -class LayoutItem: - ft_object: FT2Font - char: str - glyph_index: GlyphIndexType - x: float - prev_kern: float - - -def warn_on_missing_glyph(codepoint, fontnames): +def warn_on_missing_glyph(codepoint: CharacterCodeType, fontnames: str): _api.warn_external( f"Glyph {codepoint} " f"({chr(codepoint).encode('ascii', 'namereplace').decode('ascii')}) " f"missing from font(s) {fontnames}.") -def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=None): +def layout(string: str, font: FT2Font, *, + features: tuple[str] | None = None, + language: str | tuple[tuple[str, int, int], ...] | None = None + ) -> Iterator[LayoutItem]: """ Render *string* with *font*. @@ -41,8 +35,6 @@ def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=N The font. features : tuple of str, optional The font features to apply to the text. - kern_mode : Kerning - A FreeType kerning mode. language : str, optional The language of the text in a format accepted by libraqm, namely `a BCP47 language code `_. @@ -51,20 +43,8 @@ def layout(string, font, *, features=None, kern_mode=Kerning.DEFAULT, language=N ------ LayoutItem """ - x = 0 - prev_glyph_index = None - char_to_font = font._get_fontmap(string) # TODO: Pass in features and language. - base_font = font - for char in string: - # This has done the fallback logic - font = char_to_font.get(char, base_font) - glyph_index = font.get_char_index(ord(char)) - kern = ( - base_font.get_kerning(prev_glyph_index, glyph_index, kern_mode) / 64 - if prev_glyph_index is not None else 0. - ) - x += kern - glyph = font.load_glyph(glyph_index, flags=LoadFlags.NO_HINTING) - yield LayoutItem(font, char, glyph_index, x, kern) - x += glyph.linearHoriAdvance / 65536 - prev_glyph_index = glyph_index + for raqm_item in font._layout(string, LoadFlags.NO_HINTING, + features=features, language=language): + raqm_item.ft_object.load_glyph(raqm_item.glyph_index, + flags=LoadFlags.NO_HINTING) + yield raqm_item diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 0ff17a105c20..b1a3f5c9f18b 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -199,7 +199,10 @@ def __init__(self, subset_size: int = 0): self.glyph_maps: dict[str, GlyphMap] = {} self.subset_size = subset_size - def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: + def track(self, font: FT2Font, s: str, + features: tuple[str, ...] | None = ..., + language: str | tuple[tuple[str, int, int], ...] | None = None + ) -> list[tuple[int, CharacterCodeType]]: """ Record that string *s* is being typeset using font *font*. @@ -209,6 +212,14 @@ def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: A font that is being used for the provided string. s : str The string that should be marked as tracked by the provided font. + features : tuple[str, ...], optional + The font feature tags to use for the font. + + Available font feature tags may be found at + https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. Returns ------- @@ -220,8 +231,9 @@ def track(self, font: FT2Font, s: str) -> list[tuple[int, CharacterCodeType]]: and the character codes will be returned from the string unchanged. """ return [ - self.track_glyph(f, ord(c), f.get_char_index(ord(c))) - for c, f in font._get_fontmap(s).items() + self.track_glyph(raqm_item.ft_object, raqm_item.char, raqm_item.glyph_index) + for raqm_item in font._layout(s, ft2font.LoadFlags.NO_HINTING, + features=features, language=language) ] def track_glyph(self, font: FT2Font, chars: str | CharacterCodeType, diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index a5035d16e24f..613b00987730 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -34,7 +34,7 @@ from matplotlib.figure import Figure from matplotlib.font_manager import get_font, fontManager as _fontManager from matplotlib._afm import AFM -from matplotlib.ft2font import FT2Font, FaceFlags, Kerning, LoadFlags, StyleFlags +from matplotlib.ft2font import FT2Font, FaceFlags, LoadFlags, StyleFlags from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path from matplotlib.dates import UTC @@ -469,6 +469,7 @@ class Op(Enum): textpos = b'Td' selectfont = b'Tf' textmatrix = b'Tm' + textrise = b'Ts' show = b'Tj' showkern = b'TJ' setlinewidth = b'w' @@ -2285,6 +2286,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # If fonttype is neither 3 nor 42, emit the whole string at once # without manual kerning. if fonttype not in [3, 42]: + if not mpl.rcParams['pdf.use14corefonts']: + self.file._character_tracker.track(font, s, + features=features, language=language) self.file.output(Op.begin_text, self.file.fontName(prop), fontsize, Op.selectfont) self._setup_textpos(x, y, angle) @@ -2305,6 +2309,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # kerning between chunks. else: def output_singlebyte_chunk(kerns_or_chars): + if not kerns_or_chars: + return self.file.output( # See pdf spec "Text space details" for the 1000/fontsize # (aka. 1000/T_fs) factor. @@ -2312,6 +2318,7 @@ def output_singlebyte_chunk(kerns_or_chars): else self._encode_glyphs(group, fonttype) for tp, group in itertools.groupby(kerns_or_chars, type)], Op.showkern) + kerns_or_chars.clear() # Do the rotation and global translation as a single matrix # concatenation up front self.file.output(Op.gsave) @@ -2326,24 +2333,26 @@ def output_singlebyte_chunk(kerns_or_chars): # Emit all the characters in a BT/ET group. self.file.output(Op.begin_text) for item in _text_helpers.layout(s, font, features=features, - kern_mode=Kerning.UNFITTED, language=language): subset, charcode = self.file._character_tracker.track_glyph( item.ft_object, item.char, item.glyph_index) if (item.ft_object, subset) != prev_font: - if singlebyte_chunk: - output_singlebyte_chunk(singlebyte_chunk) + output_singlebyte_chunk(singlebyte_chunk) ft_name = self.file.fontName(item.ft_object.fname, subset) self.file.output(ft_name, fontsize, Op.selectfont) self._setup_textpos(item.x, 0, 0, prev_start_x, 0, 0) - singlebyte_chunk = [] prev_font = (item.ft_object, subset) prev_start_x = item.x + if item.y: + output_singlebyte_chunk(singlebyte_chunk) + self.file.output(item.y, Op.textrise) if item.prev_kern: singlebyte_chunk.append(item.prev_kern) singlebyte_chunk.append(charcode) - if singlebyte_chunk: - output_singlebyte_chunk(singlebyte_chunk) + if item.y: + output_singlebyte_chunk(singlebyte_chunk) + self.file.output(0, Op.textrise) + output_singlebyte_chunk(singlebyte_chunk) self.file.output(Op.end_text) self.file.output(Op.grestore) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 2743da13aec5..8ad31290a643 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -776,7 +776,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) - stream = [] # list of (ps_name, x, char_name) + stream = [] # list of (ps_name, x, y, char_name) if mpl.rcParams['ps.useafm']: font = self._get_font_afm(prop) @@ -794,7 +794,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): kern = font.get_kern_dist_from_name(last_name, name) last_name = name thisx += kern * scale - stream.append((ps_name, thisx, name)) + stream.append((ps_name, thisx, 0, name)) thisx += width * scale else: @@ -814,14 +814,13 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): ps_name = (item.ft_object.postscript_name .encode("ascii", "replace").decode("ascii")) glyph_name = item.ft_object.get_glyph_name(item.glyph_index) - stream.append((f'{ps_name}-{subset}', item.x, glyph_name)) + stream.append((f'{ps_name}-{subset}', item.x, item.y, glyph_name)) self.set_color(*gc.get_rgb()) - for ps_name, group in itertools. \ - groupby(stream, lambda entry: entry[0]): + for ps_name, group in itertools.groupby(stream, lambda entry: entry[0]): self.set_font(ps_name, prop.get_size_in_points(), False) - thetext = "\n".join(f"{x:g} 0 m /{name:s} glyphshow" - for _, x, name in group) + thetext = "\n".join(f"{x:g} {y:g} m /{name:s} glyphshow" + for _, x, y, name in group) self._pswriter.write(f"""\ gsave {self._get_clip_cmd(gc)} diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 7b94a2b9ba2b..06d868bdab62 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1048,6 +1048,11 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): text2path = self._text2path color = rgb2hex(gc.get_rgb()) fontsize = prop.get_size_in_points() + if mtext is not None: + features = mtext.get_fontfeatures() + language = mtext.get_language() + else: + features = language = None style = {} if color != '#000000': @@ -1068,7 +1073,8 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): if not ismath: font = text2path._get_font(prop) glyph_info, glyph_map_new, rects = text2path.get_glyphs_with_font( - font, s, glyph_map=glyph_map, return_new_glyphs_only=True) + font, s, features=features, language=language, + glyph_map=glyph_map, return_new_glyphs_only=True) self._update_glyph_map_defs(glyph_map_new) for glyph_repr, xposition, yposition, scale in glyph_info: diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 88745e5e5cc9..9345c1c9057f 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -219,7 +219,6 @@ class FT2Font(Buffer): ) -> None: ... if sys.version_info[:2] >= (3, 12): def __buffer__(self, flags: int) -> memoryview: ... - def _get_fontmap(self, string: str) -> dict[str, FT2Font]: ... def _layout( self, text: str, diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 3c066a59e939..4a874deb5343 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -972,7 +972,7 @@ def test_fallback_last_resort(recwarn): "Glyph 128579 (\\N{UPSIDE-DOWN FACE}) missing from font(s)") -def test__get_fontmap(): +def test__layout(): fonts, test_str = _gen_multi_font_text() # Add some glyphs that don't exist in either font to check the Last Resort fallback. missing_glyphs = '\n几个汉字' @@ -981,11 +981,11 @@ def test__get_fontmap(): ft = fm.get_font( fm.fontManager._find_fonts_by_props(fm.FontProperties(family=fonts)) ) - fontmap = ft._get_fontmap(test_str) - for char, font in fontmap.items(): - if char in missing_glyphs: - assert Path(font.fname).name == 'LastResortHE-Regular.ttf' - elif ord(char) > 127: - assert Path(font.fname).name == 'DejaVuSans.ttf' - else: - assert Path(font.fname).name == 'cmr10.ttf' + for substr in test_str.split('\n'): + for item in ft._layout(substr, ft2font.LoadFlags.DEFAULT): + if item.char in missing_glyphs: + assert Path(item.ft_object.fname).name == 'LastResortHE-Regular.ttf' + elif ord(item.char) > 127: + assert Path(item.ft_object.fname).name == 'DejaVuSans.ttf' + else: + assert Path(item.ft_object.fname).name == 'cmr10.ttf' diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 84d833a1a0ea..5ae606e413f0 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -115,7 +115,7 @@ def find_matplotlib_font(**kw): ax.set_yticks([]) -@image_comparison(['complex.png']) +@image_comparison(['complex'], extensions=['png', 'pdf', 'svg', 'eps']) def test_complex_shaping(): # Raqm is Arabic for writing; note that because Arabic is RTL, the characters here # may seem to be in a different order than expected, but libraqm will order them @@ -1240,7 +1240,8 @@ def test_ytick_rotation_mode(): plt.subplots_adjust(left=0.4, right=0.6, top=.99, bottom=.01) -@image_comparison(baseline_images=['features.png'], remove_text=False, style='mpl20') +@image_comparison(['features'], remove_text=False, style='mpl20', + extensions=['png', 'pdf', 'svg', 'eps']) def test_text_features(): fig = plt.figure(figsize=(5, 1.5)) t = fig.text(1, 0.7, 'Default: fi ffi fl st', @@ -1270,7 +1271,8 @@ def test_text_language_invalid(input, match): Text(0, 0, 'foo', language=input) -@image_comparison(baseline_images=['language.png'], remove_text=False, style='mpl20') +@image_comparison(['language'], remove_text=False, style='mpl20', + extensions=['png', 'pdf', 'svg', 'eps']) def test_text_language(): fig = plt.figure(figsize=(5, 3)) diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index e7bb95159deb..d7c1cdf1622f 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -147,15 +147,16 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, glyph_map_new = glyph_map xpositions = [] + ypositions = [] glyph_reprs = [] for item in _text_helpers.layout(s, font, features=features, language=language): glyph_repr = self._get_glyph_repr(item.ft_object, item.glyph_index) glyph_reprs.append(glyph_repr) xpositions.append(item.x) + ypositions.append(item.y) if glyph_repr not in glyph_map: glyph_map_new[glyph_repr] = item.ft_object.get_path() - ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) rects = [] diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 6aa9188317fa..21d8b01656b8 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -623,54 +623,6 @@ PyFT2Font_get_kerning(PyFT2Font *self, FT_UInt left, FT_UInt right, return self->get_kerning(left, right, mode); } -const char *PyFT2Font_get_fontmap__doc__ = R"""( - Get a mapping between characters and the font that includes them. - - .. warning:: - This API uses the fallback list and is both private and provisional: do not use - it directly. - - Parameters - ---------- - text : str - The characters for which to find fonts. - - Returns - ------- - dict[str, FT2Font] - A dictionary mapping unicode characters to `.FT2Font` objects. -)"""; - -static py::dict -PyFT2Font_get_fontmap(PyFT2Font *self, std::u32string text) -{ - std::set codepoints; - - py::dict char_to_font; - for (auto code : text) { - if (!codepoints.insert(code).second) { - continue; - } - - py::object target_font; - int index; - if (self->get_char_fallback_index(code, index)) { - if (index >= 0) { - target_font = self->fallbacks[index]; - } else { - target_font = py::cast(self); - } - } else { - // TODO Handle recursion! - target_font = py::cast(self); - } - - auto key = py::cast(std::u32string(1, code)); - char_to_font[key] = target_font; - } - return char_to_font; -} - const char *PyFT2Font_set_text__doc__ = R"""( Set the text *string* and *angle*. @@ -1705,8 +1657,6 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(), "features"_a=nullptr, "language"_a=nullptr, PyFT2Font_set_text__doc__) - .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, - PyFT2Font_get_fontmap__doc__) .def("get_num_glyphs", &PyFT2Font::get_num_glyphs, PyFT2Font_get_num_glyphs__doc__) .def("load_char", &PyFT2Font_load_char,