beamer-ppt
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.
Install
mkdir -p .claude/skills/beamer-ppt && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/13451" && unzip -o skill.zip -d .claude/skills/beamer-ppt && rm skill.zipInstalls to .claude/skills/beamer-ppt
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
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.About this skill
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
/presentcommand to produce the finalslides/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
# ── 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
# ── 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)
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:
---
*Content truncated.*