Repository navigation
Description
Documentation Link
https://matplotlib.org/devdocs/users/explain/text/text_intro.html
Problem
Since #30000 landed, matplotlib shapes and reorders complex scripts itself. That makes the workaround recommended in essentially every "Arabic text in matplotlib" answer actively wrong:
text = get_display(arabic_reshaper.reshape(text)) # necessary before 3.11, reversed nowThe string is already in visual order, matplotlib applies the bidi algorithm again, and the label renders reversed. Nothing raises, and to a reader who does not read Arabic the output still looks like Arabic in the right place, which is why I think it is worth a note rather than leaving it to be discovered.
arabic_reshaper appears nowhere in the docs, so nothing currently tells someone upgrading that the pre-processing should be removed.
Measured on matplotlib 3.11.0, libraqm 0.10.5, macOS, using SFArabic.ttf and the string الإمارات. Mean absolute pixel difference, normalised to 64px height, against a Pillow/Raqm render of the same string in the same font:
| render | difference |
|---|---|
| raw logical string | 7.75 (antialiasing only, i.e. matching) |
get_display(reshape(...)) |
76.28 (reversed) |
One extra detail worth knowing: the reshape collapses lam-alef, so الإمارات goes from 8 codepoints to 7. Where an application stored the pre-shaped string rather than only rendering it, mapping the presentation forms back does not recover the original, because that one codepoint expands in logical order while its neighbours are still in visual order. السلام comes back as السالم, a real but different word.
Self-contained reproducer
import matplotlib
matplotlib.use("Agg")
import arabic_reshaper, numpy as np
import matplotlib.pyplot as plt, matplotlib.ft2font as ft
from bidi.algorithm import get_display
from matplotlib import font_manager
from PIL import Image, ImageDraw, ImageFont, features
FONT = "/System/Library/Fonts/SFArabic.ttf" # any Arabic-capable TTF
TEXT = "الإمارات"
PRE = get_display(arabic_reshaper.reshape(TEXT))
print("matplotlib", matplotlib.__version__, "libraqm", ft.__libraqm_version__)
print("codepoints:", len(TEXT), "->", len(PRE))
fp = font_manager.FontProperties(fname=FONT)
def mpl(text, path):
fig = plt.figure(figsize=(6, 1.3), dpi=100)
fig.text(0.04, 0.3, text, fontproperties=fp, fontsize=48)
fig.savefig(path, facecolor="white"); plt.close(fig)
def pil(text, path, px=67):
font = ImageFont.truetype(FONT, px, layout_engine=ImageFont.Layout.RAQM)
img = Image.new("L", (600, 130), 255)
ImageDraw.Draw(img).text((25, 15), text, font=font, fill=0); img.save(path)
mpl(TEXT, "a_raw.png"); mpl(PRE, "b_preshaped.png"); pil(TEXT, "c_reference.png")
def normalised(path, h=64):
a = 255 - np.asarray(Image.open(path).convert("L"), float)
ys, xs = np.nonzero(a > 40)
im = Image.fromarray((255 - a[ys.min():ys.max()+1, xs.min():xs.max()+1]).astype("uint8"))
return np.asarray(im.resize((int(im.width * h / im.height), h)), float)
def diff(p, q):
a, b = normalised(p), normalised(q)
w = min(a.shape[1], b.shape[1])
return float(np.abs(a[:, :w] - b[:, :w]).mean())
print("raw vs reference:", round(diff("a_raw.png", "c_reference.png"), 2))
print("preshaped vs reference:", round(diff("b_preshaped.png", "c_reference.png"), 2))Open a_raw.png and b_preshaped.png side by side, the second is the first read backwards.
Suggested improvement
A documentation-only note, in the text guide and in the 3.11 release note: pass the logical string and remove any arabic_reshaper / python-bidi pre-processing, because matplotlib now does the shaping and reordering.
Worth saying plainly that this is unconditional, unlike the equivalent situation in Pillow. I checked extern/meson.build before writing this: libraqm is a required dependency, resolved either from the system or from the vendored static subproject, so there is no build in which the old pre-processing is still correct.
Happy to open that PR if you would like it. I did the equivalent for Pillow in python-pillow/Pillow#9925.
matplotlib Version
3.11.0
Operating system
macOS
Activity
rcomer commentedon Aug 30, 2026
If you search the Matplotlib docs for "Arabic", you get this changenote
https://matplotlib.org/stable/release/prev_whats_new/whats_new_3.11.0.html#complex-text-layout-with-libraqm
Although it does not explicitly say that workarounds should be removed, the fact that it's now natively supported implies they are at least unnecessary.
Prompted by this issue, I just went and edited some StackOverflow answers to clarify those workarounds are only needed for older versions.
story645 commentedon Aug 30, 2026
I think a PR that says that explicitly inside an admonition in the change notes is fine since folks have had to do the workaround for so long and there are so many docs about it.
Not sure about putting it in main notes if we don't already mention rtl languages but otherwise an entry on rtl languages somewhere in the docs might be helpful.
tacaswell commentedon Aug 30, 2026
I left comments with code on #32263 . TL;DR if you wrap the text in unicode LRO / PDF then pre-processed text will render correctly with all versions of mpl.
Syamjith-NK commentedon Sep 2, 2026
Answered on #32263 rather than here, so linking it for anyone reading this thread.
I tested the LRO/PDF wrapping across three fonts and nine strings. It reads correctly in all nine — including a lam-alef ligature and Latin digits mixed in — so the recommendation holds. It does not always render identically: the wrapped string draws the font's static Presentation Forms glyphs instead of the font's own shaping, so joins loosen and the line widens wherever those differ. Arial Unicode was unaffected; Geeza Pro differed on every string tested.
Measurements and the per-font table are in #32263.