From ebd16eb897570ef89dd04f04501f9a65dee16bf4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 22 Mar 2025 05:15:06 -0400 Subject: [PATCH 1/2] Add language parameter to Text objects --- doc/release/next_whats_new/text_language.rst | 37 +++++++++++++ lib/matplotlib/_text_helpers.py | 7 ++- lib/matplotlib/backends/backend_agg.py | 3 +- lib/matplotlib/backends/backend_pdf.py | 6 ++- lib/matplotlib/backends/backend_ps.py | 3 +- lib/matplotlib/ft2font.pyi | 7 ++- lib/matplotlib/mpl-data/matplotlibrc | 5 ++ lib/matplotlib/rcsetup.py | 1 + lib/matplotlib/tests/test_ft2font.py | 31 +++++++++++ lib/matplotlib/tests/test_text.py | 57 ++++++++++++++++++++ lib/matplotlib/text.py | 38 +++++++++++++ lib/matplotlib/text.pyi | 4 +- lib/matplotlib/textpath.py | 12 +++-- lib/matplotlib/textpath.pyi | 5 +- src/ft2font.cpp | 24 ++++++++- src/ft2font.h | 6 ++- src/ft2font_wrapper.cpp | 22 ++++++-- 17 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 doc/release/next_whats_new/text_language.rst diff --git a/doc/release/next_whats_new/text_language.rst b/doc/release/next_whats_new/text_language.rst new file mode 100644 index 000000000000..d79510ae61a4 --- /dev/null +++ b/doc/release/next_whats_new/text_language.rst @@ -0,0 +1,37 @@ +Specifying text language +------------------------ + +OpenType fonts may support language systems which can be used to select different +typographic conventions, e.g., localized variants of letters that share a single Unicode +code point, or different default font features. The text API now supports setting a +language to be used and may be set/get with: + +- `matplotlib.text.Text.set_language` / `matplotlib.text.Text.get_language` +- Any API that creates a `.Text` object by passing the *language* argument (e.g., + ``plt.xlabel(..., language=...)``) + +The language of the text must be in a format accepted by libraqm, namely `a BCP47 +language code `_. If None or +unset, then no particular language will be implied, and default font settings will be +used. + +For example, Matplotlib's default font ``DejaVu Sans`` supports language-specific glyphs +in the Serbian and Macedonian languages in the Cyrillic alphabet, or the Sámi family of +languages in the Latin alphabet. + +.. plot:: + :include-source: + + fig = plt.figure(figsize=(7, 3)) + + char = '\U00000431' + fig.text(0.5, 0.8, f'\\U{ord(char):08x}', fontsize=40, horizontalalignment='center') + fig.text(0, 0.6, f'Serbian: {char}', fontsize=40, language='sr') + fig.text(1, 0.6, f'Russian: {char}', fontsize=40, language='ru', + horizontalalignment='right') + + char = '\U0000014a' + fig.text(0.5, 0.3, f'\\U{ord(char):08x}', fontsize=40, horizontalalignment='center') + fig.text(0, 0.1, f'English: {char}', fontsize=40, language='en') + fig.text(1, 0.1, f'Inari Sámi: {char}', fontsize=40, language='smn', + horizontalalignment='right') diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py index a874c8f4bf81..bb4c50f34a25 100644 --- a/lib/matplotlib/_text_helpers.py +++ b/lib/matplotlib/_text_helpers.py @@ -26,7 +26,7 @@ def warn_on_missing_glyph(codepoint, fontnames): f"missing from font(s) {fontnames}.") -def layout(string, font, *, kern_mode=Kerning.DEFAULT): +def layout(string, font, *, kern_mode=Kerning.DEFAULT, language=None): """ Render *string* with *font*. @@ -41,6 +41,9 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT): The font. 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 `_. Yields ------ @@ -48,7 +51,7 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT): """ x = 0 prev_glyph_idx = None - char_to_font = font._get_fontmap(string) + char_to_font = font._get_fontmap(string) # TODO: Pass in language. base_font = font for char in string: # This has done the fallback logic diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index feb4b0c8be01..2da422a88e84 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -190,7 +190,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): font = self._prepare_font(prop) # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). - font.set_text(s, 0, flags=get_hinting_flag()) + font.set_text(s, 0, flags=get_hinting_flag(), + language=mtext.get_language() if mtext is not None else None) font.draw_glyphs_to_bitmap( antialiased=gc.get_antialiased()) d = font.get_descent() / 64.0 diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index ff351e301176..2bf0c2c4bd4b 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2345,6 +2345,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): return self.draw_mathtext(gc, x, y, s, prop, angle) fontsize = prop.get_size_in_points() + language = mtext.get_language() if mtext is not None else None if mpl.rcParams['pdf.use14corefonts']: font = self._get_font_afm(prop) @@ -2355,7 +2356,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): fonttype = mpl.rcParams['pdf.fonttype'] if gc.get_url() is not None: - font.set_text(s) + font.set_text(s, language=language) width, height = font.get_width_height() self.file._annotations[-1][1].append(_get_link_annotation( gc, x, y, width / 64, height / 64, angle)) @@ -2389,7 +2390,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): multibyte_glyphs = [] prev_was_multibyte = True prev_font = font - for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED): + for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED, + language=language): if _font_supports_glyph(fonttype, ord(item.char)): if prev_was_multibyte or item.ft_object != prev_font: singlebyte_chunks.append((item.ft_object, item.x, [])) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 368564a1518d..a3ba63ccf079 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -794,9 +794,10 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): thisx += width * scale else: + language = mtext.get_language() if mtext is not None else None font = self._get_font_ttf(prop) self._character_tracker.track(font, s) - for item in _text_helpers.layout(s, font): + for item in _text_helpers.layout(s, font, language=language): ps_name = (item.ft_object.postscript_name .encode("ascii", "replace").decode("ascii")) glyph_name = item.ft_object.get_glyph_name(item.glyph_idx) diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 55c076bb68b6..91d8d6a38818 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -243,7 +243,12 @@ class FT2Font(Buffer): def set_charmap(self, i: int) -> None: ... def set_size(self, ptsize: float, dpi: float) -> None: ... def set_text( - self, string: str, angle: float = ..., flags: LoadFlags = ... + self, + string: str, + angle: float = ..., + flags: LoadFlags = ..., + *, + language: str | list[tuple[str, int, int]] | None = ..., ) -> NDArray[np.float64]: ... @property def ascender(self) -> int: ... diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 223eed396535..66a2569ca6f7 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -292,6 +292,11 @@ ## for more information on text properties #text.color: black +## The language of the text in a format accepted by libraqm, namely `a BCP47 language +## code `_. If None, then no +## particular language will be implied, and default font settings will be used. +#text.language: None + ## FreeType hinting flag ("foo" corresponds to FT_LOAD_FOO); may be one of the ## following (Proprietary Matplotlib-specific synonyms are given in parentheses, ## but their use is discouraged): diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index b4224d169815..586365dcf3f2 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1045,6 +1045,7 @@ def _convert_validator_spec(key, conv): "text.kerning_factor": validate_int_or_None, "text.antialiased": validate_bool, "text.parse_math": validate_bool, + "text.language": validate_string_or_None, "mathtext.cal": validate_font_properties, "mathtext.rm": validate_font_properties, diff --git a/lib/matplotlib/tests/test_ft2font.py b/lib/matplotlib/tests/test_ft2font.py index 70e611e17bcc..c464dddc051f 100644 --- a/lib/matplotlib/tests/test_ft2font.py +++ b/lib/matplotlib/tests/test_ft2font.py @@ -783,6 +783,37 @@ def test_ft2font_set_text(): assert font.get_bitmap_offset() == (6, 0) +@pytest.mark.parametrize( + 'input', + [ + [1, 2, 3], + [(1, 2)], + [('en', 'foo', 2)], + [('en', 1, 'foo')], + ], + ids=[ + 'nontuple', + 'wrong length', + 'wrong start type', + 'wrong end type', + ], +) +def test_ft2font_language_invalid(input): + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1) + with pytest.raises(TypeError): + font.set_text('foo', language=input) + + +def test_ft2font_language(): + # This is just a smoke test. + file = fm.findfont('DejaVu Sans') + font = ft2font.FT2Font(file, hinting_factor=1) + font.set_text('foo') + font.set_text('foo', language='en') + font.set_text('foo', language=[('en', 1, 2)]) + + def test_ft2font_loading(): file = fm.findfont('DejaVu Sans') font = ft2font.FT2Font(file, hinting_factor=1) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 8dba63eeef32..0d6062f5f401 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -1202,3 +1202,60 @@ def test_ytick_rotation_mode(): tick.set_rotation(angle) plt.subplots_adjust(left=0.4, right=0.6, top=.99, bottom=.01) + + +@pytest.mark.parametrize( + 'input, match', + [ + ([1, 2, 3], 'must be list of tuple'), + ([(1, 2)], 'must be list of tuple'), + ([('en', 'foo', 2)], 'start location must be int'), + ([('en', 1, 'foo')], 'end location must be int'), + ], +) +def test_text_language_invalid(input, match): + with pytest.raises(TypeError, match=match): + Text(0, 0, 'foo', language=input) + + +@image_comparison(baseline_images=['language.png'], remove_text=False, style='mpl20') +def test_text_language(): + fig = plt.figure(figsize=(5, 3)) + + t = fig.text(0, 0.8, 'Default', fontsize=32) + assert t.get_language() is None + t = fig.text(0, 0.55, 'Lang A', fontsize=32) + assert t.get_language() is None + t = fig.text(0, 0.3, 'Lang B', fontsize=32) + assert t.get_language() is None + t = fig.text(0, 0.05, 'Mixed', fontsize=32) + assert t.get_language() is None + + # DejaVu Sans supports language-specific glyphs in the Serbian and Macedonian + # languages in the Cyrillic alphabet. + cyrillic = '\U00000431' + t = fig.text(0.4, 0.8, cyrillic, fontsize=32) + assert t.get_language() is None + t = fig.text(0.4, 0.55, cyrillic, fontsize=32, language='sr') + assert t.get_language() == 'sr' + t = fig.text(0.4, 0.3, cyrillic, fontsize=32) + t.set_language('ru') + assert t.get_language() == 'ru' + t = fig.text(0.4, 0.05, cyrillic * 4, fontsize=32, + language=[('ru', 0, 1), ('sr', 1, 2), ('ru', 2, 3), ('sr', 3, 4)]) + assert t.get_language() == (('ru', 0, 1), ('sr', 1, 2), ('ru', 2, 3), ('sr', 3, 4)) + + # Or the Sámi family of languages in the Latin alphabet. + latin = '\U0000014a' + t = fig.text(0.7, 0.8, latin, fontsize=32) + assert t.get_language() is None + with plt.rc_context({'text.language': 'en'}): + t = fig.text(0.7, 0.55, latin, fontsize=32) + assert t.get_language() == 'en' + t = fig.text(0.7, 0.3, latin, fontsize=32, language='smn') + assert t.get_language() == 'smn' + # Tuples are not documented, but we'll allow it. + t = fig.text(0.7, 0.05, latin * 4, fontsize=32) + t.set_language((('en', 0, 1), ('smn', 1, 2), ('en', 2, 3), ('smn', 3, 4))) + assert t.get_language() == ( + ('en', 0, 1), ('smn', 1, 2), ('en', 2, 3), ('smn', 3, 4)) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index acde4fb179a2..4d80f9874941 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2,6 +2,7 @@ Classes for including text in a figure. """ +from collections.abc import Sequence import functools import logging import math @@ -136,6 +137,7 @@ def __init__(self, super().__init__() self._x, self._y = x, y self._text = '' + self.set_language(None) self._reset_visual_defaults( text=text, color=color, @@ -1422,6 +1424,42 @@ def _va_for_angle(self, angle): return 'baseline' if anchor_at_left else 'top' return 'top' if anchor_at_left else 'baseline' + def get_language(self): + """Return the language this Text is in.""" + return self._language + + def set_language(self, language): + """ + Set the language of the text. + + Parameters + ---------- + language : str or None + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. + + If None, then defaults to :rc:`text.language`. + """ + _api.check_isinstance((Sequence, str, None), language=language) + language = mpl._val_or_rc(language, 'text.language') + + if not cbook.is_scalar_or_string(language): + language = tuple(language) + for val in language: + if not isinstance(val, tuple) or len(val) != 3: + raise TypeError('language must be list of tuple, not {language!r}') + sublang, start, end = val + if not isinstance(sublang, str): + raise TypeError( + 'sub-language specification must be str, not {sublang!r}') + if not isinstance(start, int): + raise TypeError('start location must be int, not {start!r}') + if not isinstance(end, int): + raise TypeError('end location must be int, not {end!r}') + + self._language = language + self.stale = True + class OffsetFrom: """Callable helper class for working with `Annotation`.""" diff --git a/lib/matplotlib/text.pyi b/lib/matplotlib/text.pyi index 41c7b761ae32..eb3c076b1c5c 100644 --- a/lib/matplotlib/text.pyi +++ b/lib/matplotlib/text.pyi @@ -14,7 +14,7 @@ from .transforms import ( Transform, ) -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from typing import Any, Literal from .typing import ColorType, CoordsType @@ -108,6 +108,8 @@ class Text(Artist): def set_antialiased(self, antialiased: bool) -> None: ... def _ha_for_angle(self, angle: Any) -> Literal['center', 'right', 'left'] | None: ... def _va_for_angle(self, angle: Any) -> Literal['center', 'top', 'baseline'] | None: ... + def get_language(self) -> str | tuple[tuple[str, int, int], ...] | None: ... + def set_language(self, language: str | Sequence[tuple[str, int, int]] | None) -> None: ... class OffsetFrom: def __init__( diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index b57597ded363..039b0a5e416e 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -69,7 +69,7 @@ def get_text_width_height_descent(self, s, prop, ismath): d /= 64.0 return w * scale, h * scale, d * scale - def get_text_path(self, prop, s, ismath=False): + def get_text_path(self, prop, s, ismath=False, *, language=None): """ Convert text *s* to path (a tuple of vertices and codes for matplotlib.path.Path). @@ -82,6 +82,9 @@ def get_text_path(self, prop, s, ismath=False): The text to be converted. ismath : {False, True, "TeX"} If True, use mathtext parser. If "TeX", use tex for rendering. + language : str, optional + The language of the text in a format accepted by libraqm, namely `a BCP47 + language code `_. Returns ------- @@ -109,7 +112,8 @@ def get_text_path(self, prop, s, ismath=False): glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s) elif not ismath: font = self._get_font(prop) - glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s) + glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s, + language=language) else: glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s) @@ -130,7 +134,7 @@ def get_text_path(self, prop, s, ismath=False): return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, - return_new_glyphs_only=False): + return_new_glyphs_only=False, *, language=None): """ Convert string *s* to vertices and codes using the provided ttf font. """ @@ -145,7 +149,7 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, xpositions = [] glyph_ids = [] - for item in _text_helpers.layout(s, font): + for item in _text_helpers.layout(s, font, language=language): char_id = self._get_char_id(item.ft_object, ord(item.char)) glyph_ids.append(char_id) xpositions.append(item.x) diff --git a/lib/matplotlib/textpath.pyi b/lib/matplotlib/textpath.pyi index 34d4e92ac47e..b83b337aa541 100644 --- a/lib/matplotlib/textpath.pyi +++ b/lib/matplotlib/textpath.pyi @@ -16,7 +16,8 @@ class TextToPath: self, s: str, prop: FontProperties, ismath: bool | Literal["TeX"] ) -> tuple[float, float, float]: ... def get_text_path( - self, prop: FontProperties, s: str, ismath: bool | Literal["TeX"] = ... + self, prop: FontProperties, s: str, ismath: bool | Literal["TeX"] = ..., *, + language: str | list[tuple[str, int, int]] | None = ..., ) -> list[np.ndarray]: ... def get_glyphs_with_font( self, @@ -24,6 +25,8 @@ class TextToPath: s: str, glyph_map: dict[str, tuple[np.ndarray, np.ndarray]] | None = ..., return_new_glyphs_only: bool = ..., + *, + language: str | list[tuple[str, int, int]] | None = ..., ) -> tuple[ list[tuple[str, float, float, float]], dict[str, tuple[np.ndarray, np.ndarray]], diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 890fc61974b0..341422ad8018 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -319,7 +319,9 @@ void FT2Font::set_kerning_factor(int factor) } void FT2Font::set_text( - std::u32string_view text, double angle, FT_Int32 flags, std::vector &xys) + std::u32string_view text, double angle, FT_Int32 flags, + LanguageType languages, + std::vector &xys) { FT_Matrix matrix; /* transformation matrix */ @@ -358,6 +360,16 @@ void FT2Font::set_text( if (!raqm_set_freetype_load_flags(rq, flags)) { throw std::runtime_error("failed to set text flags for layout"); } + if (languages) { + for (auto & [lang_str, start, end] : *languages) { + if (!raqm_set_language(rq, lang_str.c_str(), start, end - start)) { + throw std::runtime_error( + "failed to set language from {} "_s + "for {} characters to {!r} for layout"_s.format( + start, end, lang_str)); + } + } + } if (!raqm_layout(rq)) { throw std::runtime_error("failed to layout text"); } @@ -422,6 +434,16 @@ void FT2Font::set_text( if (!raqm_set_freetype_load_flags(rq, flags)) { throw std::runtime_error("failed to set text flags for layout"); } + if (languages) { + for (auto & [lang_str, start, end] : *languages) { + if (!raqm_set_language(rq, lang_str.c_str(), start, end - start)) { + throw std::runtime_error( + "failed to set language from {} "_s + "for {} characters to {!r} for layout"_s.format( + start, end, lang_str)); + } + } + } if (!raqm_layout(rq)) { throw std::runtime_error("failed to layout text"); } diff --git a/src/ft2font.h b/src/ft2font.h index ffaf511ab9ca..b468804b4830 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -100,6 +101,9 @@ extern FT_Library _ft2Library; class FT2Font { public: + using LanguageRange = std::tuple; + using LanguageType = std::optional>; + FT2Font(long hinting_factor, std::vector &fallback_list, bool warn_if_used); virtual ~FT2Font(); @@ -110,7 +114,7 @@ class FT2Font void set_charmap(int i); void select_charmap(unsigned long i); void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags, - std::vector &xys); + LanguageType languages, std::vector &xys); int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode); void set_kerning_factor(int factor); void load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback); diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 65fcb4b7e013..186bf7864dc2 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -693,7 +693,8 @@ const char *PyFT2Font_set_text__doc__ = R"""( static py::array_t PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0, - std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT) + std::variant flags_or_int = LoadFlags::FORCE_AUTOHINT, + std::variant languages_or_str = nullptr) { std::vector xys; LoadFlags flags; @@ -713,7 +714,21 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0 throw py::type_error("flags must be LoadFlags or int"); } - self->set_text(text, angle, static_cast(flags), xys); + 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"); + } + + self->set_text(text, angle, static_cast(flags), languages, xys); py::ssize_t dims[] = { static_cast(xys.size()) / 2, 2 }; py::array_t result(dims); @@ -1534,7 +1549,8 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used()) .def("get_kerning", &PyFT2Font_get_kerning, "left"_a, "right"_a, "mode"_a, PyFT2Font_get_kerning__doc__) .def("set_text", &PyFT2Font_set_text, - "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, + "string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT, py::kw_only(), + "language"_a=nullptr, PyFT2Font_set_text__doc__) .def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a, PyFT2Font_get_fontmap__doc__) From 6ef55edf89b0505505625a74867766334837090f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 2 Sep 2025 23:06:23 -0400 Subject: [PATCH 2/2] Update test images for language change --- .../baseline_images/test_text/language.png | Bin 0 -> 21299 bytes lib/matplotlib/tests/test_text.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_text/language.png diff --git a/lib/matplotlib/tests/baseline_images/test_text/language.png b/lib/matplotlib/tests/baseline_images/test_text/language.png new file mode 100644 index 0000000000000000000000000000000000000000..32e7f67b566431998698f40992605b5b1f06c37c GIT binary patch literal 21299 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

PvnO) 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