Packages

PowerPoint (.pptx) generation for Elixir with editable charts

Current section

Files

Jump to
podium lib podium.ex
Raw

lib/podium.ex

defmodule Podium do
@moduledoc """
Elixir library for generating PowerPoint (.pptx) files with editable charts.
Podium provides a pipe-friendly API for building presentations programmatically.
Create slides independently with `Podium.Slide.new/1`, populate them with
content using `add_chart/4`, `add_image/3`, `add_text_box/3`, etc., then
add them to a presentation with `add_slide/2` and save.
## Example
alias Podium.Chart.ChartData
data =
ChartData.new()
|> ChartData.add_categories(["Q1", "Q2", "Q3"])
|> ChartData.add_series("Revenue", [100, 200, 300])
slide =
Podium.Slide.new(:title_content)
|> Podium.add_chart(:column_clustered, data, x: {1, :inches}, y: {2, :inches}, width: {8, :inches}, height: {4, :inches})
|> Podium.add_text_box("Hello", x: {1, :inches}, y: {6, :inches}, width: {4, :inches}, height: {1, :inches})
Podium.new()
|> Podium.add_slide(slide)
|> Podium.save("output.pptx")
## Text Formatting Reference
Text content can be a plain string or rich text (a list of paragraphs).
### Run options
Each run is a `{text, opts}` tuple. Available options:
* `:font_size` - font size in points (e.g. `12`)
* `:bold` - `true` to bold
* `:italic` - `true` to italicize
* `:underline` - `true` for single underline, or one of `:single`, `:double`,
`:heavy`, `:dotted`, `:dotted_heavy`, `:dash`, `:dash_heavy`, `:dash_long`,
`:dash_long_heavy`, `:dot_dash`, `:dot_dash_heavy`, `:dot_dot_dash`,
`:dot_dot_dash_heavy`, `:wavy`, `:wavy_heavy`, `:wavy_double`, `:words`
* `:strikethrough` - `true` for single strikethrough
* `:superscript` - `true` for superscript
* `:subscript` - `true` for subscript
* `:color` - hex RGB string (e.g. `"FF0000"`)
* `:font` - font family name (e.g. `"Arial"`)
* `:hyperlink` - URL string, `[url: "...", tooltip: "..."]`,
`{:slide, slide}` for slide-to-slide links, or an action atom
(`:next_slide`, `:previous_slide`, `:first_slide`, `:last_slide`, `:end_show`)
* `:lang` - language code (default `"en-US"`)
### Paragraph options
When using `{runs, opts}` tuple paragraphs:
* `:alignment` - `:left`, `:center`, `:right`, or `:justify`
* `:line_spacing` - line spacing multiplier (e.g. `1.5`)
* `:space_before` - space before paragraph in points
* `:space_after` - space after paragraph in points
* `:bullet` - `true` for bullet, `:number` for numbered, a character string
for custom bullet, or `false` to suppress
* `:level` - indentation level (0-based)
### Fill types
* Hex string - `"4472C4"` for solid fill
* `{:gradient, stops, opts}` - gradient fill where stops are `[{position, color}, ...]`
and opts include `:angle` (default `5_400_000` = 90 degrees in 60,000ths)
* `{:pattern, preset, opts}` - pattern fill, see `Podium.Pattern` for presets;
opts: `:foreground`, `:background` (hex color strings)
* `{:picture, binary}` - picture fill from image binary data
### Line types
* Hex string - `"000000"` for a line with default width
* Keyword list - `[color: "000000", width: {2, :pt}, dash_style: :dash]`
* Dash styles: `:solid`, `:dash`, `:dot`, `:dash_dot`, `:long_dash`,
`:long_dash_dot`, `:long_dash_dot_dot`, `:sys_dot`, `:sys_dash`,
`:sys_dash_dot`, `:sys_dash_dot_dot`
"""
@type emu :: non_neg_integer()
@type dimension :: emu() | {number(), :inches | :cm | :pt}
@type hex_color :: String.t()
@type fill ::
hex_color()
| {:gradient, [{non_neg_integer(), hex_color()}], keyword()}
| {:pattern, atom(), keyword()}
| {:picture, binary()}
| nil
@type line :: hex_color() | keyword() | nil
@type alignment :: :left | :center | :right | :justify
@type anchor :: :top | :middle | :bottom
@type run :: String.t() | {String.t(), keyword()}
@type paragraph :: [run()] | {[run()], keyword()}
@type rich_text :: String.t() | [paragraph()]
@type layout ::
:title_slide
| :title_content
| :section_header
| :two_content
| :comparison
| :title_only
| :blank
| :content_caption
| :picture_caption
| :title_vertical_text
| :vertical_title_text
| pos_integer()
@type chart_type ::
:column_clustered
| :column_stacked
| :column_stacked_100
| :bar_clustered
| :bar_stacked
| :bar_stacked_100
| :line
| :line_markers
| :line_stacked
| :line_markers_stacked
| :line_stacked_100
| :line_markers_stacked_100
| :pie
| :pie_exploded
| :area
| :area_stacked
| :area_stacked_100
| :doughnut
| :doughnut_exploded
| :radar
| :radar_filled
| :radar_markers
| :scatter
| :scatter_lines
| :scatter_lines_no_markers
| :scatter_smooth
| :scatter_smooth_no_markers
| :bubble
| :bubble_3d
@type connector_type :: :straight | :elbow | :curved
alias Podium.{Presentation, Slide}
@doc """
Creates a new presentation.
## Options
* `:slide_width` - slide width, default 16:9 (12_192_000 EMU). Accepts EMU integer or `{value, unit}`.
* `:slide_height` - slide height, default 16:9 (6_858_000 EMU). Accepts EMU integer or `{value, unit}`.
* `:title` - document title string
* `:author` - document author string
* `:subject` - document subject string
* `:keywords` - keywords string
* `:category` - category string
* `:comments` - comments/description string
* `:last_modified_by` - last modified by string
* `:created` - `DateTime` for creation timestamp
* `:modified` - `DateTime` for modification timestamp
* `:last_printed` - `DateTime` for last printed timestamp
* `:revision` - revision number (integer)
* `:content_status` - content status string
* `:language` - language code string
* `:version` - version string
"""
@spec new(keyword()) :: Podium.Presentation.t()
def new(opts \\ []) do
Presentation.new(opts)
end
@doc """
Adds a slide to the presentation. Returns the updated presentation.
Slides are created independently with `Podium.Slide.new/1`, populated
with content, then added to the presentation.
## Example
slide = Podium.Slide.new(:title_content)
prs = Podium.add_slide(prs, slide)
"""
@spec add_slide(Podium.Presentation.t(), Podium.Slide.t()) :: Podium.Presentation.t()
def add_slide(prs, slide) do
Presentation.add_slide(prs, slide)
end
@doc """
Adds a text box to a slide.
## Options (required)
* `:x` - horizontal position (EMU integer or `{value, unit}`)
* `:y` - vertical position
* `:width` - box width
* `:height` - box height
## Options (optional)
* `:fill` - fill color or fill tuple (see module doc)
* `:line` - line color or line opts (see module doc)
* `:rotation` - rotation in degrees
* `:margin_left`, `:margin_right`, `:margin_top`, `:margin_bottom` - text margins
* `:anchor` - vertical text anchor: `:top`, `:middle`, or `:bottom`
* `:auto_size` - `:none`, `:text_to_fit_shape`, or `:shape_to_fit_text`
* `:word_wrap` - `true` (default) or `false`
* `:alignment` - default text alignment: `:left`, `:center`, `:right`, `:justify`
* `:font_size` - default font size in points
"""
@spec add_text_box(Podium.Slide.t(), rich_text(), keyword()) :: Podium.Slide.t()
def add_text_box(slide, text, opts) do
Slide.add_text_box(slide, text, opts)
end
@doc """
Adds an auto shape to a slide.
## Parameters
* `preset` - shape type atom (see `Podium.AutoShapeType` for all 187 presets)
## Options (required)
* `:x`, `:y`, `:width`, `:height` - position and size
## Options (optional)
* `:text` - text content for the shape (string or rich text)
* `:fill` - fill (omit to use theme default)
* `:line` - line color or line opts
* `:rotation` - rotation in degrees
* `:margin_left`, `:margin_right`, `:margin_top`, `:margin_bottom` - text margins
* `:anchor` - vertical text anchor
* `:auto_size` - auto sizing behavior
* `:word_wrap` - word wrap setting
* `:alignment` - default text alignment
* `:font_size` - default font size in points
"""
@spec add_auto_shape(Podium.Slide.t(), atom(), keyword()) :: Podium.Slide.t()
def add_auto_shape(slide, preset, opts) do
Slide.add_auto_shape(slide, preset, opts)
end
@doc """
Adds a connector between two points on a slide.
## Parameters
* `connector_type` - `:straight`, `:elbow`, or `:curved`
* `begin_x`, `begin_y` - start point coordinates
* `end_x`, `end_y` - end point coordinates
## Options
* `:line` - line color string or keyword list with `:color`, `:width`, `:dash_style`
"""
@spec add_connector(
Podium.Slide.t(),
connector_type(),
dimension(),
dimension(),
dimension(),
dimension(),
keyword()
) :: Podium.Slide.t()
def add_connector(slide, connector_type, begin_x, begin_y, end_x, end_y, opts \\ []) do
Slide.add_connector(slide, connector_type, begin_x, begin_y, end_x, end_y, opts)
end
@doc """
Adds a chart to a slide. Returns the updated slide.
## Parameters
* `chart_type` - chart type atom (see `Podium.Chart.ChartType` for all types)
* `chart_data` - `Podium.Chart.ChartData`, `Podium.Chart.XyChartData`, or
`Podium.Chart.BubbleChartData` struct
## Options (required)
* `:x`, `:y`, `:width`, `:height` - position and size
## Options (optional)
* `:title` - chart title as string, or keyword list with `:text`, `:font_size`,
`:bold`, `:italic`, `:color`, `:font`
* `:legend` - legend position atom (`:left`, `:right`, `:top`, `:bottom`) or `false`
to hide, or keyword list with `:position`, `:font_size`, `:bold`, `:italic`,
`:color`, `:font`
* `:data_labels` - list of atoms to show (`:value`, `:category`, `:series`, `:percent`)
or keyword list with `:show` (list), `:position` (`:center`, `:inside_end`,
`:inside_base`, `:outside_end`, `:top`, `:bottom`, `:left`, `:right`, `:best_fit`),
`:number_format` (Excel format string), `:color` (hex color for label text)
* `:category_axis` - keyword list with `:title`, `:type` (`:date` for date axis),
`:number_format`, `:major_gridlines`, `:minor_gridlines`, `:visible`, `:reverse`,
`:crosses`, `:label_rotation`, `:major_tick_mark`, `:minor_tick_mark`,
`:base_time_unit`, `:major_time_unit`, `:minor_time_unit`, `:major_unit`, `:minor_unit`,
`:color` (hex color for tick label text), `:line_color` (hex color for axis line)
* `:value_axis` - keyword list with `:title`, `:number_format`, `:major_gridlines`,
`:minor_gridlines`, `:min`, `:max`, `:major_unit`, `:minor_unit`, `:visible`,
`:reverse`, `:crosses`, `:label_rotation`, `:major_tick_mark`, `:minor_tick_mark`,
`:color` (hex color for tick label text), `:line_color` (hex color for axis line),
`:major_gridlines_color`, `:minor_gridlines_color` (hex colors for gridlines)
"""
@spec add_chart(
Podium.Slide.t(),
chart_type(),
Podium.Chart.ChartData.t()
| Podium.Chart.XyChartData.t()
| Podium.Chart.BubbleChartData.t(),
keyword()
) :: Podium.Slide.t()
def add_chart(slide, chart_type, chart_data, opts) do
Slide.add_chart(slide, chart_type, chart_data, opts)
end
@doc """
Adds a combo chart (multiple chart types in one plot area) to a slide.
Returns the updated slide.
## Parameters
* `chart_data` - `%ChartData{}` with shared categories and series
* `plots` - list of `{chart_type, opts}` tuples where opts are:
* `:series` - list of zero-based series indices to include in this plot
* `:secondary_axis` - `true` to plot on secondary value axis (default `false`)
## Options (required)
* `:x`, `:y`, `:width`, `:height` - position and size
## Options (optional)
* `:title` - chart title (string or keyword list)
* `:legend` - legend position or keyword list
* `:data_labels` - data label configuration
* `:category_axis` - category axis options
* `:value_axis` - primary value axis options
* `:secondary_value_axis` - secondary value axis options (same keys as `:value_axis`)
"""
@spec add_combo_chart(
Podium.Slide.t(),
Podium.Chart.ChartData.t(),
[{chart_type(), keyword()}],
keyword()
) :: Podium.Slide.t()
def add_combo_chart(slide, chart_data, plots, opts) do
Slide.add_combo_chart(slide, chart_data, plots, opts)
end
@doc """
Adds an image to a slide. Returns the updated slide.
Image format is auto-detected from magic bytes. Supported formats:
PNG, JPEG, BMP, GIF, TIFF, EMF, WMF.
## Options (required)
* `:x` - horizontal position
* `:y` - vertical position
## Options (optional)
* `:width` - image width (auto-calculated from native size if only `:height` given or neither given)
* `:height` - image height (auto-calculated from native size if only `:width` given or neither given)
* `:crop` - keyword list with `:left`, `:top`, `:right`, `:bottom` (values in 1/1000ths of a percent)
* `:rotation` - rotation in degrees
* `:shape` - clip shape preset: `:ellipse`, `:diamond`, `:round_rect`, `:star5`,
`:star6`, `:star8`, `:heart`, `:triangle`, `:hexagon`, `:octagon`, or a preset
geometry string (default `"rect"`)
"""
@spec add_image(Podium.Slide.t(), binary(), keyword()) :: Podium.Slide.t()
def add_image(slide, binary, opts) do
Slide.add_image(slide, binary, opts)
end
@doc """
Adds a text box with a picture (blip) fill to a slide. Returns the updated slide.
## Options (required)
* `:x`, `:y`, `:width`, `:height` - position and size
## Options (optional)
* `:fill_mode` - `:stretch` (default) or `:tile`
* All other text box options (`:rotation`, `:margin_*`, `:anchor`, etc.)
"""
@spec add_picture_fill_text_box(
Podium.Slide.t(),
rich_text(),
binary(),
keyword()
) :: Podium.Slide.t()
def add_picture_fill_text_box(slide, text, image_binary, opts) do
Slide.add_picture_fill_text_box(slide, text, image_binary, opts)
end
@doc """
Adds a freeform shape (built with `Podium.Freeform`) to a slide.
## Options
* `:origin_x`, `:origin_y` - offset for the shape's bounding box origin
* `:fill` - fill color or fill tuple
* `:line` - line color or line opts
* `:rotation` - rotation in degrees
"""
@spec add_freeform(Podium.Slide.t(), Podium.Freeform.t(), keyword()) :: Podium.Slide.t()
def add_freeform(slide, %Podium.Freeform{} = fb, opts \\ []) do
Slide.add_freeform(slide, fb, opts)
end
@doc """
Adds a video (movie) to a slide. Returns the updated slide.
All position/size options are required (no auto-scaling for video).
## Options (required)
* `:x`, `:y`, `:width`, `:height` - position and size
## Options (optional)
* `:mime_type` - MIME type string (default `"video/unknown"`)
* `:poster_frame` - poster frame image binary (default: 1x1 transparent PNG)
"""
@spec add_movie(Podium.Slide.t(), binary(), keyword()) :: Podium.Slide.t()
def add_movie(slide, binary, opts) do
Slide.add_video(slide, binary, opts)
end
@doc """
Adds a table to a slide.
`rows` is a list of lists where each inner list is a row of cell values.
Cell values can be:
* Plain string - `"text"`
* Rich text list - `[{"bold", bold: true}]`
* Cell tuple with options - `{"text", col_span: 2, fill: "FF0000"}`
* `:merge` atom for cells covered by a span
## Options (required)
* `:x`, `:y`, `:width`, `:height` - position and size
## Options (optional)
* `:col_widths` - list of column widths (auto-calculated if omitted)
* `:row_heights` - list of row heights (auto-calculated if omitted)
* `:table_style` - keyword list of style booleans:
* `:first_row` - highlight first row (default `true`)
* `:last_row` - highlight last row (default `false`)
* `:first_col` - highlight first column (default `false`)
* `:last_col` - highlight last column (default `false`)
* `:band_row` - alternating row bands (default `true`)
* `:band_col` - alternating column bands (default `false`)
## Cell options
* `:col_span` - number of columns to span
* `:row_span` - number of rows to span
* `:fill` - cell fill color or fill tuple
* `:borders` - `[left: color_or_opts, right: ..., top: ..., bottom: ...]`
* `:padding` - `[left: dim, right: dim, top: dim, bottom: dim]`
* `:anchor` - vertical text alignment: `:top`, `:middle`, `:bottom`
"""
@spec add_table(Podium.Slide.t(), [[term()]], keyword()) :: Podium.Slide.t()
def add_table(slide, rows, opts) do
Slide.add_table(slide, rows, opts)
end
@doc """
Sets a placeholder's text content on a slide.
The slide must have been created with a layout that has the given placeholder.
## Available placeholders by layout
| 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`, `:caption` (use `set_picture_placeholder/3` for `:picture`) |
| `:title_vertical_text` | `:title`, `:body` |
| `:vertical_title_text` | `:title`, `:body` |
"""
@spec set_placeholder(Podium.Slide.t(), atom(), rich_text()) :: Podium.Slide.t()
def set_placeholder(slide, name, text) do
layout = Slide.layout_atom(slide.layout_index)
ph = Podium.Placeholder.new(layout, name, text)
%{slide | placeholders: slide.placeholders ++ [ph]}
end
@doc """
Sets a picture placeholder on a slide. Returns the updated slide.
Only works on layouts with picture placeholders (e.g. `:picture_caption`).
"""
@spec set_picture_placeholder(Podium.Slide.t(), atom(), binary()) :: Podium.Slide.t()
def set_picture_placeholder(slide, name, binary) do
Slide.set_picture_placeholder(slide, name, binary)
end
@doc """
Places a chart into a content placeholder. Returns the updated slide.
The placeholder must be a content placeholder (type: nil) — e.g. `:content` on
`:title_content`, or `:left_content`/`:right_content` on `:two_content`.
Position and size are inherited from the template layout.
"""
@spec set_chart_placeholder(
Podium.Presentation.t(),
Podium.Slide.t(),
atom(),
chart_type(),
Podium.Chart.ChartData.t(),
keyword()
) :: Podium.Slide.t()
def set_chart_placeholder(prs, slide, name, chart_type, chart_data, opts \\ []) do
Presentation.set_chart_placeholder(prs, slide, name, chart_type, chart_data, opts)
end
@doc """
Places a table into a content placeholder. Returns the updated slide.
The placeholder must be a content placeholder (type: nil).
Position and size are inherited from the template layout.
"""
@spec set_table_placeholder(
Podium.Presentation.t(),
Podium.Slide.t(),
atom(),
[[term()]],
keyword()
) :: Podium.Slide.t()
def set_table_placeholder(prs, slide, name, rows, opts \\ []) do
Presentation.set_table_placeholder(prs, slide, name, rows, opts)
end
@doc """
Sets presentation-level footer, date, and slide number.
## Options
* `:footer` - footer text string
* `:date` - date text string
* `:slide_number` - boolean, whether to show slide numbers
"""
@spec set_footer(Podium.Presentation.t(), keyword()) :: Podium.Presentation.t()
def set_footer(prs, opts) do
Presentation.set_footer(prs, opts)
end
@doc """
Sets speaker notes on a slide.
"""
@spec set_notes(Podium.Slide.t(), String.t()) :: Podium.Slide.t()
def set_notes(slide, text) do
Presentation.set_notes(slide, text)
end
@doc """
Sets core document properties (Dublin Core metadata).
## Options
* `:title` - document title
* `:author` - document author
* `:subject` - document subject
* `:keywords` - keywords
* `:category` - category
* `:comments` - comments/description
* `:last_modified_by` - last modified by
* `:created` - creation timestamp (`DateTime`)
* `:modified` - modification timestamp (`DateTime`)
* `:last_printed` - last printed timestamp (`DateTime`)
* `:revision` - revision number (integer)
* `:content_status` - content status string
* `:language` - language code string
* `:version` - version string
"""
@spec set_core_properties(Podium.Presentation.t(), keyword()) :: Podium.Presentation.t()
def set_core_properties(prs, opts) do
Presentation.set_core_properties(prs, opts)
end
@doc """
Replaces a slide in the presentation with an updated version (matched by ref).
"""
@spec put_slide(Podium.Presentation.t(), Podium.Slide.t()) :: Podium.Presentation.t()
def put_slide(prs, slide) do
Presentation.put_slide(prs, slide)
end
@doc """
Saves the presentation to a file.
"""
@spec save(Podium.Presentation.t(), String.t()) :: :ok | {:error, term()}
def save(prs, path) do
Presentation.save(prs, path)
end
@doc """
Saves the presentation to an in-memory binary.
"""
@spec save_to_memory(Podium.Presentation.t()) :: {:ok, binary()} | {:error, term()}
def save_to_memory(prs) do
Presentation.save_to_memory(prs)
end
end