Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Doc: tell 3.11 upgraders to remove the pre-shaping workaround - #32263

Merged
ksunden merged 3 commits into
matplotlib:mainfrom
Syamjith-NK:docs/text-shaping-note
Sep 4, 2026
Merged

Doc: tell 3.11 upgraders to remove the pre-shaping workaround#32263
ksunden merged 3 commits into
matplotlib:mainfrom
Syamjith-NK:docs/text-shaping-note

Conversation

@Syamjith-NK

@Syamjith-NK Syamjith-NK commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Closes #32262.

Reworked per review. The note is now an admonition on the 3.11 change note rather than a new section in the fonts guide, since that is where someone upgrading will meet it, and the section I had added to fonts.py is reverted.

It covers @tacaswell's three cases: require 3.11 and delete the pre-processing, version-gate it, or wrap the pre-processed string in LRO/PDF when you do not control the call.

On that third case, I verified the LRO/PDF suggestion against Arabic since it was raised as unverified. It reads correctly in every string I tried, including with a lam-alef ligature and with Latin digits mixed in. It does not always render identically, because it draws the font's presentation-form glyphs rather than the font's own shaping: identical with Arial Unicode, a small metric difference with SF Arabic, visibly looser joins with Geeza Pro. The wording reflects that distinction. Numbers are in the thread.

Happy to add a dedicated right-to-left section to the text docs as a follow-up if @story645 still wants one. I left it out here to keep this reviewable.

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.
@github-actions github-actions Bot added the Documentation: user guide files in galleries/users_explain or doc/users label Aug 30, 2026
@github-actions

Copy link
Copy Markdown

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a week or so, please leave a new comment below and that should bring it to our attention. Most of our reviewers are volunteers and sometimes things fall through the cracks. We also ask that you please finish addressing any review comments on this PR and wait for it to be merged (or closed) before opening a new one, as it can be a valuable learning experience to go through the review process.

You can also join us on discourse chat for real-time discussion.

For details on testing, writing docs, and our review process, please see the developer guide.
Please let us know if (and how) you use AI, it will help us give you better feedback on your PR.

We strive to be a welcoming and open project. Please follow our Code of Conduct.

@Syamjith-NK

Copy link
Copy Markdown
Contributor Author

Answering the AI question in the template, since it was asked directly.

The finding is mine. I hit it building something in Arabic that looked wrong to me, then measured it against a Pillow/Raqm render of the same string in the same font, and wrote the reproducer in #32262, which runs on my own machine.

I used an AI assistant to help draft and tighten the wording of this section, and to check that the module docstring still parsed as RST before I pushed. The technical claims I checked myself, including reading extern/meson.build to confirm libraqm is a required dependency rather than assuming it, since that is what makes this unconditional here and not in Pillow.

Happy to rewrite any of the prose if it does not sound like the rest of the page.

@tacaswell

Copy link
Copy Markdown
Member

To verify there is no generic way to determine if an Arabic, Hebrew, or Devanagari string is pre-ordered from logical to display order? How big of a problem/common is it that applications have pre-reversed strings kicking around?

Per https://www.w3.org/International/questions/qa-bidi-unicode-controls.en.html#basedirection if the user wraps pre-reversed text in the LRO/PDF unicode points the text will render correctly in all versions of mpl (because <3.11 we ignore the bidirectional details all together and >= 3.11 we (via libraqm) respect them correctly).

import matplotlib
import matplotlib.pyplot as plt

# editors / GH will correctly render RTL so use \N to be clear
# hello = "שלום"
hello = "\N{HEBREW LETTER SHIN}\N{HEBREW LETTER LAMED}\N{HEBREW LETTER VAV}\N{HEBREW LETTER FINAL MEM}"
hello_r = hello[::-1]
hello_fixed = "\N{LRO}" + hello_r + "\N{PDF}"

fig, ax = plt.subplots()
ax.set_title(f'{matplotlib.__version__=}')

ax.text(0, .7, "mpl direct")
ax.text(.5, .7, hello, fontsize=30)

ax.text(0, .5, "pre-flipped")
ax.text(.5, .5, hello_r, fontsize=30)

ax.text(0, .25, "pre-flipped + LRO/PDF")
ax.text(.5, .25, hello_fixed, fontsize=30)

plt.show()
fig.savefig(f'/tmp/bidi_mpl{matplotlib.__version__.replace('.', '-')}.png')
bidi_mpl3-10-8 bidi_mpl3-11-1

@tacaswell

Copy link
Copy Markdown
Member

I think this works correctly with Arabic as well but I do not know enough Arabic to be 100% confident it is correct.

from bidi import get_display
import arabic_reshaper
import matplotlib
import matplotlib.pyplot as plt

# The Emirates 
word = "الإمارات"


word_pre =get_display(arabic_reshaper.reshape(word))
word_pre_fixed = "\N{LRO}" + word_pre + "\N{PDF}"

fig, ax = plt.subplots()
ax.set_title(f'{matplotlib.__version__=}')

ax.text(0, .9, "mpl direct")
ax.text(.5, .9, word, fontsize=30)
ax.text(0, .7, "pre-flipped")
ax.text(.5, .7, word[::-1], fontsize=30)
ax.text(0, .5, "pre-shaped")
ax.text(.5, .5, word_pre, fontsize=30)
ax.text(0, .25, "pre-shaped + LRO/PDF")
ax.text(.5, .25, word_pre_fixed, fontsize=30)

plt.show()
fig.savefig(f'/tmp/bidi_mpl{matplotlib.__version__.replace('.', '-')}.png')
bidi_mpl3-11-1 bidi_mpl3-10-9

I think the path forward here is to document:

  • if you can pin to > 3.11, then in just works
  • if you can version-gate behavior then put in two branches so you can drop pre-processing the text before passing to mpl when you can pin your minimum to 3.11
  • if you have to support both sides and do not control the direct mpl calls (e.g. you are passing text into a thrid-party library that is calling mpl on your behalf) then pre-process and wrape the text in LRO/PDF.

@Syamjith-NK

Copy link
Copy Markdown
Contributor Author

Thanks, all three of these are more useful than what I filed.

On LRO/PDF with Arabic. You said you were not confident, so I checked it. It works, with one caveat I think is worth writing down.

I rendered LRO + get_display(reshape(w)) + PDF against the plain logical string on 3.11 and compared the rasters directly. Mean absolute pixel difference, both crops normalised to 72px height, so 0.0 means the two images are identical:

font الإمارات المبيعات مرحبا
Arial Unicode 0.0 0.0 0.0
SF Arabic 9.02 0.0 0.0
Geeza Pro 39.65 31.85 25.0

The reading is correct in all nine, including with a lam-alef ligature and with Latin digits mixed in, so your recommendation holds. But it is not always the same rendering. The wrapped string draws the font's static Presentation Forms glyphs instead of the font's own shaping, so wherever those are not metrically identical to the shaped output you get looser joins and a wider line. With Geeza Pro that is visible at a glance, not just in the numbers.

So for the third branch I would document it as "renders correctly on every version" rather than "renders identically". For someone who does not control the matplotlib calls that still seems like clearly the right trade.

Is there a generic way to tell a string is pre-ordered? Partly, and the split is the interesting part.

  • The reshaping half leaves a fingerprint. arabic_reshaper output lands in Arabic Presentation Forms-B (U+FE70 to U+FEFF), which essentially never appears in text a person typed. Over 1,856 generated samples, arabic_reshaper 3.0.0 emits 122 distinct codepoints from that block and never U+FEFF, which matters because U+FEFF sits inside the block and is the byte order mark, so a naive range check flags every file that has one.
  • The reordering half does not. Your hello[::-1] case has the same codepoints in a different order. Nothing distinguishes it from correctly stored text without knowing the language, so a general detector is not possible there.
  • Presentation Forms-A looks like a second signal and is not usable as one. The reshaper does emit from it (U+FDF2, and U+FBE8/U+FBE9 for the Uighur alef), and ﷺ ﷻ ﷽ are used deliberately in ordinary Arabic, so flagging that block flags correct text.

I wrapped the Forms-B check in a small linter and ran it across 3,883 mixed files here: 31 flagged spans, all true positives.

How common is it? A floor, counting only public code that GitHub indexes: 1,172 files match get_display(arabic_reshaper.reshape, and 4,048 match arabic_reshaper.reshape. arabic-reshaper had 5.31M downloads in the last month. Not all of that reaches matplotlib, but the recipe is the accepted answer almost everywhere the question is asked, which is what makes 3.11 a quiet change rather than a loud one.

@rcomer thank you for editing those StackOverflow answers. That reaches more people than this PR will, and I should have thought to do it myself.

@story645 that makes sense, and it is a better home than where I put it. I will rewrite this as an admonition on the 3.11 change note covering @tacaswell's three branches, and drop the section I added to the fonts page unless you would still like something on RTL there.

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.
@Syamjith-NK Syamjith-NK changed the title Doc: note that Matplotlib shapes complex scripts itself since 3.11 Doc: tell 3.11 upgraders to remove the pre-shaping workaround Sep 1, 2026
@github-actions github-actions Bot removed the Documentation: user guide files in galleries/users_explain or doc/users label Sep 1, 2026
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)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
ax.set_title(get_display(arabic_reshaper.reshape(text)))
preprocessed = get_display(arabic_reshaper.reshape(text))
ax.set_title(preprocessed)

so it's clear you're using the same thing below with the direction overrides.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied, thanks — that was a real gap and not just style: the override snippet further down already referred to preprocessed, so with the expression inlined above it the name was never bound and the two examples did not read as the same value.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please don't reply by copy-pasting an LLM; it just repeated what I said with more words, and isn't even right about what it's saying.

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.
Arabic, Persian, Urdu and Hebrew was to reorder the string before passing it in,
typically with ``arabic_reshaper`` and ``python-bidi``::

preprocessed = get_display(arabic_reshaper.reshape(text))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do we want to include the imports? On the hand, it makes the code more understandable to people who are not already using these, but on the other hand if you do not already recognize these functions, this note is not for you.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it is likely fine without the imports, though get_display is a little generic of a name.

Overall, though, the context is pretty clear and this is an example of workarounds that people previously did, not a suggestion of something to do, which makes it easier to justify a shorter snippet.

@tacaswell tacaswell added this to the v3.11-doc milestone Sep 4, 2026
@ksunden
ksunden merged commit ef5ac81 into matplotlib:main Sep 4, 2026
24 checks passed
QuLogic pushed a commit that referenced this pull request Sep 5, 2026
QuLogic added a commit that referenced this pull request Sep 5, 2026
…263-on-v3.11.x

Backport PR #32263 on branch v3.11.x (Doc: tell 3.11 upgraders to remove the pre-shaping workaround)
QuLogic pushed a commit that referenced this pull request Sep 5, 2026
QuLogic added a commit that referenced this pull request Sep 5, 2026
…263-on-v3.11.0-doc

Backport PR #32263 on branch v3.11.0-doc (Doc: tell 3.11 upgraders to remove the pre-shaping workaround)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Doc]: 3.11 text shaping silently reverses the arabic_reshaper + python-bidi workaround

4 participants