Render a deck-spec Deck to PPTX, PDF, and HTML — three renderers, one API.
Built at Trollfabriken AITrix AB for the AIMOS Insight municipal-audit explainer pipeline. One Deck object compiles to a PowerPoint handout for kommun officials, a PDF for the public record, and an HTML embed for the news site — same content, three formats, single source of truth. PPTX uses python-pptx, PDF uses Playwright Chromium, HTML is the lingua franca underneath.
| Previous problem | Solution |
|---|---|
python-pptx requires 60+ lines per slide for consulting-quality output |
Eleven prebuilt layouts; declarative Slide(layout="content", title=..., body=...) |
| Marp/Slidev give HTML/PDF but not editable PPTX | Three renderers, one Deck source of truth |
| Aspose/Spire are paid commercial with a .NET runtime | MIT, pure Python, no .NET, no licence cost |
| LibreOffice headless PDF export is slow and fragile across versions | Playwright Chromium prints from our HTML; deterministic |
| Adding a custom theme means editing every renderer | One theme directory with CSS + optional PPTX template; all three renderers pick it up |
| Swedish characters break in naive PPTX writers | UTF-8 throughout; tested with Swedish fixtures |
| Slide layouts diverge between PPTX and PDF outputs | PDF is the HTML rendering printed by Chromium; same visual everywhere except PPTX |
| Markdown-in-body needs preprocessing | Markdown spans (**bold**, *italic*, `code`) handled directly in renderers |
pip install slide-render
playwright install chromium # for PDF exportOptional extras:
pip install slide-render[images] # Pillow for image manipulation
pip install slide-render[fonts] # fonttools for custom font handling
pip install slide-render[dev] # pytest, ruff, pypdf, buildRuntime requirements:
- Python >= 3.10
- Playwright Chromium browser (PDF only; PPTX and HTML do not need it)
from deck_spec import Deck, Slide
from slide_render import render
deck = Deck(
title="Q1 Audit Findings",
slides=[
Slide(layout="title", title="Q1 Audit Findings",
subtitle="Uddevalla kommun · 2026"),
Slide(layout="stat", title="Beslut utan beslutsunderlag",
stat_value="34%", stat_label="av granskade ärenden",
stat_supporting="Mot mål 0%"),
Slide(layout="bullet", title="Rekommendationer",
bullets=[
"Rättsutredning före varje beslut",
"Digital signering av beslutsunderlag",
"Stickprovskontroll månadsvis",
]),
Slide(layout="comparison", title="Före vs efter",
columns=[
{"heading": "Före", "bullets": ["Manuell registrering",
"Spridda filer"]},
{"heading": "Efter", "bullets": ["Automatisk loggning",
"Central databas"]},
]),
],
)
# Three outputs, one source
render(deck, "audit.pptx")
render(deck, "audit.pdf")
render(deck, "audit_html/", format="html")Format is auto-detected from the output path suffix. Pass format= to override.
Deck ──► slide-render ──► PPTX (python-pptx; native shapes and master layouts)
├──► HTML (Jinja2 templates; one section per slide)
└──► PDF (HTML rendered above, printed by Playwright Chromium)
PDF shares the HTML rendering path. One layout codebase covers both. Adding a new layout requires one HTML template; PPTX gets its own dedicated builder because python-pptx's placeholder model is incompatible with CSS layout.
Lower-level entry points:
from slide_render.pptx import render_pptx
from slide_render.pdf import render_pdf
from slide_render.html import render_html, render_html_string
render_pptx(deck, "out.pptx", config=...)
render_pdf(deck, "out.pdf", config=...)
render_html(deck, "out_dir/", config=...) # writes index.html + assets/
html_str = render_html_string(deck, config=...) # single-string variantFour themes ship with the package. Each is a directory under slide_render/themes/.
| Theme | Description |
|---|---|
default |
Clean modern style. Blue accent, white background. |
civic |
Serious tone for municipal and legal content. Navy accent, generous margins. |
audit |
Report-style. Accent on numbers. Suited to AIMOS Insight scorecards. |
explainer |
Educational, friendly, larger type. Used for parent-facing guides. |
One Deck renders cleanly in any of the four themes. Theme is a runtime choice:
from slide_render import render, RenderConfig, list_themes
print(list_themes()) # ["default", "civic", "audit", "explainer"]
render(deck, "audit.pptx", config=RenderConfig(theme_name="civic"))RenderConfig is a Pydantic v2 model. All fields are optional.
from slide_render import RenderConfig
config = RenderConfig(
# Universal
theme_name="civic", # override the theme named in the Deck
fonts_dir=Path("./fonts"), # extra font directory to make available
# PPTX
pptx_template=Path("base.pptx"), # clone from an existing .pptx template
pptx_master_layout=True, # use master slide layouts vs raw shapes
# PDF
pdf_page_size="16:9", # "16:9" | "4:3" | "A4" | "letter"
pdf_print_background=True,
pdf_orientation="landscape", # "landscape" | "portrait"
pdf_chromium_executable=None, # override Playwright's Chromium path
# HTML
html_standalone=True, # inline all CSS; no external files
html_reveal_compat=False, # emit Reveal.js-compatible <section> structure
html_include_speaker_notes=False, # add <aside class="notes">
# Shared
verbose=False,
)| Field | Default | Effect |
|---|---|---|
theme_name |
None |
Overrides deck.theme.name |
pdf_page_size |
"16:9" |
Sets Chromium paper size |
html_standalone |
True |
Produces a single self-contained file |
html_reveal_compat |
False |
Wraps slides in Reveal.js <section> tags |
pptx_template |
None |
Starts from an existing .pptx instead of blank |
verbose |
False |
Prints per-slide progress to stdout |
Auto-detect format from extension:
slide-render deck.json --output audit.pptx
slide-render deck.json --output audit.pdf
slide-render deck.json --output audit_html/Multiple outputs in one pass:
slide-render deck.json --output audit.pptx --output audit.pdf --output audit_html/Theme override:
slide-render deck.json --theme civic --output audit.pptxList available themes:
slide-render themesValidate before rendering:
slide-render deck.json --validate --output audit.pdfsrc/slide_render/
├── __init__.py ← render() dispatcher and public exports
├── config.py ← RenderConfig (Pydantic v2)
├── cli.py ← CLI entry point
├── dispatch.py ← auto-detect format from output path
├── exceptions.py ← SlideRenderError and subclasses
├── themes/
│ ├── __init__.py
│ ├── registry.py ← discover and load themes
│ ├── default/ ← theme.json, styles.css, pptx_template.pptx
│ ├── civic/
│ ├── audit/
│ └── explainer/
├── pptx/
│ ├── __init__.py
│ ├── renderer.py ← render_pptx orchestrator
│ ├── layouts.py ← map deck-spec layout → python-pptx layout
│ ├── elements.py ← text, image, shape, table emit functions
│ ├── theme_apply.py ← apply theme colours/fonts to slide master
│ └── notes.py ← write speaker notes to slides
├── html/
│ ├── __init__.py
│ ├── renderer.py ← render_html and render_html_string
│ ├── assets.py ← copy/inline CSS, images, fonts
│ ├── jinja_env.py ← Jinja2 environment with StrictUndefined
│ └── templates/
│ ├── deck.html.j2 ← outer HTML; one section per slide
│ ├── base.css.j2 ← base styling rules
│ ├── reveal_compat.html.j2
│ └── layouts/ ← one .html.j2 per layout name
│ ├── title.html.j2
│ ├── content.html.j2
│ ├── bullet.html.j2
│ ├── image.html.j2
│ ├── image_caption.html.j2
│ ├── two_column.html.j2
│ ├── comparison.html.j2
│ ├── stat.html.j2
│ ├── quote.html.j2
│ ├── section.html.j2
│ └── blank.html.j2
├── pdf/
│ ├── __init__.py
│ ├── renderer.py ← render_pdf: HTML render then Chromium print
│ └── chromium.py ← Playwright launch and page.pdf() call
└── shared/
├── __init__.py
├── element_helpers.py ← image loading, base64 decode, file:// URIs
├── color_utils.py ← palette key resolution ("accent1" → hex)
└── text_format.py ← markdown span handling (bold, italic, code)
© Trollfabriken AITrix AB — MIT licensed