Génération de PDF arabe / RTL
Le code ReportLab naïf produit un arabe subtilement mais totalement erroné : les lettres apparaissent déconnectées, les mots dans une ligne s'affichent à l'envers et les lignes dans un paragraphe s'échangent leurs places. Rien de tout cela n'est visible dans le texte extrait — uniquement à l'affichage. Cette compétence intègre un constructeur qui a déjà résolu ces problèmes.
Utilisez scripts/arabic_pdf.py plutôt que d'écrire la logique de reshapage à partir de zéro.
Chaque mode d'échec ci-dessous a été découvert en le rencontrant dans un document réel.
Dépendances
pip install reportlab arabic-reshaper python-bidi pypdfium2
pypdfium2 n'est nécessaire que pour l'aperçu, mais l'aperçu n'est pas optionnel — voir
Vérification ci-dessous.
Construire un document
Importez le constructeur soit en copiant scripts/arabic_pdf.py à côté de votre
script de génération, soit en ajoutant le dossier scripts/ de cette compétence à sys.path :
import pathlib, sys
sys.path.insert(0, str(pathlib.Path.home() / ".claude" / "skills" / "arabic-pdf" / "scripts"))
from arabic_pdf import ArabicPDF, preview
pdf = ArabicPDF("report.pdf", doc_title="عنوان المستند", page_numbers=True)
pdf.title("عنوان المستند")
pdf.subtitle("سطر فرعي")
pdf.heading("أولاً — المقدمة")
pdf.para("فقرة عربية تتضمن مصطلحات لاتينية مثل PostgreSQL و FastAPI.")
pdf.heading("ثانياً — جدول")
pdf.table(
["البند", "التفصيل"], # colonne la plus à droite en premier
[["الواجهة الخلفية", "Python — FastAPI"],
["قاعدة البيانات", "PostgreSQL — SQLAlchemy"]],
[5.0, 11.0], # largeurs de colonnes en cm
)
pdf.bullets(["نقطة أولى", "نقطة ثانية"])
pdf.save()
preview("report.pdf") # un PNG par page
Ajustez le chemin si la compétence se trouve ailleurs — rien d'autre dans le constructeur ne dépend de son emplacement d'installation.
Les méthodes s'enchaînent, donc pdf.heading(...).para(...) fonctionne aussi.
| Méthode | Objectif |
|---|---|
title / subtitle | En-tête de document centré |
heading | En-tête de section en gras, aligné à droite |
para | Paragraphe de corps |
bullets(items, marker="–") | Une ligne alignée à droite par élément |
table(header, rows, widths_cm) | Tableau RTL ; ordre de lecture naturel en entrée, colonnes inversées en sortie |
spacer(pts) / page_break() | Contrôle de la mise en page |
save() | Écrit le fichier, renvoie le chemin |
Le texte des cellules et des paragraphes peut contenir \n pour forcer un saut de ligne.
Vérification — toujours générer et regarder
Après save(), appelez preview() et visualisez les PNG avec l'outil Read. L'extraction
de texte ne peut pas détecter les défauts RTL, car les caractères sont tous présents et corrects ;
seuls leur ordre et leur forme sont erronés. Vérifiez spécifiquement :
- Est-ce que le premier mot de chaque paragraphe se trouve en haut à droite ?
- Les termes latins (
PostgreSQL,FastAPI) se trouvent-ils à leur place au milieu de la phrase, plutôt que de sauter à l'extrémité gauche de la ligne ? - Les lettres sont-elles connectées, ou séparées comme
ا ل س ل ا م? - Y a-t-il des boîtes noires pleines ? La police ne possède pas ces glyphes — choisissez-en une autre.
- Les parenthèses sont-elles orientées correctement —
(20 فأكثر)et non)20 فأكثر(? - Les paires dans les termes latins sont-elles intactes —
Array<String>,DECIMAL(5,2)? - Une ligne de tableau a-t-elle été orpheline sur la page suivante ?
Corriger un défaut que vous n'avez jamais recherché coûte bien plus tard, lorsque le document est déjà chez son destinataire.
Comment la correction est obtenue
Utile pour adapter le constructeur ou déboguer un document étrange.
- Reshapage —
arabic_reshapermappe les caractères à leurs formes de présentation positionnelles pour que les lettres se connectent. - Bidi avec une direction de base explicite —
get_display(..., base_dir="R"). Sansbase_dir, la direction est déduite du premier caractère fort, donc une ligne commençant par un mot latin est disposée de gauche à droite et l'arabe qui suit se place mal. C'est la cause la plus courante de « presque correct mais certaines lignes sont mélangées ». - Retour à la ligne manuel — le texte reshapé est en ordre visuel. Si ReportLab
le coupe, les mots qu'il pousse à la ligne suivante sont logiquement les premiers,
donc les lignes sont mélangées. Le constructeur coupe d'abord, reshape chaque ligne
séparément, et les joint avec
<br/>pour que ReportLab ne recoupe jamais. Une marge de sécurité de 8pt couvre les différences de mesure ; une ligne qui dépasse d'un point suffit à l'inverser. - Miroir —
get_displayréordonne mais n'applique jamais la règle bidi L4, qui dit qu'un caractère en miroir disposé de droite à gauche est dessiné comme son partenaire. Sans lui(20 فأكثر)revient avec les deux parenthèses échangées et leurs glyphes originaux, et imprime)20 فأكثر(.prepare()les substitue d'abord, donc le réordonnancement déplace le glyphe correct. - Îlots latins —
<et>sont en miroir mais ne sont pas des parenthèses appariées Unicode, donc la règle qui maintientDECIMAL(5,2)ensemble ne les couvre pas ; le>deArray<String>prend la direction de l'arabe après lui et est projeté de l'autre côté. Chaque segment latin est enveloppé dansLRI…PDIet les isolateurs sont supprimés après le réordonnancement, pour que rien d'invisible n'atteigne le PDF. - Échappement après le reshapage — ReportLab nécessite l'échappement de
&,<,>, mais échapper d'abord envoie<dans le processus bidi comme quatre caractères et son;final est éjecté de l'autre côté de la ligne. Shapez, puis échappez. - Colonnes de tableau inversées — ReportLab dispose les colonnes de gauche à droite, donc le constructeur les inverse, vous permettant de passer les colonnes dans l'ordre de lecture naturel.
KeepTogethersur les tableaux — empêche une ligne isolée de se retrouver sur une nouvelle page.
Les points 4–6 sont de la même classe de défauts que les séquences de parenthèses divisées décrites dans
arabic-docx, atteints depuis la direction opposée : là, une paire ne doit pas être
séparée entre les séquences, ici une paire ne doit pas être à moitié en miroir. Tout
caractère en miroir doit être résolu avec son partenaire.
Polices
register_fonts() recherche une police capable d'afficher l'arabe : Arial puis Tahoma sur
Windows, Arial sur macOS, Noto Naskh Arabic puis DejaVu Sans sur Linux. Pour utiliser une
police spécifique (Amiri et Cairo conviennent bien aux documents formels) :
from arabic_pdf import register_fonts
register_fonts("/chemin/Amiri-Regular.ttf", "/chemin/Amiri-Bold.ttf") # avant ArabicPDF(...)
Les polices intégrées de ReportLab n'ont pas de couverture arabe, donc un TTF doit être enregistré — c'est pourquoi une police manquante apparaît comme des boîtes noires plutôt qu'une erreur.
Ne jamais transposer cette approche à Word ou PowerPoint
C'est la limite la plus importante dans toute la famille de compétences RTL.
| ReportLab / Pillow | Word / PowerPoint | |
|---|---|---|
| Moteur de texte | aucun | moteur bidirectionnel complet |
| Attend | des glyphes finaux en ordre visuel | Unicode brut en ordre logique |
| Technique | reshape + bidi + coupure manuelle | indicateurs de direction dans le XML |
Le pré-reshapage est nécessaire ici et destructeur là-bas : Word et PowerPoint s'occupent du reshapage au moment du rendu, donc leur donner des formes de présentation les fait reshaper une seconde fois — glyphes cassés et texte qui ne peut plus être recherché, copié ou vérifié orthographiquement. Les deux approches sont opposées, pas des variations.
Utilisez arabic-docx pour les .docx et arabic-pptx pour les .pptx. Ne portez pas de code
entre les trois sans relire pourquoi ils diffèrent.
Remarques
arabic_digits("38")→"٣٨"lorsque les chiffres arabo-indiens sont souhaités. Purement cosmétique ; les chiffres occidentaux sont tout à fait normaux dans l'écriture technique arabe.- Conservez un formatage sobre par défaut — texte noir, filets fins. Les documents académiques et officiels arabes sont plus crédibles ainsi, et les utilisateurs qui demandent « simple » le pensent vraiment.
- Les mêmes mécanismes gèrent le persan, l'ourdou et l'hébreu ; seule la police doit couvrir l'écriture.
Ce qui fait que l'arabe généré semble généré
Ce n'est pas le vocabulaire — ce sont les ajouts. Chaque habitude ci-dessous indique au lecteur comment le document a été écrit plutôt que son sujet, et un lecteur qui édite à la main les supprime tous. Écrivez la phrase, pas le récit de son écriture.
| Habitude | Au lieu de | Écrivez |
|---|---|---|
| Raconter l'acte d'écrire | « الشروط ثلاثة، نذكرها صراحة فيما يلي » | « الشروط ثلاثة : » |
| Pointer vers une autre section | « …ونعود إلى هذه النقطة لاحقاً » | Supprimez la phrase |
| Étiquettes de version ou statut dans un titre | « الخطة (النسخة الثانية — معتمدة) » | « الخطة » |
| Argumenter contre une option que personne n'a soulevée | « نحفظ المسار فقط. تخزين الملف كاملاً يضخّم الحجم بلا فائدة. » | « نحفظ المسار فقط. » |
| Justifier un choix par ce que le brief a omis | « لم يرد ذلك في الطلب، غير أن طبيعة العمل تفرضه. » | « طبيعة العمل تفرض ذلك. » |
| Première personne du singulier | « وأستطيع لاحقاً تحليل الحالات » | « ويمكن لاحقاً تحليل الحالات » |
| Réaffirmer un fait que le document a déjà donné | Répétition de l'information dans une autre section | Supprimez la répétition |
L'arabe formel préfère l'impersonnel ou le pluriel à « أنا » ; le singulier ressemble à une note pour soi-même plutôt qu'à un document.