Skip to content

Commit 161e393

Browse files
committed
Add language parameter to Text objects
1 parent f017a0e commit 161e393

File tree

10 files changed

+81
-14
lines changed

10 files changed

+81
-14
lines changed

lib/matplotlib/_text_helpers.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def warn_on_missing_glyph(codepoint, fontnames):
4343
f"Matplotlib currently does not support {block} natively.")
4444

4545

46-
def layout(string, font, *, kern_mode=Kerning.DEFAULT):
46+
def layout(string, font, language, *, kern_mode=Kerning.DEFAULT):
4747
"""
4848
Render *string* with *font*.
4949
@@ -65,7 +65,7 @@ def layout(string, font, *, kern_mode=Kerning.DEFAULT):
6565
"""
6666
x = 0
6767
prev_glyph_idx = None
68-
char_to_font = font._get_fontmap(string)
68+
char_to_font = font._get_fontmap(string) # TODO: Pass in language.
6969
base_font = font
7070
for char in string:
7171
# This has done the fallback logic

lib/matplotlib/backends/backend_agg.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
189189
font = self._prepare_font(prop)
190190
# We pass '0' for angle here, since it will be rotated (in raster
191191
# space) in the following call to draw_text_image).
192-
font.set_text(s, 0, flags=get_hinting_flag())
192+
font.set_text(s, 0, flags=get_hinting_flag(),
193+
language=mtext.get_language() if mtext is not None else None)
193194
font.draw_glyphs_to_bitmap(
194195
antialiased=gc.get_antialiased())
195196
d = font.get_descent() / 64.0

lib/matplotlib/backends/backend_pdf.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -2345,6 +2345,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23452345
return self.draw_mathtext(gc, x, y, s, prop, angle)
23462346

23472347
fontsize = prop.get_size_in_points()
2348+
language = mtext.get_language() if mtext is not None else None
23482349

23492350
if mpl.rcParams['pdf.use14corefonts']:
23502351
font = self._get_font_afm(prop)
@@ -2355,7 +2356,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23552356
fonttype = mpl.rcParams['pdf.fonttype']
23562357

23572358
if gc.get_url() is not None:
2358-
font.set_text(s)
2359+
font.set_text(s, language=language)
23592360
width, height = font.get_width_height()
23602361
self.file._annotations[-1][1].append(_get_link_annotation(
23612362
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):
23892390
multibyte_glyphs = []
23902391
prev_was_multibyte = True
23912392
prev_font = font
2392-
for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED):
2393+
for item in _text_helpers.layout(s, font, language,
2394+
kern_mode=Kerning.UNFITTED):
23932395
if _font_supports_glyph(fonttype, ord(item.char)):
23942396
if prev_was_multibyte or item.ft_object != prev_font:
23952397
singlebyte_chunks.append((item.ft_object, item.x, []))

lib/matplotlib/backends/backend_ps.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -795,9 +795,10 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
795795
thisx += width * scale
796796

797797
else:
798+
language = mtext.get_language() if mtext is not None else None
798799
font = self._get_font_ttf(prop)
799800
self._character_tracker.track(font, s)
800-
for item in _text_helpers.layout(s, font):
801+
for item in _text_helpers.layout(s, font, language):
801802
ps_name = (item.ft_object.postscript_name
802803
.encode("ascii", "replace").decode("ascii"))
803804
glyph_name = item.ft_object.get_glyph_name(item.glyph_idx)

lib/matplotlib/text.py

+31
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def __init__(self,
136136
super().__init__()
137137
self._x, self._y = x, y
138138
self._text = ''
139+
self._language = None
139140
self._reset_visual_defaults(
140141
text=text,
141142
color=color,
@@ -1422,6 +1423,36 @@ def _va_for_angle(self, angle):
14221423
return 'baseline' if anchor_at_left else 'top'
14231424
return 'top' if anchor_at_left else 'baseline'
14241425

1426+
def get_language(self):
1427+
"""Return the language this Text is in."""
1428+
return self._language
1429+
1430+
def set_language(self, language):
1431+
"""
1432+
Set the language of the text.
1433+
1434+
Parameters
1435+
----------
1436+
language : str or list[tuple[str, int, int]]
1437+
1438+
"""
1439+
_api.check_isinstance((list, str, None), language=language)
1440+
if isinstance(language, list):
1441+
for val in language:
1442+
if not isinstance(val, tuple) or len(val) != 3:
1443+
raise ValueError('language must be list of tuple, not {language!r}')
1444+
sublang, start, end = val
1445+
if not isinstance(sublang, str):
1446+
raise ValueError(
1447+
'sub-language specification must be str, not {sublang!r}')
1448+
if not isinstance(start, int):
1449+
raise ValueError('start location must be int, not {start!r}')
1450+
if not isinstance(end, int):
1451+
raise ValueError('end location must be int, not {end!r}')
1452+
1453+
self._language = language
1454+
self.stale = True
1455+
14251456

14261457
class OffsetFrom:
14271458
"""Callable helper class for working with `Annotation`."""

lib/matplotlib/text.pyi

+2
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ class Text(Artist):
108108
def set_antialiased(self, antialiased: bool) -> None: ...
109109
def _ha_for_angle(self, angle: Any) -> Literal['center', 'right', 'left'] | None: ...
110110
def _va_for_angle(self, angle: Any) -> Literal['center', 'top', 'baseline'] | None: ...
111+
def get_language(self) -> str | list[tuple[str, int, int]] | None: ...
112+
def set_language(self, language: str | list[tuple[str, int, int]] | None) -> None: ...
111113

112114
class OffsetFrom:
113115
def __init__(

lib/matplotlib/textpath.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def get_text_width_height_descent(self, s, prop, ismath):
6969
d /= 64.0
7070
return w * scale, h * scale, d * scale
7171

72-
def get_text_path(self, prop, s, ismath=False):
72+
def get_text_path(self, prop, s, ismath=False, language=None):
7373
"""
7474
Convert text *s* to path (a tuple of vertices and codes for
7575
matplotlib.path.Path).
@@ -109,7 +109,8 @@ def get_text_path(self, prop, s, ismath=False):
109109
glyph_info, glyph_map, rects = self.get_glyphs_tex(prop, s)
110110
elif not ismath:
111111
font = self._get_font(prop)
112-
glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s)
112+
glyph_info, glyph_map, rects = self.get_glyphs_with_font(font, s,
113+
language=language)
113114
else:
114115
glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s)
115116

@@ -130,7 +131,7 @@ def get_text_path(self, prop, s, ismath=False):
130131
return verts, codes
131132

132133
def get_glyphs_with_font(self, font, s, glyph_map=None,
133-
return_new_glyphs_only=False):
134+
return_new_glyphs_only=False, language=None):
134135
"""
135136
Convert string *s* to vertices and codes using the provided ttf font.
136137
"""
@@ -145,7 +146,7 @@ def get_glyphs_with_font(self, font, s, glyph_map=None,
145146

146147
xpositions = []
147148
glyph_ids = []
148-
for item in _text_helpers.layout(s, font):
149+
for item in _text_helpers.layout(s, font, language):
149150
char_id = self._get_char_id(item.ft_object, ord(item.char))
150151
glyph_ids.append(char_id)
151152
xpositions.append(item.x)

src/ft2font.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,8 @@ void FT2Font::set_kerning_factor(int factor)
399399
}
400400

401401
void FT2Font::set_text(
402-
std::u32string_view text, double angle, FT_Int32 flags, std::vector<double> &xys)
402+
std::u32string_view text, double angle, FT_Int32 flags, LanguageType languages,
403+
std::vector<double> &xys)
403404
{
404405
FT_Matrix matrix; /* transformation matrix */
405406

src/ft2font.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#ifndef MPL_FT2FONT_H
77
#define MPL_FT2FONT_H
88

9+
#include <optional>
910
#include <set>
1011
#include <string>
1112
#include <string_view>
@@ -70,6 +71,9 @@ class FT2Font
7071
typedef void (*WarnFunc)(FT_ULong charcode, std::set<FT_String*> family_names);
7172

7273
public:
74+
using LanguageRange = std::tuple<std::string, int, int>;
75+
using LanguageType = std::optional<std::vector<LanguageRange>>;
76+
7377
FT2Font(FT_Open_Args &open_args, long hinting_factor,
7478
std::vector<FT2Font *> &fallback_list, WarnFunc warn);
7579
virtual ~FT2Font();
@@ -78,7 +82,7 @@ class FT2Font
7882
void set_charmap(int i);
7983
void select_charmap(unsigned long i);
8084
void set_text(std::u32string_view codepoints, double angle, FT_Int32 flags,
81-
std::vector<double> &xys);
85+
LanguageType languages, std::vector<double> &xys);
8286
int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, bool fallback);
8387
int get_kerning(FT_UInt left, FT_UInt right, FT_Kerning_Mode mode, FT_Vector &delta);
8488
void set_kerning_factor(int factor);

src/ft2font_wrapper.cpp

+26-2
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,8 @@ const char *PyFT2Font_set_text__doc__ = R"""(
705705

706706
static py::array_t<double>
707707
PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0,
708-
std::variant<LoadFlags, FT_Int32> flags_or_int = LoadFlags::FORCE_AUTOHINT)
708+
std::variant<LoadFlags, FT_Int32> flags_or_int = LoadFlags::FORCE_AUTOHINT,
709+
py::object language_obj = py::none())
709710
{
710711
std::vector<double> xys;
711712
LoadFlags flags;
@@ -725,7 +726,29 @@ PyFT2Font_set_text(PyFT2Font *self, std::u32string_view text, double angle = 0.0
725726
throw py::type_error("flags must be LoadFlags or int");
726727
}
727728

728-
self->x->set_text(text, angle, static_cast<FT_Int32>(flags), xys);
729+
FT2Font::LanguageType languages;
730+
if (py::isinstance<std::string>(language_obj)) {
731+
languages = std::vector<FT2Font::LanguageRange>{
732+
FT2Font::LanguageRange{language_obj.cast<std::string>(), 0, text.size()}
733+
};
734+
} else if (py::isinstance<py::list>(language_obj)) {
735+
languages = std::vector<FT2Font::LanguageRange>{};
736+
737+
for (py::handle lang_range_obj : language_obj.cast<py::list>()) {
738+
if (!py::isinstance<py::tuple>(lang_range_obj)) {
739+
throw py::type_error("languages must be str or list of tuple");
740+
}
741+
742+
auto lang_range = lang_range_obj.cast<py::tuple>();
743+
auto lang_str = lang_range[0].cast<std::string>();
744+
auto start = lang_range[1].cast<size_t>();
745+
auto end = lang_range[2].cast<size_t>();
746+
747+
languages->emplace_back(lang_str, start, end);
748+
}
749+
}
750+
751+
self->x->set_text(text, angle, static_cast<FT_Int32>(flags), languages, xys);
729752

730753
py::ssize_t dims[] = { static_cast<py::ssize_t>(xys.size()) / 2, 2 };
731754
py::array_t<double> result(dims);
@@ -1752,6 +1775,7 @@ PYBIND11_MODULE(ft2font, m, py::mod_gil_not_used())
17521775
PyFT2Font_get_kerning__doc__)
17531776
.def("set_text", &PyFT2Font_set_text,
17541777
"string"_a, "angle"_a=0.0, "flags"_a=LoadFlags::FORCE_AUTOHINT,
1778+
"language"_a=py::none(),
17551779
PyFT2Font_set_text__doc__)
17561780
.def("_get_fontmap", &PyFT2Font_get_fontmap, "string"_a,
17571781
PyFT2Font_get_fontmap__doc__)

0 commit comments

Comments
 (0)