# 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
```elixir
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`:
```elixir
def deps do
[
{:podium, "~> 0.1.0"}
]
end
```
## Usage
### Presentations and slides
```elixir
# 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:
```elixir
# 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}`:
```elixir
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
```elixir
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.
```elixir
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
```elixir
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
```elixir
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
```elixir
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
```elixir
# 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:
```elixir
{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](https://github.com/scanny/python-pptx) by Steve Canny. Without python-pptx as a reference, this library would not exist.
## License
MIT — see [LICENSE](LICENSE).