Powerpointer: Write Presentations in Markdown, Export to PowerPoint
Have you ever spent more time fighting PowerPoint formatting than actually writing your slides? md_to_pptx.py — which I’m calling Powerpointer — flips that around: you write your presentation as a plain Markdown file and get a polished .pptx out the other end.
Because every slide uses PowerPoint’s standard Title and Content placeholders, you can apply any theme afterwards and the fonts and colours update automatically.
How it works
The pipeline is straightforward:
mistuneparses the Markdown into an abstract syntax tree.- A custom AST walker converts every node into one of three internal types:
Paragraph,Table, orPicture. python-pptxwrites each slide using those objects.- Diagrams and code blocks are rasterised to PNG first so they embed cleanly.
Installation
git clone https://github.com/your-username/Powerpointer.git
cd Powerpointer
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS / Linux
pip install -r requirements.txt
Dependencies:
| Package | Role |
|---|---|
python-pptx | Creates and writes .pptx files |
mistune | Parses Markdown to an AST |
Pillow | Scales images and renders code blocks |
requests | Fetches remote images and Mermaid PNGs |
pygments | Syntax-highlights code fences |
matplotlib | Renders LaTeX math ($$…$$ blocks) |
Basic usage
# Minimal
python md_to_pptx.py slides.md presentation.pptx
# Apply a corporate theme
python md_to_pptx.py slides.md presentation.pptx --template brand.pptx
# Break slides on every # heading instead of ---
python md_to_pptx.py slides.md presentation.pptx --split h1
# Behind a proxy that performs SSL inspection
python md_to_pptx.py slides.md presentation.pptx --insecure
Slide-splitting modes
flowchart TD
rule["--split rule (default)\nnew slide on ---"]
h1["--split h1\nnew slide on --- or # heading"]
h2["--split h2\nnew slide on ---, # heading, or ## heading"]
rule --> h1 --> h2
Writing slides in Markdown
Title slide
# My Presentation
> Subtitle or author name
The first slide automatically uses the Title Slide layout. A blockquote immediately below the title becomes the subtitle.
Bullets and inline formatting
## Agenda
- Top-level bullet
- Nested bullet (indent with 2 spaces)
- Deeply nested
- **Bold**, *italic*, `monospace`
Nesting is unlimited; each extra indent level increases the PowerPoint bullet level.
Tables
Standard GFM pipe tables render with a styled header row and alternating row colours:
## Tool comparison
| Tool | Input | Output |
|-------------|------------|--------|
| Powerpointer | Markdown | .pptx |
| Marp | Markdown | HTML / PDF |
| reveal.js | HTML / MD | Browser slides |
Mermaid diagrams
Wrap any Mermaid code in a fenced block tagged mermaid. The script calls mermaid.ink at 2 000 px wide and embeds the PNG:
```mermaid
sequenceDiagram
User->>Script: python md_to_pptx.py slides.md out.pptx
Script->>mermaid.ink: POST diagram code
mermaid.ink-->>Script: PNG bytes
Script->>out.pptx: embed Picture shape
```
Which, rendered here, looks like:
sequenceDiagram
User->>Script: python md_to_pptx.py slides.md out.pptx
Script->>mermaid.ink: POST diagram code
mermaid.ink-->>Script: PNG bytes
Script->>out.pptx: embed Picture shape
Syntax-highlighted code blocks
Any fenced code block with a language tag is rendered to a PNG with a Monokai dark theme using Pygments + Pillow and embedded as a picture shape — so it looks exactly like a code editor screenshot, without any manual formatting:
```python
def greet(name: str) -> str:
return f"Hello, {name}!"
```
LaTeX math
Display-math blocks delimited by $$ are rendered to PNG via matplotlib mathtext:
$$
E = mc^2
$$
No internet connection required for math — it all happens locally.
Images


A slide containing only a single image fills the entire content area. When a slide has both text and visuals, the content area is split ~40 / 60 vertically.
Under the hood: the Mermaid encoding
One interesting implementation detail is how diagrams are sent to mermaid.ink. The service accepts diagram code as a URL segment, so it must be encoded. The modern pako: format compresses the JSON payload with zlib (matching JavaScript’s pako.deflate()) for shorter URLs and better Unicode support:
def _encode_mermaid_pako(code: str) -> str:
payload = json.dumps({"code": code, "mermaid": {"theme": "default"}})
# zlib.compress uses wbits=15 — a zlib stream with header + Adler-32,
# which matches pako.deflate() in JS (NOT raw DEFLATE with wbits=-15).
compressed = zlib.compress(payload.encode("utf-8"))
return base64.urlsafe_b64encode(compressed).decode().rstrip("=")
def render_mermaid(code: str) -> Optional[bytes]:
pako_encoded = _encode_mermaid_pako(code)
url = f"https://mermaid.ink/img/pako:{pako_encoded}?type=png&width=2000"
data = _fetch_with_ssl_retry(url)
if data:
return data
# Fallback: plain base64 of raw mermaid code (legacy format)
encoded = base64.urlsafe_b64encode(code.encode()).decode()
url = f"https://mermaid.ink/img/{encoded}?type=png&width=2000"
return _fetch_with_ssl_retry(url)
The script also auto-retries without TLS verification on SSL errors, which is useful behind corporate proxies — or you can pass --insecure explicitly.
Applying a theme
After generating the file, open it in PowerPoint:
- Design → Themes — pick any built-in theme.
- Because every text block sits in a standard placeholder, the theme’s fonts and accent colours apply in one click.
- To start from your own master slide instead, pass it via
--template your_theme.pptx.
Project structure
Powerpointer/
├── md_to_pptx.py # Main converter (~700 lines)
├── requirements.txt
├── example.md # Demo presentation
└── example_output.pptx # Pre-generated sample output
The full source is on GitHub. Drop a ⭐ if it saves you an hour of slide-wrangling.


Leave a Comment