# 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 position ``` ## Template 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 .potx ``` ## Convert / 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 conversion ``` ## Gotchas - **Placeholder indexes are template-specific** — `placeholders[1]` on the default template is the body, but custom templates differ; iterate `slide.placeholders` and check `.placeholder_format.idx`/`.name`. - **`prs.slides` has no `add_slide`-at-position, no delete, no copy** — deletion/reorder require the `_sldIdLst` XML manipulation above; copying slides between files is not supported (rebuild instead). - **First paragraph already exists** in a text frame: set `tf.text` for it, use `add_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 need `run.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. - **`.potx` templates open fine but must be saved as `.pptx`** to be presentable; saving over the `.potx` alters the user's template. - **Group shapes hide their children** from a flat `slide.shapes` text walk — recurse into `shape_type == 6` (see reader recipe).