Folio

Print-quality PDF/SVG/PNG from Markdown + Elixir, powered by Typst's layout engine via Rustler NIF.

Hex.pmDocs

Why Folio

Data-Driven Documents at Runtime

Typst reads static files. Folio builds content trees from live Elixir data — Ecto queries, API responses, GenServer state. A Phoenix app generates PDFs from the same data it renders in HTML, with zero intermediate files:

defmodule Invoices do
use Folio
def invoice_pdf(order) do
rows =
Enum.map(order.line_items, fn item ->
table_row([
table_cell(item.name),
table_cell(to_string(item.quantity)),
table_cell(Money.to_string(item.price))
])
end)
Folio.to_pdf([
heading(1, "Invoice #{order.number}"),
table([gutter: "4pt"],
do: [table_header(["Item", "Qty", "Price"]) | rows]
)
])
end
end

Composable Document Fragments

DSL functions return plain structs — document pieces are first-class Elixir values. Build reusable components as regular functions, pattern-match on them, store them, pipe them:

defmodule Reports.Components do
use Folio
def kpi_card(label, value, trend) do
block([above: "12pt", below: "12pt"], do: [
strong(label),
parbreak(),
text("#{value} (#{trend})"),
])
end
end

Structured Content Without Typst Templates

Folio's normal path constructs Typst content trees directly in Rust and feeds them to the layout engine. Markdown is parsed by Comrak, while DSL values remain structured data:

Elixir-Native Concurrency for Batch Generation

With Typst CLI, generating 10,000 invoices means 10,000 process spawns. With Folio on dirty schedulers:

results =
Task.async_stream(
orders,
fn order -> Folio.to_pdf(build_invoice(order)) end,
max_concurrency: System.schedulers_online()
)
|> Enum.to_list()

Fonts and layout data are loaded once and shared across compilations.

Quick start

Add Folio to your dependencies:

def deps do
[{:folio, "~> 0.4"}]
end

Folio ships with precompiled NIFs for macOS (Intel & Apple Silicon) and Linux (x86_64 & aarch64, glibc). No Rust toolchain is required.

To build from source instead (e.g. for a custom target or during development):

FOLIO_BUILD=1 mix compile

Render Markdown to PDF with math and tables:

use Folio
{:ok, pdf} = Folio.to_pdf("# Hello\n\n**Bold** and $x^2$ math.")

Or use the uppercase ~MD sigil for static multi-line Markdown — the p modifier returns {:ok, pdf_binary} directly. As with other uppercase Elixir sigils, ~MD does not interpolate:

{:ok, pdf} = ~MD"""
# Report
Some **bold** content with inline $E = m c^2$ math.
| Metric | Value |
|--------|-------|
| A | 1 |
| B | 2 |
"""p

For dynamic values and full control, compose content with the DSL — every function returns a plain struct:

{:ok, pdf} = Folio.to_pdf([
heading(1, "Hello"),
text("Normal "),
strong("bold"),
text(" and "),
emph("italic"),
])

Style text inline, build shaped containers, and use full Typst track sizing in tables:

{:ok, pdf} = Folio.to_pdf([
rect(width: "100%", fill: "#6c63ff", radius: "8pt", inset: "20pt",
body: [text("INVOICE", size: "24pt", weight: "bold", fill: "white")]
),
table([columns: ["1fr", "1fr", "auto"], gutter: "8pt", inset: "10pt", fill: "#f8f8ff"],
do: [
table_header([table_cell("Item"), table_cell("Qty"), table_cell("Price")]),
for item <- items do
table_row([table_cell(item.name), table_cell("#{item.qty}"), table_cell(item.price)])
end
]
),
])

Export to PDF, SVG, or PNG with configurable resolution:

{:ok, pdf} = Folio.to_pdf("# Hello") # PDF binary
{:ok, svgs} = Folio.to_svg("# Hello") # [String.t()] per page
{:ok, pngs} = Folio.to_png("# Hello", dpi: 3) # [binary()] per page

Full API documentation at hexdocs.pm/folio.

Comparison with other Elixir PDF libraries

FolioChromicPDFpdf_generatorImprintorpdfPrawnEx
ApproachTypst layout engine via Rustler NIFHeadless Chrome → PDFwkhtmltopdf or Chrome via shellTypst templates via Rustler NIFRaw PDF primitives in pure ElixirRaw PDF primitives in pure Elixir
Input formatMarkdown + Elixir DSLHTMLHTMLTypst source stringsProgrammatic API callsProgrammatic API calls
Layout engineTypst (print-quality typesetting)Chrome (CSS box model)Chrome / wkhtmltopdf (CSS)Typst (full Typst language)None (manual positioning)None (manual positioning)
External depsNone (precompiled NIFs)Chromium + GhostscriptChromium/wkhtmltopdf + Node.jsRust toolchain (compile-time only)NoneNone
Runtime overheadIn-process NIFExternal Chrome processExternal process per PDFIn-process NIFIn-processIn-process
Text layoutAutomatic (hyphenation, justification, ligatures, kerning)Browser CSSBrowser CSSAutomatic (full Typst)Manual text_at(x, y)Manual text_at(x, y)
Math$E = mc^2$ via Typst math parserNoNo$E = mc^2$ via Typst math parserNoNo
TablesStructured DSL with header/rowspan/colspanHTML tablesHTML tablesTypst tablesManual grid drawingBasic row grid
BibliographyBuilt-in (.bib, .yaml)NoNoVia Typst packagesNoNo
Multi-page flowAutomaticBrowser paginationBrowser paginationAutomaticManual page managementManual page management
Output formatsPDF, SVG, PNGPDF, PDF/APDFPDFPDFPDF
Template injection riskNone in the structured API; raw_typst/1 is an explicit trusted-input escape hatchHTML injection possibleHTML injection possibleTypst code injection possibleN/AN/A
Batch performanceFonts shared, in-process NIFChrome session poolProcess spawn per PDFIn-process NIFIn-processIn-process

When to use what

License

MIT — see LICENSE.md