From c4e501adfcbbe637032419abebed0b1cccd26e39 Mon Sep 17 00:00:00 2001 From: Syamjith NK Date: Sun, 30 Aug 2026 16:06:53 +0400 Subject: [PATCH 1/3] Doc: note that Matplotlib shapes complex scripts itself since 3.11 The arabic_reshaper + python-bidi pre-processing that was the standard workaround before shaping landed now renders labels backwards, and the text documentation did not mention shaping at all. --- galleries/users_explain/text/fonts.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/galleries/users_explain/text/fonts.py b/galleries/users_explain/text/fonts.py index 40cc9eaa93eb..3566c0ec50d4 100644 --- a/galleries/users_explain/text/fonts.py +++ b/galleries/users_explain/text/fonts.py @@ -183,4 +183,27 @@ A majority of this work was done by Aitik Gupta supported by Google Summer of Code 2021. + +Text shaping +------------ + +Some scripts cannot be drawn by taking a string one character at a time. Arabic +letters change shape depending on their neighbours, and right-to-left text is +stored in logical order but drawn in visual order. As of Matplotlib 3.11 this is +handled for you: FreeType is driven through libraqm, which uses HarfBuzz to shape +the string and a bidi implementation to reorder it. + +Pass the logical string, the one you would read aloud:: + + ax.set_title("الإمارات") + +Before 3.11 no shaping was done, and the usual workaround was to pre-process the +string with ``arabic_reshaper`` and ``python-bidi``:: + + ax.set_title(get_display(arabic_reshaper.reshape("الإمارات"))) + +That pre-processing must now be removed. The string it produces is already in +visual order, so Matplotlib reorders it a second time and the label is drawn +backwards. Nothing is raised, and the output still looks like Arabic to a reader +who does not read Arabic. """ # noqa: E501 From 4441b4c0d984d005df970b9784ea5e297aa79a27 Mon Sep 17 00:00:00 2001 From: Syamjith NK Date: Tue, 1 Sep 2026 11:12:53 +0400 Subject: [PATCH 2/3] Doc: tell 3.11 upgraders to remove the pre-shaping workaround Moves the note into an admonition on the 3.11 change note, per review, and covers the three cases: pin to 3.11, version-gate, or wrap the pre-processed string in LRO/PDF when the call cannot be changed. --- .../prev_whats_new/whats_new_3.11.0.rst | 29 +++++++++++++++++++ galleries/users_explain/text/fonts.py | 23 --------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/doc/release/prev_whats_new/whats_new_3.11.0.rst b/doc/release/prev_whats_new/whats_new_3.11.0.rst index 95c9f8313873..1829280b07ef 100644 --- a/doc/release/prev_whats_new/whats_new_3.11.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.11.0.rst @@ -774,6 +774,35 @@ Text support has been extended to include complex text layout. This support incl Note, all advanced features require corresponding font support, and may require additional fonts over the builtin DejaVu Sans. +.. admonition:: Remove any pre-shaping workaround + :class: important + + Because Matplotlib did not previously reorder text, the usual workaround for + Arabic, Persian, Urdu and Hebrew was to reorder the string before passing it in, + typically with ``arabic_reshaper`` and ``python-bidi``:: + + ax.set_title(get_display(arabic_reshaper.reshape(text))) + + Matplotlib now reorders the string itself, so a string that arrives already in + visual order is reordered a second time and is drawn backwards. Nothing is + raised, and to a reader who does not read the script the result still looks + like correct text, so this is easy to ship without noticing. + + - If you can require Matplotlib 3.11, pass the logical string and delete the + pre-processing. + - If you support older versions as well, branch on the Matplotlib version and + pre-process only on the older one. + - If you cannot change the call at all, because the text is handed to a + third-party library that calls Matplotlib for you, wrap the pre-processed + string in ``LEFT-TO-RIGHT OVERRIDE`` and ``POP DIRECTIONAL FORMATTING``:: + + text = ('\N{LEFT-TO-RIGHT OVERRIDE}' + preprocessed + + '\N{POP DIRECTIONAL FORMATTING}') + + That reads correctly on every version. It does not always render + identically, because it draws the font's presentation-form glyphs rather + than the font's own shaping, and some fonts space those differently. + Specifying font feature tags ---------------------------- diff --git a/galleries/users_explain/text/fonts.py b/galleries/users_explain/text/fonts.py index 3566c0ec50d4..40cc9eaa93eb 100644 --- a/galleries/users_explain/text/fonts.py +++ b/galleries/users_explain/text/fonts.py @@ -183,27 +183,4 @@ A majority of this work was done by Aitik Gupta supported by Google Summer of Code 2021. - -Text shaping ------------- - -Some scripts cannot be drawn by taking a string one character at a time. Arabic -letters change shape depending on their neighbours, and right-to-left text is -stored in logical order but drawn in visual order. As of Matplotlib 3.11 this is -handled for you: FreeType is driven through libraqm, which uses HarfBuzz to shape -the string and a bidi implementation to reorder it. - -Pass the logical string, the one you would read aloud:: - - ax.set_title("الإمارات") - -Before 3.11 no shaping was done, and the usual workaround was to pre-process the -string with ``arabic_reshaper`` and ``python-bidi``:: - - ax.set_title(get_display(arabic_reshaper.reshape("الإمارات"))) - -That pre-processing must now be removed. The string it produces is already in -visual order, so Matplotlib reorders it a second time and the label is drawn -backwards. Nothing is raised, and the output still looks like Arabic to a reader -who does not read Arabic. """ # noqa: E501 From 7a27c6281a8b004a656d8e67661a540cf377cc01 Mon Sep 17 00:00:00 2001 From: Syamjith NK Date: Fri, 4 Sep 2026 16:44:47 +0400 Subject: [PATCH 3/3] Doc: name the pre-processed string so the override snippet resolves Apply QuLogic's review suggestion. The later LEFT-TO-RIGHT OVERRIDE example already referred to `preprocessed`, but the line above inlined the expression and never bound that name, so the two snippets did not read as the same value. --- doc/release/prev_whats_new/whats_new_3.11.0.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/release/prev_whats_new/whats_new_3.11.0.rst b/doc/release/prev_whats_new/whats_new_3.11.0.rst index 1829280b07ef..0ff9fea3b998 100644 --- a/doc/release/prev_whats_new/whats_new_3.11.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.11.0.rst @@ -781,7 +781,8 @@ additional fonts over the builtin DejaVu Sans. Arabic, Persian, Urdu and Hebrew was to reorder the string before passing it in, typically with ``arabic_reshaper`` and ``python-bidi``:: - ax.set_title(get_display(arabic_reshaper.reshape(text))) + preprocessed = get_display(arabic_reshaper.reshape(text)) + ax.set_title(preprocessed) Matplotlib now reorders the string itself, so a string that arrives already in visual order is reordered a second time and is drawn backwards. Nothing is