Arabic and Hebrew decks generated by code often look almost right: the letters are correct, but full stops sit at the wrong end of the line, bullets hang on the left and tables run backwards. The cause is nearly always the same. The file never says the text is right-to-left.
This page lists where a .pptx stores direction, shows working code for python-pptx and PptxGenJS, and reports what each approach wrote when we ran it. Tested on 12 November 2026 with python-pptx 1.0.2 and PptxGenJS 4.0.1.
The short version
- Direction lives on the paragraph:
<a:pPr rtl="1">. Set it on every paragraph. - Alignment is separate:
algn="r". Right-aligned text is not right-to-left text. - Arabic uses the complex-script font,
<a:cs typeface="…">, not the Latin one. - A table has its own flag,
<a:tblPr rtl="1">, which puts the first column on the right. Its cells still needrtl="1"on their paragraphs. - The presentation-level
rtlflag does not change any text.
What the file stores
A .pptx is a zip of XML files. This is a correct right-to-left paragraph in ppt/slides/slide1.xml:
<a:p>
<a:pPr algn="r" rtl="1"/>
<a:r>
<a:rPr lang="ar-SA">
<a:cs typeface="Tahoma"/>
</a:rPr>
<a:t>ارتفعت الإيرادات إلى 1,500,000 ريال (SAR).</a:t>
</a:r>
</a:p>
| Where | Attribute | What it does |
|---|---|---|
Paragraph, <a:pPr> |
rtl="1" |
Base direction of the paragraph. This is the one that matters. |
Paragraph, <a:pPr> |
algn="r" |
Alignment only. Independent of direction. |
Run, <a:rPr> |
lang="ar-SA" |
Language of the run, used for proofing. |
Run, <a:rPr> |
<a:cs typeface="…"/> |
Font used for Arabic and other complex scripts. |
Table, <a:tblPr> |
rtl="1" |
Lays the columns out from the right. |
Text box, <a:bodyPr> |
rtlCol="1" |
Orders multiple text columns from the right. |
Presentation, <p:presentation> |
rtl="1" |
A view setting for the application. It does not change text. |
A paragraph that does not set rtl inherits it from its text box's list style, then the slide layout, then the slide master, then the presentation defaults. Templates made for English set none of these, so the result is left-to-right.
Store the text itself in logical order, the order you would type it. Do not reverse it or reorder mixed Arabic and English yourself: PowerPoint applies the Unicode bidirectional algorithm when it draws the line, and it needs the paragraph direction to do that correctly.
python-pptx
python-pptx has no public property for paragraph direction, so the attribute is set on the underlying XML. font.name sets only the Latin font, so the complex-script font is added the same way.
from lxml import etree
from pptx import Presentation
from pptx.enum.lang import MSO_LANGUAGE_ID
from pptx.enum.text import PP_ALIGN
from pptx.oxml.ns import qn
from pptx.util import Inches
def make_rtl(paragraph, font=None):
"""Mark one paragraph right-to-left, right-aligned and Arabic."""
paragraph.alignment = PP_ALIGN.RIGHT
paragraph._p.get_or_add_pPr().set("rtl", "1")
for run in paragraph.runs:
run.font.language_id = MSO_LANGUAGE_ID.ARABIC # written as lang="ar-SA"
if font:
# font.name only sets the Latin font; Arabic text uses <a:cs>.
properties = run._r.get_or_add_rPr()
for old in properties.findall(qn("a:cs")):
properties.remove(old)
etree.SubElement(properties, qn("a:cs")).set("typeface", font)
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = "تقرير الربع الثالث"
body = slide.placeholders[1].text_frame
body.text = "ارتفعت الإيرادات إلى 1,500,000 ريال (SAR)."
body.add_paragraph().text = "انتقلت الأنظمة من Microsoft Azure إلى Google Cloud."
for shape in (slide.shapes.title, slide.placeholders[1]):
for paragraph in shape.text_frame.paragraphs:
make_rtl(paragraph, font="Tahoma")
table_slide = prs.slides.add_slide(prs.slide_layouts[5])
table = table_slide.shapes.add_table(2, 2, Inches(1), Inches(2), Inches(6), Inches(1.5)).table
for r, row in enumerate([["المؤشر", "الربع الثالث"], ["الإيرادات", "1,500,000"]]):
for c, value in enumerate(row):
table.cell(r, c).text = value
for paragraph in table.cell(r, c).text_frame.paragraphs:
make_rtl(paragraph)
table._tbl.tblPr.set("rtl", "1") # first column on the right
prs.save("deck.pptx")
Call make_rtl after the text is set. Assigning text_frame.text or cell.text replaces the paragraphs, which discards attributes set earlier.
PptxGenJS
PptxGenJS has an rtlMode option. It works on text objects and on individual table cells.
import pptxgen from "pptxgenjs";
const pptx = new pptxgen();
const slide = pptx.addSlide();
const rtl = { rtlMode: true, align: "right", lang: "ar-SA", fontFace: "Tahoma" };
slide.addText("ارتفعت الإيرادات إلى 1,500,000 ريال (SAR).", { x: 0.5, y: 0.5, w: 9, h: 1, ...rtl });
slide.addText(
[
{ text: "افتتاح مكتب في الرياض", options: { ...rtl, bullet: true, breakLine: true } },
{ text: "توظيف 12 مهندساً", options: { ...rtl, bullet: true } },
],
{ x: 0.5, y: 1.7, w: 9, h: 1.5 },
);
// Tables: rtlMode has to go on every cell. The table itself is not mirrored,
// so list the columns in the order they should appear from left to right.
const cell = (text) => ({ text, options: rtl });
slide.addTable(
[
[cell("الربع الثالث"), cell("المؤشر")],
[cell("1,500,000"), cell("الإيرادات")],
],
{ x: 0.5, y: 3.5, w: 9 },
);
await pptx.writeFile({ fileName: "deck.pptx" });
Three things we found by inspecting the output of version 4.0.1:
fontFacewrites both the Latin and the complex-script font, so Arabic picks it up.rtlModein the options ofaddTabledid not reach the cells. Their paragraphs were right-aligned but had nortl="1". PuttingrtlModeon each cell fixed it.- The table element was written without
rtl="1"either way, so the columns are not mirrored. Order them yourself, as above.
What each approach wrote
We generated a file each way and counted the Arabic paragraphs the file marks right-to-left.
| Approach | Marked right-to-left | Not left-aligned |
|---|---|---|
| PptxGenJS, no options | 0 of 1 | 0 of 1 |
PptxGenJS, align: "right" only |
0 of 1 | 1 of 1 |
PptxGenJS, pptx.rtlMode = true only |
0 of 1 | 0 of 1 |
PptxGenJS, rtlMode on text, and on the table instead of its cells |
3 of 6 | 6 of 6 |
PptxGenJS, rtlMode on text and on every cell (code above) |
6 of 6 | 6 of 6 |
python-pptx, make_rtl on every paragraph (code above) |
6 of 6 | 6 of 6 |
The second and third rows are the common traps. Right alignment alone, or the presentation-level switch alone, leaves every paragraph left-to-right.
Open XML SDK (C#)
We did not run this one. The property names below are from Microsoft's SDK reference, linked for each.
| Setting | Property |
|---|---|
| Paragraph direction | ParagraphProperties.RightToLeft |
| Table direction | TableProperties.RightToLeft |
| Text column order | BodyProperties.RightToLeftColumns |
| Presentation view flag | Presentation.RightToLeft |
Five mistakes that leave text left-to-right
- Aligning right and stopping there. The text hugs the right edge, but its base direction is still left-to-right, so punctuation and brackets land at the wrong end.
- Relying on the presentation flag.
<p:presentation rtl="1">does not touch paragraphs. - Setting direction on the first paragraph only. Every paragraph carries its own attribute, and each bullet is its own paragraph.
- Forgetting table cells. A cell's text is its own set of paragraphs. The table flag mirrors the columns and nothing else.
- Setting only the Latin font. Arabic is drawn with the complex-script font. If it is not set, PowerPoint uses the theme's default and your chosen typeface never appears.
Check a file
This prints, for each slide, how many paragraphs set rtl="1" directly:
import re
import sys
import zipfile
with zipfile.ZipFile(sys.argv[1]) as deck:
slides = sorted(n for n in deck.namelist() if re.fullmatch(r"ppt/slides/slide\d+\.xml", n))
for name in slides:
xml = deck.read(name).decode("utf-8")
paragraphs = [p for p in re.findall(r"<a:p>.*?</a:p>", xml, flags=re.S) if "<a:t>" in p]
marked = [p for p in paragraphs if re.search(r'<a:pPr[^>]*\brtl="1"', p)]
print(f"{name}: {len(marked)} of {len(paragraphs)} paragraphs marked right-to-left")
It is a quick check, not a full one: it does not follow inheritance from the layout and master, and it counts every paragraph, not only the Arabic ones. A template that sets direction once in its master would show zero here and still be correct.