SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%

# 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-specificplaceholders[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).