Tutorials

Right-to-Left Text in PowerPoint Files: A Developer Reference (python-pptx, PptxGenJS, Open XML)

O
Omair AlAdawi
•
8 min read
•

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 need rtl="1" on their paragraphs.
  • The presentation-level rtl flag 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:

  • fontFace writes both the Latin and the complex-script font, so Arabic picks it up.
  • rtlMode in the options of addTable did not reach the cells. Their paragraphs were right-aligned but had no rtl="1". Putting rtlMode on 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

  1. 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.
  2. Relying on the presentation flag. <p:presentation rtl="1"> does not touch paragraphs.
  3. Setting direction on the first paragraph only. Every paragraph carries its own attribute, and each bullet is its own paragraph.
  4. Forgetting table cells. A cell's text is its own set of paragraphs. The table flag mirrors the columns and nothing else.
  5. 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.

Related

O
Omair AlAdawi

Founder & CEO

Omair AlAdawi is the founder of Sharayeh, with over 8 years of experience in software engineering and EdTech. He leads the development of AI-powered presentation and document conversion tools used by 50,000+ users across 190 countries. His expertise spans natural language processing, multilingual systems, and Arabic RTL technology.

Share this article: