| name | beamer-ppt |
| description | Create Beamer-style academic PPTX presentations using python-pptx. Produces publication-quality .pptx files with navy-blue Metropolis theme (16:9, frame title bars, progress bar) for conference talks, job market presentations, and seminar slides. Called by /present command. |
Beamer-ppt-Creator
Purpose
This skill generates professional academic PPTX presentations that faithfully replicate the visual style of LaTeX Beamer (Metropolis theme). Output is a .pptx file that can be opened, edited, and presented directly in PowerPoint or LibreOffice Impress — no LaTeX installation required.
When to Use
- Called by
/present command to produce the final slides/slides.pptx
- Preparing conference, seminar, or job market slides
- Converting a completed economics paper into a slide deck
Design Principles
- One idea per slide — split if content overflows
- Minimum 20pt for body text; 24pt for frame titles
- Consistent palette — navy blue primary, one accent color only
- Figures over tables — embed PNG images at ≥ 200 DPI
- Last slide = Takeaways, never "Questions?"
Implementation
This skill executes Python code using python-pptx. Always install dependencies first:
pip install python-pptx pdf2image --break-system-packages
apt-get install -y poppler-utils 2>/dev/null || true
Color Palettes by Theme
| Theme | Title Bar bg | Accent | Slide bg |
|---|
| A. Metropolis (default) | RGB(0, 35, 82) navy | RGB(180, 30, 30) red | RGB(245, 245, 245) light gray |
| B. Minimal (job market) | RGB(0, 35, 82) navy | RGB(0, 35, 82) navy | RGB(255, 255, 255) white |
| C. Madrid (traditional) | RGB(31, 73, 125) dark blue | RGB(189, 152, 44) gold | RGB(255, 255, 255) white |
Core Helper Functions
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
import os
prs = Presentation()
prs.slide_width = Inches(13.33)
prs.slide_height = Inches(7.5)
NAVY = RGBColor(0, 35, 82)
RED = RGBColor(180, 30, 30)
LGRAY = RGBColor(245, 245, 245)
WHITE = RGBColor(255, 255, 255)
BLACK = RGBColor(30, 30, 30)
MGRAY = RGBColor(100, 100, 100)
def add_bg(slide, prs, color):
"""Full-slide background rectangle."""
shape = slide.shapes.add_shape(
1, 0, 0, prs.slide_width, prs.slide_height)
shape.fill.solid()
shape.fill.fore_color.rgb = color
shape.line.fill.background()
return shape
def add_frame_title(slide, prs, text, bg=NAVY, fg=WHITE):
"""Navy title bar (1.1 in tall) — mimics Beamer \\frametitle."""
bar = slide.shapes.add_shape(
, , , prs.slide_width, Inches())
bar.fill.solid()
bar.fill.fore_color.rgb = bg
bar.line.fill.background()
tf = bar.text_frame
tf.word_wrap =
tf.margin_left = Inches()
tf.margin_top = Inches()
p = tf.paragraphs[]
p.text = text
p.font.bold =
p.font.size = Pt()
p.font.color.rgb = fg
p.alignment = PP_ALIGN.LEFT
():
h = Inches()
top = prs.slide_height - h
track = slide.shapes.add_shape(
, , top, prs.slide_width, h)
track.fill.solid()
track.fill.fore_color.rgb = RGBColor(, , )
track.line.fill.background()
filled_w = (prs.slide_width * current / (total, ))
filled_w > :
bar = slide.shapes.add_shape(, , top, filled_w, h)
bar.fill.solid()
bar.fill.fore_color.rgb = color
bar.line.fill.background()
():
slide.notes_slide.notes_text_frame.text = notes_text
Slide Factory Functions
def make_title_slide(prs, title, subtitle, author, institute, date_line):
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, NAVY)
def _tb(left, top, w, h):
tb = slide.shapes.add_textbox(
Inches(left), Inches(top), Inches(w), Inches(h))
tb.text_frame.word_wrap = True
return tb.text_frame
tf = _tb(1, 1.7, 11.33, 2.0)
p = tf.paragraphs[0]
p.text = title; p.font.bold = True
p.font.size = Pt(34); p.font.color.rgb = WHITE
p.alignment = PP_ALIGN.CENTER
if subtitle:
p2 = tf.add_paragraph()
p2.text = subtitle; p2.font.size = Pt(20)
p2.font.color.rgb = LGRAY; p2.alignment = PP_ALIGN.CENTER
tf2 = _tb(1, 4.3, 11.33, 1.4)
p3 = tf2.paragraphs[0]
p3.text = author; p3.font.size = Pt(18)
p3.font.color.rgb = WHITE; p3.alignment = PP_ALIGN.CENTER
p4 = tf2.add_paragraph()
p4.text = institute; p4.font.size = Pt(15)
p4.font.color.rgb = LGRAY; p4.alignment = PP_ALIGN.CENTER
tf3 = _tb(1, 6.1, 11.33, 0.8)
p5 = tf3.paragraphs[0]
p5.text = date_line; p5.font.size = Pt(13)
p5.font.color.rgb = LGRAY; p5.alignment = PP_ALIGN.CENTER
return slide
():
slide = prs.slides.add_slide(prs.slide_layouts[])
add_bg(slide, prs, bg)
add_frame_title(slide, prs, title)
tb = slide.shapes.add_textbox(
Inches(), Inches(), Inches(), Inches())
tf = tb.text_frame; tf.word_wrap =
i, (lvl, text) (bullets):
p = tf.paragraphs[i] i == tf.add_paragraph()
p.text = text; p.level = lvl
p.font.size = Pt( lvl == )
p.font.color.rgb = BLACK
p.space_before = Pt( lvl == )
current total:
add_progress_bar(slide, prs, current, total)
slide
():
slide = prs.slides.add_slide(prs.slide_layouts[])
add_bg(slide, prs, LGRAY)
add_frame_title(slide, prs, title)
slide.shapes.add_picture(
img_path,
left=Inches(), top=Inches(),
width=Inches(), height=Inches())
caption:
cap = slide.shapes.add_textbox(
Inches(), Inches(), Inches(), Inches())
cap.text_frame.paragraphs[].text = caption
cap.text_frame.paragraphs[].font.size = Pt()
cap.text_frame.paragraphs[].font.color.rgb = MGRAY
current total:
add_progress_bar(slide, prs, current, total)
slide
():
slide = prs.slides.add_slide(prs.slide_layouts[])
add_bg(slide, prs, LGRAY)
add_frame_title(slide, prs, title)
nc = (headers); nr = (rows) +
tbl = slide.shapes.add_table(
nr, nc,
Inches(), Inches(),
Inches(), Inches()).table
j, h (headers):
c = tbl.cell(, j)
c.text = h
c.text_frame.paragraphs[].font.bold =
c.text_frame.paragraphs[].font.size = Pt()
c.text_frame.paragraphs[].font.color.rgb = WHITE
c.fill.solid(); c.fill.fore_color.rgb = NAVY
i, row (rows):
j, val (row):
c = tbl.cell(i + , j)
c.text = (val)
c.text_frame.paragraphs[].font.size = Pt()
highlight_last_col j == nc - :
c.text_frame.paragraphs[].font.bold =
footnote:
fn = slide.shapes.add_textbox(
Inches(), Inches(), Inches(), Inches())
fn.text_frame.paragraphs[].text = footnote
fn.text_frame.paragraphs[].font.size = Pt()
fn.text_frame.paragraphs[].font.color.rgb = MGRAY
current total:
add_progress_bar(slide, prs, current, total)
slide
():
slide = prs.slides.add_slide(prs.slide_layouts[])
add_bg(slide, prs, LGRAY)
add_frame_title(slide, prs, title)
col_bullets, left_offset [(left_bullets, ),
(right_bullets, )]:
tb = slide.shapes.add_textbox(
Inches(left_offset), Inches(),
Inches(), Inches())
tf = tb.text_frame; tf.word_wrap =
i, (lvl, text) (col_bullets):
p = tf.paragraphs[i] i == tf.add_paragraph()
p.text = text; p.level = lvl
p.font.size = Pt( lvl == )
p.font.color.rgb = BLACK
p.space_before = Pt( lvl == )
current total:
add_progress_bar(slide, prs, current, total)
slide
PDF → PNG Conversion (for figures from /plot)
import subprocess
def pdf_to_png(pdf_path, dpi=200):
"""Convert PDF figure to PNG for embedding in PPTX."""
png_base = pdf_path.replace(".pdf", "")
try:
subprocess.run(
["pdftoppm", "-r", str(dpi), "-png", "-singlefile",
pdf_path, png_base],
check=True, capture_output=True)
return png_base + ".png"
except (subprocess.CalledProcessError, FileNotFoundError):
from pdf2image import convert_from_path
imgs = convert_from_path(pdf_path, dpi=dpi)
png_path = png_base + ".png"
imgs[0].save(png_path, "PNG")
return png_path
Save, Export PDF & Verify
import subprocess
def save_and_verify(prs, output_path, export_pdf=True):
"""Save PPTX, optionally export PDF via LibreOffice, then verify."""
os.makedirs(os.path.dirname(output_path), exist_ok=True)
prs.save(output_path)
check = Presentation(output_path)
n = len(check.slides)
assert n > 0, "PPTX is empty — check slide generation."
print(f"✅ PPTX saved : {output_path}")
print(f" {n} slides | {os.path.getsize(output_path) // 1024} KB")
pdf_path = None
if export_pdf:
pdf_path = _pptx_to_pdf(output_path)
return output_path, pdf_path
def _pptx_to_pdf(pptx_path):
"""Convert PPTX → PDF using LibreOffice headless."""
out_dir = os.path.dirname(pptx_path)
try:
result = subprocess.run(
["libreoffice", "--headless", "--convert-to", "pdf",
"--outdir", out_dir, pptx_path],
capture_output=True, text=True, timeout=120
)
pdf_path = pptx_path.replace(".pptx", ".pdf")
if os.path.exists(pdf_path):
print(f"✅ PDF exported: ")
()
pdf_path
:
()
()
FileNotFoundError:
()
()
()
()
subprocess.TimeoutExpired:
()
()
Slide Structure by Presentation Type
| Slide Section | 15-min conf (≤15) | 45-min seminar (≤30) | Job market (≤20) |
|---|
| Title | 1 | 1 | 1 |
| Motivation | 1–2 | 2–3 | 2–3 |
| This Paper | 1 | 1 | 1 |
| Related Lit | — | 1–2 | 1–2 |
| Data | 1 | 2 | 2 |
| Identification | 2 | 3–4 | 3 |
| Main Results | 3 | 5–7 | 4–5 |
| Robustness | 1 | 2–3 | 2 |
| Heterogeneity | — | 2–3 | 1–2 |
| Takeaways | 1 | 1 | 1 |
Best Practices
- One message per slide — split if content overflows
- Use figures over tables — embed PNG at ≥ 200 DPI
- Bold the preferred specification column in regression tables
- Add speaker notes to every key slide via
add_speaker_notes()
- Prepare appendix slides for anticipated Q&A
- Timing: budget 1.5 min/slide; final slide must be Takeaways
Common Pitfalls
- ❌ Too much text (max 5 bullets per slide, max 10 words per bullet)
- ❌ Tables with more than 4 columns
- ❌ Ending with "Thank you / Questions?" — use Takeaways instead
- ❌ Embedding low-resolution images (< 150 DPI looks blurry on projectors)
- ❌ Skipping the "This Paper" preview slide