Skip to content

Commit ffd43e9

Browse files
committed
Add language parameter to Text objects
1 parent b9e94e4 commit ffd43e9

16 files changed

+123
-33
lines changed

lib/matplotlib/_text_helpers.py

Lines changed: 2 additions & 2 deletions
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/backend_bases.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,8 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
494494
"""
495495
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")
496496

497-
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
497+
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None,
498+
language=None):
498499
"""
499500
Draw a text instance.
500501
@@ -516,6 +517,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
516517
If True, use mathtext parser.
517518
mtext : `~matplotlib.text.Text`
518519
The original text object to be rendered.
520+
language : str or list[tuple[str, int, int]]
521+
The language of the text.
519522
520523
Notes
521524
-----

lib/matplotlib/backend_bases.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class RendererBase:
116116
angle: float,
117117
ismath: bool | Literal["TeX"] = ...,
118118
mtext: Text | None = ...,
119+
language: str | list[tuple[str, int, int]] = ...,
119120
) -> None: ...
120121
def get_text_width_height_descent(
121122
self, s: str, prop: FontProperties, ismath: bool | Literal["TeX"]

lib/matplotlib/backends/backend_agg.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,15 @@ def draw_mathtext(self, gc, x, y, s, prop, angle):
182182
y = round(y - oy + yd)
183183
self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)
184184

185-
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
185+
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None,
186+
language=None):
186187
# docstring inherited
187188
if ismath:
188189
return self.draw_mathtext(gc, x, y, s, prop, angle)
189190
font = self._prepare_font(prop)
190191
# We pass '0' for angle here, since it will be rotated (in raster
191192
# space) in the following call to draw_text_image).
192-
font.set_text(s, 0, flags=get_hinting_flag())
193+
font.set_text(s, 0, flags=get_hinting_flag(), language=language)
193194
font.draw_glyphs_to_bitmap(
194195
antialiased=gc.get_antialiased())
195196
d = font.get_descent() / 64.0
@@ -203,7 +204,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
203204
y = round(y + yo + yd)
204205
self._renderer.draw_text_image(font, x, y + 1, angle, gc)
205206

206-
def get_text_width_height_descent(self, s, prop, ismath):
207+
def get_text_width_height_descent(self, s, prop, ismath, language=None):
207208
# docstring inherited
208209

209210
_api.check_in_list(["TeX", True, False], ismath=ismath)
@@ -216,7 +217,7 @@ def get_text_width_height_descent(self, s, prop, ismath):
216217
return width, height, descent
217218

218219
font = self._prepare_font(prop)
219-
font.set_text(s, 0.0, flags=get_hinting_flag())
220+
font.set_text(s, 0.0, flags=get_hinting_flag(), language=language)
220221
w, h = font.get_width_height() # width and height of unrotated string
221222
d = font.get_descent()
222223
w /= 64.0 # convert from subpixels

lib/matplotlib/backends/backend_cairo.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ def draw_image(self, gc, x, y, im):
214214
ctx.paint()
215215
ctx.restore()
216216

217-
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
217+
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None,
218+
language=None): # TODO: Apply language tag, if possible.
218219
# docstring inherited
219220

220221
# Note: (x, y) are device/display coords, not user-coords, unlike other

lib/matplotlib/backends/backend_pdf.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2327,7 +2327,8 @@ def encode_string(self, s, fonttype):
23272327
return s.encode('cp1252', 'replace')
23282328
return s.encode('utf-16be', 'replace')
23292329

2330-
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
2330+
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None,
2331+
language=None):
23312332
# docstring inherited
23322333

23332334
# TODO: combine consecutive texts into one BT/ET delimited section
@@ -2347,7 +2348,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23472348
fonttype = mpl.rcParams['pdf.fonttype']
23482349

23492350
if gc.get_url() is not None:
2350-
font.set_text(s)
2351+
font.set_text(s, language=language)
23512352
width, height = font.get_width_height()
23522353
self.file._annotations[-1][1].append(_get_link_annotation(
23532354
gc, x, y, width / 64, height / 64, angle))
@@ -2381,7 +2382,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
23812382
multibyte_glyphs = []
23822383
prev_was_multibyte = True
23832384
prev_font = font
2384-
for item in _text_helpers.layout(s, font, kern_mode=Kerning.UNFITTED):
2385+
for item in _text_helpers.layout(s, font, language,
2386+
kern_mode=Kerning.UNFITTED):
23852387
if _font_supports_glyph(fonttype, ord(item.char)):
23862388
if prev_was_multibyte or item.ft_object != prev_font:
23872389
singlebyte_chunks.append((item.ft_object, item.x, []))

lib/matplotlib/backends/backend_pgf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,8 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
673673
# docstring inherited
674674
self.draw_text(gc, x, y, s, prop, angle, ismath="TeX", mtext=mtext)
675675

676-
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
676+
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None,
677+
language=None): # TODO: Implement language if possible.
677678
# docstring inherited
678679

679680
# prepare string for tex

lib/matplotlib/backends/backend_ps.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -758,7 +758,8 @@ def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
758758
self.textcnt += 1
759759

760760
@_log_if_debug_on
761-
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
761+
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None,
762+
language=None):
762763
# docstring inherited
763764

764765
if self._is_transparent(gc.get_rgb()):
@@ -795,7 +796,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
795796
else:
796797
font = self._get_font_ttf(prop)
797798
self._character_tracker.track(font, s)
798-
for item in _text_helpers.layout(s, font):
799+
for item in _text_helpers.layout(s, font, language):
799800
ps_name = (item.ft_object.postscript_name
800801
.encode("ascii", "replace").decode("ascii"))
801802
glyph_name = item.ft_object.get_glyph_name(item.glyph_idx)

lib/matplotlib/backends/backend_svg.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,7 +1035,8 @@ def _update_glyph_map_defs(self, glyph_map_new):
10351035
def _adjust_char_id(self, char_id):
10361036
return char_id.replace("%20", "_")
10371037

1038-
def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None):
1038+
def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None,
1039+
language=None):
10391040
# docstring inherited
10401041
writer = self.writer
10411042

@@ -1106,7 +1107,8 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None):
11061107

11071108
writer.end('g')
11081109

1109-
def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None):
1110+
def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None,
1111+
language=None):
11101112
# NOTE: If you change the font styling CSS, then be sure the check for
11111113
# svg.fonttype = none in `lib/matplotlib/testing/compare.py::convert` remains in
11121114
# sync. Also be sure to re-generate any SVG using this mode, or else such tests
@@ -1263,7 +1265,8 @@ def _get_all_quoted_names(prop):
12631265

12641266
writer.end('g')
12651267

1266-
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
1268+
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None,
1269+
language=None):
12671270
# docstring inherited
12681271

12691272
clip_attrs = self._get_clip_attrs(gc)
@@ -1276,9 +1279,9 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
12761279
self.writer.start('a', {'xlink:href': gc.get_url()})
12771280

12781281
if mpl.rcParams['svg.fonttype'] == 'path':
1279-
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext)
1282+
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext, language)
12801283
else:
1281-
self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext)
1284+
self._draw_text_as_text(gc, x, y, s, prop, angle, ismath, mtext, language)
12821285

12831286
if gc.get_url() is not None:
12841287
self.writer.end('a')

lib/matplotlib/backends/backend_template.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ def draw_path(self, gc, path, transform, rgbFace=None):
7979
def draw_image(self, gc, x, y, im):
8080
pass
8181

82-
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
82+
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None,
83+
language=None):
8384
pass
8485

8586
def flipy(self):

lib/matplotlib/text.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
"""
44

55
import functools
6+
import inspect
67
import logging
78
import math
89
from numbers import Real
10+
import warnings
911
import weakref
1012

1113
import numpy as np
@@ -136,6 +138,7 @@ def __init__(self,
136138
super().__init__()
137139
self._x, self._y = x, y
138140
self._text = ''
141+
self._language = None
139142
self._reset_visual_defaults(
140143
text=text,
141144
color=color,
@@ -791,6 +794,7 @@ def draw(self, renderer):
791794

792795
angle = self.get_rotation()
793796

797+
lang = self._language # TODO: Split this by line.
794798
for line, wh, x, y in info:
795799

796800
mtext = self if len(info) == 1 else None
@@ -812,9 +816,19 @@ def draw(self, renderer):
812816
self._fontproperties, angle,
813817
mtext=mtext)
814818
else:
815-
textrenderer.draw_text(gc, x, y, clean_line,
816-
self._fontproperties, angle,
817-
ismath=ismath, mtext=mtext)
819+
params = inspect.signature(textrenderer.draw_text).parameters
820+
if 'language' in params:
821+
textrenderer.draw_text(gc, x, y, clean_line,
822+
self._fontproperties, angle,
823+
ismath=ismath, mtext=mtext,
824+
language=lang)
825+
else:
826+
warnings.warn(
827+
f'{textrenderer.__class__.__name__} missing language '
828+
f'parameter to draw_text method')
829+
textrenderer.draw_text(gc, x, y, clean_line,
830+
self._fontproperties, angle,
831+
ismath=ismath, mtext=mtext)
818832

819833
gc.restore()
820834
renderer.close_group('text')
@@ -1410,6 +1424,36 @@ def _va_for_angle(self, angle):
14101424
return 'baseline' if anchor_at_left else 'top'
14111425
return 'top' if anchor_at_left else 'baseline'
14121426

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

14141458
class OffsetFrom:
14151459
"""Callable helper class for working with `Annotation`."""

lib/matplotlib/text.pyi

Lines changed: 2 additions & 0 deletions
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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def _get_char_id(self, font, ccode):
4545
"""
4646
return urllib.parse.quote(f"{font.postscript_name}-{ccode:x}")
4747

48-
def get_text_width_height_descent(self, s, prop, ismath):
48+
def get_text_width_height_descent(self, s, prop, ismath, language=None):
4949
fontsize = prop.get_size_in_points()
5050

5151
if ismath == "TeX":
@@ -61,15 +61,15 @@ def get_text_width_height_descent(self, s, prop, ismath):
6161
return width * scale, height * scale, descent * scale
6262

6363
font = self._get_font(prop)
64-
font.set_text(s, 0.0, flags=LoadFlags.NO_HINTING)
64+
font.set_text(s, 0.0, flags=LoadFlags.NO_HINTING, language=language)
6565
w, h = font.get_width_height()
6666
w /= 64.0 # convert from subpixels
6767
h /= 64.0
6868
d = font.get_descent()
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 5 additions & 1 deletion
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);

0 commit comments

Comments
 (0)