Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Add language parameter to Text objects #29794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
Loading
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions 7 lib/matplotlib/_text_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def warn_on_missing_glyph(codepoint, fontnames):
f"Matplotlib currently does not support {block} natively.")


def layout(string, font, *, kern_mode=Kerning.DEFAULT):
def layout(string, font, *, language=None, kern_mode=Kerning.DEFAULT):
"""
Render *string* with *font*.

Expand All @@ -56,6 +56,9 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT):
The string to be rendered.
font : FT2Font
The font.
language : str or list of tuples of (str, int, int), optional
The language of the text in a format accepted by libraqm, namely `a BCP47
language code <https://www.w3.org/International/articles/language-tags/>`_.
kern_mode : Kerning
A FreeType kerning mode.

Expand All @@ -65,7 +68,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.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, this function is getting rewritten with libraqm, so this is just a note for later.

base_font = font
for char in string:
# This has done the fallback logic
Expand Down
3 changes: 2 additions & 1 deletion 3 lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,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
Expand Down
6 changes: 4 additions & 2 deletions 6 lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2338,6 +2338,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)
Expand All @@ -2348,7 +2349,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))
Expand Down Expand Up @@ -2382,7 +2383,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, language=language,
kern_mode=Kerning.UNFITTED):
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, []))
Expand Down
3 changes: 2 additions & 1 deletion 3 lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,9 +795,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)
Expand Down
7 changes: 6 additions & 1 deletion 7 lib/matplotlib/ft2font.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,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: ...
Expand Down
21 changes: 21 additions & 0 deletions 21 lib/matplotlib/tests/test_ft2font.py
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,27 @@ def test_ft2font_set_text():
assert font.get_bitmap_offset() == (6, 0)


def test_ft2font_language_invalid():
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0)
with pytest.raises(TypeError):
font.set_text('foo', language=[1, 2, 3])
with pytest.raises(TypeError):
font.set_text('foo', language=[(1, 2)])
with pytest.raises(TypeError):
font.set_text('foo', language=[('en', 'foo', 2)])
with pytest.raises(TypeError):
font.set_text('foo', language=[('en', 1, 'foo')])


def test_ft2font_language():
file = fm.findfont('DejaVu Sans')
font = ft2font.FT2Font(file, hinting_factor=1, _kerning_factor=0)
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, _kerning_factor=0)
Expand Down
16 changes: 16 additions & 0 deletions 16 lib/matplotlib/tests/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -1190,3 +1190,19 @@ def test_ytick_rotation_mode():
tick.set_rotation(angle)

plt.subplots_adjust(left=0.4, right=0.6, top=.99, bottom=.01)


def test_text_language_invalid():
with pytest.raises(TypeError, match='must be list of tuple'):
Text(0, 0, 'foo', language=[1, 2, 3])
with pytest.raises(TypeError, match='must be list of tuple'):
Text(0, 0, 'foo', language=[(1, 2)])
with pytest.raises(TypeError, match='start location must be int'):
Text(0, 0, 'foo', language=[('en', 'foo', 2)])
with pytest.raises(TypeError, match='end location must be int'):
Text(0, 0, 'foo', language=[('en', 1, 'foo')])


def test_text_language():
Text(0, 0, 'foo', language='en')
Text(0, 0, 'foo', language=[('en', 1, 2)])
32 changes: 32 additions & 0 deletions 32 lib/matplotlib/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
super().__init__()
self._x, self._y = x, y
self._text = ''
self._language = None
self._reset_visual_defaults(
text=text,
color=color,
Expand Down Expand Up @@ -1422,6 +1423,37 @@
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 list[tuple[str, int, int]]
The language of the text in a format accepted by libraqm, namely `a BCP47
language code <https://www.w3.org/International/articles/language-tags/>`_.
"""
_api.check_isinstance((list, str, None), language=language)
if isinstance(language, list):
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(

Check warning on line 1447 in lib/matplotlib/text.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/text.py#L1447

Added line #L1447 was not covered by tests
'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`."""
Expand Down
2 changes: 2 additions & 0 deletions 2 lib/matplotlib/text.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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 | list[tuple[str, int, int]] | None: ...
def set_language(self, language: str | list[tuple[str, int, int]] | None) -> None: ...

class OffsetFrom:
def __init__(
Expand Down
12 changes: 8 additions & 4 deletions 12 lib/matplotlib/textpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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 or list of tuples of (str, int, int), optional
The language of the text in a format accepted by libraqm, namely `a BCP47
language code <https://www.w3.org/International/articles/language-tags/>`_.

Returns
-------
Expand Down Expand Up @@ -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)

Expand All @@ -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.
"""
Expand All @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion 5 lib/matplotlib/textpath.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ 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,
font: FT2Font,
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]],
Expand Down
3 changes: 2 additions & 1 deletion 3 src/ft2font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ void FT2Font::set_kerning_factor(int factor)
}

void FT2Font::set_text(
std::u32string_view text, double angle, FT_Int32 flags, std::vector<double> &xys)
std::u32string_view text, double angle, FT_Int32 flags, LanguageType languages,
std::vector<double> &xys)
{
FT_Matrix matrix; /* transformation matrix */

Expand Down
6 changes: 5 additions & 1 deletion 6 src/ft2font.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#ifndef MPL_FT2FONT_H
#define MPL_FT2FONT_H

#include <optional>
#include <set>
#include <string>
#include <string_view>
Expand Down Expand Up @@ -70,6 +71,9 @@ class FT2Font
typedef void (*WarnFunc)(FT_ULong charcode, std::set<FT_String*> family_names);

public:
using LanguageRange = std::tuple<std::string, int, int>;
using LanguageType = std::optional<std::vector<LanguageRange>>;

FT2Font(FT_Open_Args &open_args, long hinting_factor,
std::vector<FT2Font *> &fallback_list,
WarnFunc warn, bool warn_if_used);
Expand All @@ -79,7 +83,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<double> &xys);
LanguageType languages, std::vector<double> &xys);
int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, bool fallback);
int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, FT_Vector &delta);
void set_kerning_factor(int factor);
Expand Down
22 changes: 19 additions & 3 deletions 22 src/ft2font_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,8 @@

static py::array_t<double>
PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0,
std::variant<LoadFlags, FT_Int32> flags_or_int = LoadFlags::FORCE_AUTOHINT)
std::variant<LoadFlags, FT_Int32> flags_or_int = LoadFlags::FORCE_AUTOHINT,
std::variant<FT2Font::LanguageType, std::string> languages_or_str = nullptr)
{
std::vector<double> xys;
LoadFlags flags;
Expand All @@ -732,7 +733,21 @@
throw py::type_error("flags must be LoadFlags or int");
}

self->x->set_text(text, angle, static_cast<FT_Int32>(flags), xys);
FT2Font::LanguageType languages;
if (auto value = std::get_if<FT2Font::LanguageType>(&languages_or_str)) {
languages = std::move(*value);
} else if (auto value = std::get_if<std::string>(&languages_or_str)) {
languages = std::vector<FT2Font::LanguageRange>{
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");

Check warning on line 747 in src/ft2font_wrapper.cpp

View check run for this annotation

Codecov / codecov/patch

src/ft2font_wrapper.cpp#L747

Added line #L747 was not covered by tests
}

self->x->set_text(text, angle, static_cast<FT_Int32>(flags), languages, xys);

py::ssize_t dims[] = { static_cast<py::ssize_t>(xys.size()) / 2, 2 };
py::array_t<double> result(dims);
Expand Down Expand Up @@ -1621,7 +1636,8 @@
.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__)
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.