Skip to main content

Render a deck-spec Deck to PPTX, PDF, and HTML — three renderers, one API.

Project description

slide-render

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.


What it solves

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

Installation

pip install slide-render
playwright install chromium  # for PDF export

Optional 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, build

Runtime requirements:

  • Python >= 3.10
  • Playwright Chromium browser (PDF only; PPTX and HTML do not need it)

Quick start

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")

How it works

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 variant

Themes

Four 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"))

Configuration

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

CLI

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.pptx

List available themes:

slide-render themes

Validate before rendering:

slide-render deck.json --validate --output audit.pdf

Package structure

src/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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

slide_render-0.1.0.tar.gz (26.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

slide_render-0.1.0-py3-none-any.whl (44.1 kB view details)

Uploaded Python 3

File details

Details for the file slide_render-0.1.0.tar.gz.

File metadata

  • Download URL: slide_render-0.1.0.tar.gz
  • Upload date:
  • Size: 26.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for slide_render-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2194513a367fd1f959b9ea8b1000600a11b1139bdb4431bdfc777e65d78a4967
MD5 654edef0fa81b7c574a2d4fe4c436062
BLAKE2b-256 20a2179c02c55bf538010314da0ded6c2e7b214d27d90ce357ff33cc6f270a1c

See more details on using hashes here.

Provenance

The following attestation bundles were made for slide_render-0.1.0.tar.gz:

Publisher: release.yml on tomastimelock/slide-render

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file slide_render-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: slide_render-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 44.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for slide_render-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5fece896109b9b8d1c5757f7f0c70ebb6c8e0e86965e06164fe14ff73f697c94
MD5 d0e546fd858ced1c7ff06673a8c52f5c
BLAKE2b-256 58c3108133f049f979249054568972bfb09b86038387ba1200a66583ec55a678

See more details on using hashes here.

Provenance

The following attestation bundles were made for slide_render-0.1.0-py3-none-any.whl:

Publisher: release.yml on tomastimelock/slide-render

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page