PPTX Recipes — python-pptx (+ LibreOffice)
Contents
- Create: layouts, bullets, formatting
- Tables
- Images
- Charts
- Speaker notes
- Read / extract (all text, per-slide, notes)
- Modify (find-and-replace, delete a slide, reorder)
- Template reuse
- Convert / render
- Gotchas
Create: layouts, bullets, formatting
python
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
prs = Presentation()
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5) # 16:9
# Default template layout indexes: 0 title, 1 title+content, 5 title only, 6 blank
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = "Agenda"
body = slide.placeholders[1].text_frame
body.text = "Overview" # first bullet, level 0
for txt, lvl in [("Results", 0), ("By region", 1), ("Next steps", 0)]:
p = body.add_paragraph()
p.text, p.level = txt, lvl # levels give indented bullets — never type "•"
# Run-level formatting
run = body.paragraphs[0].runs[0]
run.font.name = "Arial"
run.font.size = Pt(18)
run.font.bold = True
run.font.color.rgb = RGBColor(0x44, 0x72, 0xC4)
body.paragraphs[0].alignment = PP_ALIGN.LEFT
prs.save("deck.pptx")Tables
python
rows, cols = 3, 3
# 0.5" margins on a 13.333" slide → 12.333" usable width
tbl = slide.shapes.add_table(rows, cols, Inches(0.5), Inches(2), Inches(12.333), Inches(3)).table
tbl.columns[0].width = Inches(4)
for c, h in enumerate(["Region", "Q1", "Q2"]):
cell = tbl.cell(0, c)
cell.text = h
cell.text_frame.paragraphs[0].runs[0].font.bold = True
tbl.cell(1, 0).text = "East"Images
python
# Size by width only — height scales to keep aspect ratio
slide.shapes.add_picture("chart.png", Inches(7), Inches(1.5), width=Inches(5.8))Charts
python
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE
data = CategoryChartData()
data.categories = ["East", "West"]
data.add_series("Q1", (100, 90))
data.add_series("Q2", (150, 120))
slide.shapes.add_chart(XL_CHART_TYPE.COLUMN_CLUSTERED,
Inches(0.5), Inches(1.5), Inches(6), Inches(4.5), data)Speaker notes
python
slide.notes_slide.notes_text_frame.text = "Mention the supply-chain caveat here."Read / extract
python
from pptx import Presentation
prs = Presentation("deck.pptx")
# All text, slide by slide (walks groups too)
def shape_texts(shapes):
for sh in shapes:
if sh.shape_type == 6: # group shape — recurse
yield from shape_texts(sh.shapes)
elif sh.has_text_frame:
yield sh.text_frame.text
for i, slide in enumerate(prs.slides, 1):
print(f"--- slide {i}: {list(shape_texts(slide.shapes))}")
# Tables
for slide in prs.slides:
for sh in slide.shapes:
if sh.has_table:
rows = [[c.text for c in r.cells] for r in sh.table.rows]
# Notes
notes = [s.notes_slide.notes_text_frame.text if s.has_notes_slide else "" for s in prs.slides]Modify
python
# Find-and-replace preserving run formatting where possible
def replace_text(prs, old, new):
for slide in prs.slides:
for sh in slide.shapes:
if not sh.has_text_frame:
continue
for p in sh.text_frame.paragraphs:
for run in p.runs:
if old in run.text:
run.text = run.text.replace(old, new)
if old in p.text: # spans runs — collapse to first run
full = p.text.replace(old, new)
for r in p.runs: r.text = ""
if p.runs: p.runs[0].text = full
# Delete a slide (no public API — drop the XML relationship)
def delete_slide(prs, index):
xml_slides = prs.slides._sldIdLst
xml_slides.remove(list(xml_slides)[index])
# Reorder: remove and reinsert the sldId element at the target positionTemplate reuse
python
prs = Presentation("corporate-template.potx") # keeps theme, fonts, layouts
for layout in prs.slide_masters[0].slide_layouts:
print(layout.name) # pick layouts by name, indexes vary per template
slide = prs.slides.add_slide(prs.slide_masters[0].slide_layouts[1])
prs.save("deck.pptx") # save as .pptx, not .potxConvert / render
bash
soffice --headless --convert-to pdf deck.pptx # visual QA
pdftoppm -png -r 80 deck.pdf slide # slide-1.png … for inspection
soffice --headless --convert-to pptx legacy.ppt # legacy conversionGotchas
- Placeholder indexes are template-specific —
placeholders[1]on the default template is the body, but custom templates differ; iterateslide.placeholdersand check.placeholder_format.idx/.name. prs.slideshas noadd_slide-at-position, no delete, no copy — deletion/reorder require the_sldIdLstXML manipulation above; copying slides between files is not supported (rebuild instead).- First paragraph already exists in a text frame: set
tf.textfor it, useadd_paragraph()only for the rest, or you get a leading empty line. - python-pptx cannot measure rendered text — overflow is invisible until you render with LibreOffice; always visual-check final decks.
- Colors:
RGBColor(0x44, 0x72, 0xC4)— no alpha channel; theme colors needrun.font.color.theme_color. - Charts added by python-pptx embed an xlsx part — replacing chart data later requires
chart.replace_data(new_data), not editing cells. .potxtemplates open fine but must be saved as.pptxto be presentable; saving over the.potxalters the user's template.- Group shapes hide their children from a flat
slide.shapestext walk — recurse intoshape_type == 6(see reader recipe).