Not a bug in the reshaping itself — it is correct for its intended target. This is about the
usage the README and effectively every tutorial recommend, which silently produces malformed
Arabic on any renderer that already does complex-text layout. Pillow has linked Raqm/HarfBuzz
by default for some time now, so this path is common and getting more so.
What happens
reshape() maps base letters to Unicode presentation forms (U+FE70–U+FEFF) and get_display()
reorders to visual order. HarfBuzz then applies its own shaping and the bidi algorithm to that
already-shaped, already-reordered string. The work is done twice.
from PIL import Image, ImageDraw, ImageFont, features
import arabic_reshaper
from bidi.algorithm import get_display
print(features.check("raqm")) # True
TEXT = "مرحبا بكم" # "welcome"
FONT = "/System/Library/Fonts/SFArabic.ttf"
def render(s, path):
img = Image.new("L", (700, 160), 255)
ImageDraw.Draw(img).text((30, 40), s, font=ImageFont.truetype(FONT, 64), fill=0)
img.save(path)
render(TEXT, "plain.png") # correct
render(get_display(arabic_reshaper.reshape(TEXT)), "pre.png") # corrupted
|
rendered |
| no preprocessing |
مرحبا بكم |
reshape() + get_display() |
مكب ابحرم |
The intermediate string is 8 of 9 characters in presentation forms, in visual order:
U+FEE2 U+FEDC U+FE91 U+0020 U+FE8E U+FE92 ...
Letters come out disconnected and the word order is reversed.
Scope
5 strings × 3 fonts (SF Arabic, Geeza Pro, IBM Plex Sans Arabic) = 15 renders per path, scored by
shape-invariant IoU against a reference and graded in three bands rather than pass/fail:
| path |
no preprocessing |
with reshape() + get_display() |
| Pillow with Raqm |
15/15 identical |
0/15 identical — 14 broken, 1 recognisable |
| Pillow without Raqm |
0/15 — 15 broken |
0/15 identical — 9 recognisable, 6 still broken |
Two things follow, and the second surprised me:
- With Raqm, the preprocessing takes a path that is 100% correct and breaks all of it.
- Without Raqm, the preprocessing is a partial rescue, not a fix — it moves 9 of 15 from
broken to merely recognisable and leaves 6 broken. A pass/fail score would have reported this
as "works", which is how the recipe keeps its reputation.
So the right question is which renderer you have, not whether Arabic "needs" reshaping.
Detection is one line:
from PIL import features
needs_reshaping = not features.check("raqm")
Why it goes unnoticed
The output still looks like Arabic to anyone who does not read it, so it passes review. It matters
especially for synthetic training data: text rendered this way looks plausible at a glance, and
a model trained on it learns malformed letterforms.
Suggested fix
A note in the README that the recipe applies to renderers without complex-text shaping, with the
features.check("raqm") test. Happy to open a PR against the README if that would help.
Data
Full matrix, per-string results and the rendered images, CC BY 4.0:
https://huggingface.co/datasets/syamjithnk/arshape
Environment: Pillow 12.2.0, arabic-reshaper 3.0.0, python-bidi, macOS, Python 3.
Not a bug in the reshaping itself — it is correct for its intended target. This is about the
usage the README and effectively every tutorial recommend, which silently produces malformed
Arabic on any renderer that already does complex-text layout. Pillow has linked Raqm/HarfBuzz
by default for some time now, so this path is common and getting more so.
What happens
reshape()maps base letters to Unicode presentation forms (U+FE70–U+FEFF) andget_display()reorders to visual order. HarfBuzz then applies its own shaping and the bidi algorithm to that
already-shaped, already-reordered string. The work is done twice.
reshape()+get_display()The intermediate string is 8 of 9 characters in presentation forms, in visual order:
Letters come out disconnected and the word order is reversed.
Scope
5 strings × 3 fonts (SF Arabic, Geeza Pro, IBM Plex Sans Arabic) = 15 renders per path, scored by
shape-invariant IoU against a reference and graded in three bands rather than pass/fail:
reshape()+get_display()Two things follow, and the second surprised me:
broken to merely recognisable and leaves 6 broken. A pass/fail score would have reported this
as "works", which is how the recipe keeps its reputation.
So the right question is which renderer you have, not whether Arabic "needs" reshaping.
Detection is one line:
Why it goes unnoticed
The output still looks like Arabic to anyone who does not read it, so it passes review. It matters
especially for synthetic training data: text rendered this way looks plausible at a glance, and
a model trained on it learns malformed letterforms.
Suggested fix
A note in the README that the recipe applies to renderers without complex-text shaping, with the
features.check("raqm")test. Happy to open a PR against the README if that would help.Data
Full matrix, per-string results and the rendered images, CC BY 4.0:
https://huggingface.co/datasets/syamjithnk/arshape
Environment: Pillow 12.2.0, arabic-reshaper 3.0.0, python-bidi, macOS, Python 3.