Podium

Podium is a comprehensive Powerpoint Generation Library for Elixir, ported from Python's python-pptx (with huge thanks).

Podium's Powerpoint support is feature complete beyond most common use-cases, including:

Quick start

alias Podium.Chart.ChartData

chart_data =
  ChartData.new()
  |> ChartData.add_categories(["Q1", "Q2", "Q3", "Q4"])
  |> ChartData.add_series("Revenue", [1500, 4600, 5156, 3167], color: "4472C4")
  |> ChartData.add_series("Expenses", [1000, 2300, 2500, 3000], color: "ED7D31")

slide =
  Podium.Slide.new()
  |> Podium.add_text_box([
    {[{"Quarterly Report", bold: true, font_size: 36, color: "003366"}], alignment: :center}
  ], x: {1, :inches}, y: {0.5, :inches}, width: {10, :inches}, height: {1, :inches})
  |> Podium.add_chart(:column_clustered, chart_data,
    x: {1, :inches}, y: {2, :inches}, width: {10, :inches}, height: {4.5, :inches},
    title: "Revenue vs Expenses",
    legend: :bottom,
    data_labels: [:value]
  )

Podium.new()
|> Podium.add_slide(slide)
|> Podium.save("report.pptx")

Installation

Add podium to your dependencies in mix.exs:

def deps do
  [
    {:podium, "~> 0.1.0"}
  ]
end

Usage

Presentations and slides

# 16:9 (default)
prs = Podium.new()

# Custom dimensions
prs = Podium.new(slide_width: {10, :inches}, slide_height: {7.5, :inches})

# Create slides with different layouts
blank = Podium.Slide.new()                    # blank
title = Podium.Slide.new(:title_slide)        # title + subtitle
content = Podium.Slide.new(:title_content)    # title + body

# Add slides to a presentation
prs
|> Podium.add_slide(blank)
|> Podium.add_slide(title)
|> Podium.add_slide(content)

Rich text

Plain strings work for simple cases. For formatting, pass a list of paragraphs:

# Simple
slide = Podium.add_text_box(slide, "Hello", x: {1, :inches}, y: {1, :inches},
  width: {4, :inches}, height: {1, :inches}, font_size: 24)

# Rich — multiple paragraphs with per-run formatting
slide = Podium.add_text_box(slide, [
  [{"Title", bold: true, font_size: 28, color: "003366"}],
  [{"By ", font_size: 14}, {"Engineering", bold: true, italic: true}]
], x: {1, :inches}, y: {1, :inches}, width: {8, :inches}, height: {2, :inches},
   alignment: :center)

# Per-paragraph alignment
slide = Podium.add_text_box(slide, [
  {[{"Heading", bold: true}], alignment: :center},
  {[{"Body text here"}], alignment: :left}
], x: {1, :inches}, y: {1, :inches}, width: {8, :inches}, height: {2, :inches})

Run options: bold, italic, underline, strikethrough, superscript, subscript, font_size, color (hex RGB), font.

Paragraph spacing and bullets

Paragraph-level options go in the tuple form {runs, opts}:

slide = Podium.add_text_box(slide, [
  {[{"Spaced heading", bold: true}], line_spacing: 1.5, space_after: 12},
  {["Bullet item one"], bullet: true},
  {["Sub-item"], bullet: true, level: 1},
  {["Custom bullet"], bullet: "–"},
  {["Step one"], bullet: :number},
  {[{"E=mc", font_size: 16}, {"2", font_size: 12, superscript: true}], space_before: 6}
], x: {1, :inches}, y: {1, :inches}, width: {8, :inches}, height: {4, :inches})

Paragraph options: alignment, line_spacing (multiplier, e.g. 1.5), space_before / space_after (points), bullet (true, a custom character, or :number), level (0-based indent).

Shape fills and lines

slide = Podium.add_text_box(slide, "Alert!", x: {1, :inches}, y: {1, :inches},
  width: {4, :inches}, height: {1, :inches},
  fill: "FF0000",
  line: [color: "000000", width: {2, :pt}])

Charts

29 chart types across 10 families: column, bar, line, pie, area, doughnut, radar, scatter, bubble, and combo.

chart_data =
  ChartData.new()
  |> ChartData.add_categories(["North America", "Europe", "Asia"])
  |> ChartData.add_series("2024", [42, 28, 18], color: "4472C4")
  |> ChartData.add_series("2025", [48, 32, 25], color: "ED7D31")

slide =
  Podium.Slide.new()
  |> Podium.add_chart(:pie, chart_data,
    x: {1, :inches}, y: {1, :inches}, width: {8, :inches}, height: {5, :inches},
    title: "Market Share",
    legend: :right,                              # :left | :right | :top | :bottom | false
    data_labels: [:category, :percent],          # :value | :category | :series | :percent
    category_axis: [title: "Region"],
    value_axis: [
      title: "Share (%)",
      number_format: "0%",
      min: 0, max: 100, major_unit: 25,
      major_gridlines: true                      # default true, set false to hide
    ]
  )

Images

slide = Podium.add_image(slide, File.read!("logo.png"),
  x: {1, :inches}, y: {1, :inches}, width: {3, :inches}, height: {2, :inches})

Format is auto-detected from file magic bytes (PNG and JPEG supported).

Tables

slide = Podium.add_table(slide, [
  ["Name",  "Q1",  "Q2",  "Q3" ],
  ["Alice", "100", "200", "300"],
  ["Bob",   "150", "250", "350"]
], x: {1, :inches}, y: {2, :inches}, width: {8, :inches}, height: {3, :inches})

Cells accept the same text formats as add_text_box — plain strings or rich text lists.

Placeholders

slide =
  Podium.Slide.new(:title_slide)
  |> Podium.set_placeholder(:title, "Annual Report 2025")
  |> Podium.set_placeholder(:subtitle, "Engineering Division")

Available layouts and their placeholders:

Layout Placeholders
:title_slide:title, :subtitle
:title_content:title, :content
:section_header:title, :body
:two_content:title, :left_content, :right_content
:comparison:title, :left_heading, :left_content, :right_heading, :right_content
:title_only:title
:blank (none)
:content_caption:title, :content, :caption
:picture_caption:title, :picture, :caption
:title_vertical_text:title, :body
:vertical_title_text:title, :body

Saving

# To file
:ok = Podium.save(prs, "output.pptx")

# To memory (for streaming, uploads, etc.)
{:ok, binary} = Podium.save_to_memory(prs)

Units

All position and size values accept {number, unit} tuples or raw EMU integers:

{1, :inches}   # 914,400 EMU
{2.54, :cm}    # 914,400 EMU
{72, :pt}      # 914,400 EMU
914_400         # raw EMU

Demos

The demos/ directory has scripts covering every feature. Run any of them to generate a .pptx file in demos/output/:

mix run demos/getting-started.exs

Integration tests also produce viewable .pptx files in test/podium/integration/output/ when you run mix test.

Acknowledgments

Podium's design and feature set are ported from python-pptx by Steve Canny. Without python-pptx as a reference, this library would not exist.

License

MIT — see LICENSE.