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.
Scanned 9/1/2026
Install to Claude Code
npx -y skills add zhouziyue233/great-econometrics --skill beamer-ppt --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Beamer Ppt?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/zhouziyue233-beamer-ppt)More formats (shields.io, HTML) on the badges page.
---
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:
```bash
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
```python
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
# ── Presentation setup ───────────────────────────────────────────
prs = Presentation()
prs.slide_width = Inches(13.33) # 16:9 widescreen (Beamer aspectratio=169)
prs.slide_height = Inches(7.5)
# ── Color definitions (Metropolis theme) ─────────────────────────
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(
1, 0, 0, prs.slide_width, Inches(1.1))
bar.fill.solid()
bar.fill.fore_color.rgb = bg
bar.line.fill.background()
tf = bar.text_frame
tf.word_wrap = False
tf.margin_left = Inches(0.3)
tf.margin_top = Inches(0.22)
p = tf.paragraphs[0]
p.text = text
p.font.bold = True
p.font.size = Pt(24)
p.font.color.rgb = fg
p.alignment = PP_ALIGN.LEFT
def add_progress_bar(slide, prs, current, total, color=NAVY):
"""Metropolis-style thin progress bar at bottom."""
h = Inches(0.055)
top = prs.slide_height - h
# Gray track
track = slide.shapes.add_shape(
1, 0, top, prs.slide_width, h)
track.fill.solid()
track.fill.fore_color.rgb = RGBColor(200, 200, 200)
track.line.fill.background()
# Filled portion
filled_w = int(prs.slide_width * current / max(total, 1))
if filled_w > 0:
bar = slide.shapes.add_shape(1, 0, top, filled_w, h)
bar.fill.solid()
bar.fill.fore_color.rgb = color
bar.line.fill.background()
def add_speaker_notes(slide, notes_text):
"""Add speaker notes to a slide."""
slide.notes_slide.notes_text_frame.text = notes_text
```
### Slide Factory Functions
```python
# ── 1. Title slide ───────────────────────────────────────────────
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
# Paper title
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
# Subtitle
if subtitle:
p2 = tf.add_paragraph()
p2.text = subtitle; p2.font.size = Pt(20)
p2.font.color.rgb = LGRAY; p2.alignment = PP_ALIGN.CENTER
# Author + institute
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
# Date / conference
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
# ── 2. Content slide (bullet list) ──────────────────────────────
def make_content_slide(prs, title, bullets,
current=None, total=None, bg=LGRAY):
"""
bullets: list of (indent_level, text) tuples.
indent_level 0 = top-level bullet, 1 = sub-bullet.
"""
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, bg)
add_frame_title(slide, prs, title)
tb = slide.shapes.add_textbox(
Inches(0.5), Inches(1.3), Inches(12.33), Inches(5.8))
tf = tb.text_frame; tf.word_wrap = True
for i, (lvl, text) in enumerate(bullets):
p = tf.paragraphs[i] if i == 0 else tf.add_paragraph()
p.text = text; p.level = lvl
p.font.size = Pt(20 if lvl == 0 else 17)
p.font.color.rgb = BLACK
p.space_before = Pt(8 if lvl == 0 else 4)
if current and total:
add_progress_bar(slide, prs, current, total)
return slide
# ── 3. Figure slide ──────────────────────────────────────────────
def make_figure_slide(prs, title, img_path, caption="",
current=None, total=None):
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, LGRAY)
add_frame_title(slide, prs, title)
slide.shapes.add_picture(
img_path,
left=Inches(1.17), top=Inches(1.3),
width=Inches(11.0), height=Inches(5.2))
if caption:
cap = slide.shapes.add_textbox(
Inches(0.5), Inches(6.6), Inches(12.33), Inches(0.7))
cap.text_frame.paragraphs[0].text = caption
cap.text_frame.paragraphs[0].font.size = Pt(11)
cap.text_frame.paragraphs[0].font.color.rgb = MGRAY
if current and total:
add_progress_bar(slide, prs, current, total)
return slide
# ── 4. Regression table slide ────────────────────────────────────
def make_table_slide(prs, title, headers, rows,
footnote="", highlight_last_col=True,
current=None, total=None):
"""
headers: list of str (first col is row label).
rows: list of lists of str.
Last column is treated as the preferred specification (bolded).
"""
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, LGRAY)
add_frame_title(slide, prs, title)
nc = len(headers); nr = len(rows) + 1
tbl = slide.shapes.add_table(
nr, nc,
Inches(0.5), Inches(1.4),
Inches(12.33), Inches(4.5)).table
# Header row — navy background, white bold text
for j, h in enumerate(headers):
c = tbl.cell(0, j)
c.text = h
c.text_frame.paragraphs[0].font.bold = True
c.text_frame.paragraphs[0].font.size = Pt(14)
c.text_frame.paragraphs[0].font.color.rgb = WHITE
c.fill.solid(); c.fill.fore_color.rgb = NAVY
# Data rows
for i, row in enumerate(rows):
for j, val in enumerate(row):
c = tbl.cell(i + 1, j)
c.text = str(val)
c.text_frame.paragraphs[0].font.size = Pt(13)
if highlight_last_col and j == nc - 1:
c.text_frame.paragraphs[0].font.bold = True
if footnote:
fn = slide.shapes.add_textbox(
Inches(0.5), Inches(6.0), Inches(12.33), Inches(1.2))
fn.text_frame.paragraphs[0].text = footnote
fn.text_frame.paragraphs[0].font.size = Pt(10)
fn.text_frame.paragraphs[0].font.color.rgb = MGRAY
if current and total:
add_progress_bar(slide, prs, current, total)
return slide
# ── 5. Two-column slide ──────────────────────────────────────────
def make_two_col_slide(prs, title, left_bullets, right_bullets,
current=None, total=None):
"""Two-column layout (e.g. Robustness slide)."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, prs, LGRAY)
add_frame_title(slide, prs, title)
for col_bullets, left_offset in [(left_bullets, 0.4),
(right_bullets, 6.9)]:
tb = slide.shapes.add_textbox(
Inches(left_offset), Inches(1.35),
Inches(5.8), Inches(5.8))
tf = tb.text_frame; tf.word_wrap = True
for i, (lvl, text) in enumerate(col_bullets):
p = tf.paragraphs[i] if i == 0 else tf.add_paragraph()
p.text = text; p.level = lvl
p.font.size = Pt(18 if lvl == 0 else 15)
p.font.color.rgb = BLACK
p.space_before = Pt(6 if lvl == 0 else 3)
if current and total:
add_progress_bar(slide, prs, current, total)
return slide
```
### PDF → PNG Conversion (for figures from /plot)
```python
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):
# Fallback: pdf2image
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
```python
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)
# ── Verify PPTX ──────────────────────────────────────────────
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")
# ── Export PDF ───────────────────────────────────────────────
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}")
print(f" {os.path.getsize(pdf_path) // 1024} KB")
return pdf_path
else:
print(f"⚠️ LibreOffice conversion failed: {result.stderr.strip()}")
print(" → Open slides.pptx in PowerPoint and export manually.")
return None
except FileNotFoundError:
print("⚠️ LibreOffice not found. Install with:")
print(" apt-get install -y libreoffice # Ubuntu/Debian")
print(" brew install --cask libreoffice # macOS")
print(" → You can also export PDF from PowerPoint / LibreOffice Impress.")
return None
except subprocess.TimeoutExpired:
print("⚠️ LibreOffice timed out. Try running manually:")
print(f" libreoffice --headless --convert-to pdf {pptx_path}")
return None
```
---
## 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
1. **One message per slide** — split if content overflows
2. **Use figures over tables** — embed PNG at ≥ 200 DPI
3. **Bold the preferred specification** column in regression tables
4. **Add speaker notes** to every key slide via `add_speaker_notes()`
5. **Prepare appendix slides** for anticipated Q&A
6. **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
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!