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:
- Rich text — bold, italic, underline, strikethrough, superscript, subscript, color, font, alignment, bullets, paragraph spacing
- Charts — column (clustered/stacked), bar (clustered/stacked), line, line with markers, pie, XY, Radar — all fully editable
- Chart formatting — titles, legends, data labels, axis customization (min/max, gridlines, number format), per-series colors
- Images — PNG and JPEG with automatic format detection, masking, rotations
- Tables — rows and columns with rich text cells, cell merging, styling, full border control, etc
- Placeholders — title, subtitle, and body on standard slide layouts
- Shape styling — solid, gradient, pattern fills and lines with configurable width
- Slide dimensions — 16:9 default, fully configurable
- Extras – Speaker's notes, footers, document metadata
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"}
]
endUsage
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 EMUDemos
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.