Print-quality PDF/SVG/PNG from Markdown + Elixir, powered by Typst's layout engine via Rustler NIF.
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:
def invoice_pdf(order) do
~MD"""
# Invoice #{order.number}
#{table([gutter: "4pt"], do: [
table_header([table_cell("Item"), table_cell("Qty"), table_cell("Price")]),
for item <- order.line_items do
table_row([table_cell(item.name), table_cell("#{item.quantity}"), table_cell(Money.to_string(item.price))])
end
])}
"""p
endDSL 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
endFolio constructs Typst content trees directly in Rust and feeds them straight to the layout engine. It bypasses Typst's parser, AST, and evaluation VM entirely:
- No template injection — there's no string template to inject into
- No syntax errors — content is structurally valid by construction
- Smaller attack surface — the Typst evaluator (file I/O, package imports, plugin loading) is never invoked
- Faster for programmatic documents — skipping parse + eval stages
With Typst CLI, generating 10,000 invoices means 10,000 process spawns. With Folio on dirty schedulers:
orders
|> Task.async_stream(
fn order -> Folio.to_pdf(build_invoice(order)) end,
max_concurrency: System.schedulers_online()
)
|> Stream.each(fn {:ok, pdf} -> upload(pdf) end)
|> Stream.run()Fonts and layout data are loaded once and shared across compilations.
Add Folio to your dependencies:
def deps do
[{:folio, "~> 0.1"}]
endRender Markdown to PDF with math, tables, and Elixir interpolation:
use Folio
{:ok, pdf} = Folio.to_pdf("# Hello\n\n**Bold** and $x^2$ math.")Or use the ~MD sigil for multi-line documents — the p modifier returns {:ok, pdf_binary} directly:
{:ok, pdf} = ~MD"""
# Report
Some **bold** content with inline $E = m c^2$ math.
| Metric | Value |
|--------|-------|
| A | 1 |
| B | 2 |
"""pFor 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"),
])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 pageFull API documentation at hexdocs.pm/folio.
| Folio | ChromicPDF | pdf_generator | Imprintor | PrawnEx | ||
|---|---|---|---|---|---|---|
| Approach | Typst layout engine via Rustler NIF | Headless Chrome → PDF | wkhtmltopdf or Chrome via shell | Typst templates via Rustler NIF | Raw PDF primitives in pure Elixir | Raw PDF primitives in pure Elixir |
| Input format | Markdown + Elixir DSL | HTML | HTML | Typst source strings | Programmatic API calls | Programmatic API calls |
| Layout engine | Typst (print-quality typesetting) | Chrome (CSS box model) | Chrome / wkhtmltopdf (CSS) | Typst (full Typst language) | None (manual positioning) | None (manual positioning) |
| External deps | Rust toolchain (compile-time only) | Chromium + Ghostscript | Chromium/wkhtmltopdf + Node.js | Rust toolchain (compile-time only) | None | None |
| Runtime overhead | In-process NIF | External Chrome process | External process per PDF | In-process NIF | In-process | In-process |
| Text layout | Automatic (hyphenation, justification, ligatures, kerning) | Browser CSS | Browser CSS | Automatic (full Typst) | Manual text_at(x, y) |
Manual text_at(x, y) |
| Math | $E = mc^2$ via Typst math parser |
No | No | $E = mc^2$ via Typst math parser |
No | No |
| Tables | Structured DSL with header/rowspan/colspan | HTML tables | HTML tables | Typst tables | Manual grid drawing | Basic row grid |
| Bibliography | Built-in (.bib, .yaml) |
No | No | Via Typst packages | No | No |
| Multi-page flow | Automatic | Browser pagination | Browser pagination | Automatic | Manual page management | Manual page management |
| Output formats | PDF, SVG, PNG | PDF, PDF/A | ||||
| Template injection risk | None (no string templates) | HTML injection possible | HTML injection possible | Typst code injection possible | N/A | N/A |
| Batch performance | Fonts shared, in-process NIF | Chrome session pool | Process spawn per PDF | In-process NIF | In-process | In-process |
- Folio — Data-driven documents (invoices, reports, certificates) from Elixir data at runtime. You want print-quality typography, math, and tables without external processes or template strings.
- ChromicPDF — You already have HTML/CSS that looks right in a browser and want it as PDF. Best option for Pixel-perfect HTML-to-PDF with PDF/A compliance.
- Imprintor — You want Typst's full language (templates, packages, scripting) and are comfortable with Typst syntax. Note: passes raw Typst source strings to the evaluator, so template injection is possible with untrusted input.
- pdf / PrawnEx — Simple PDFs with manual positioning (labels, receipts, badges) where you control every coordinate and don't need automatic text flow.
MIT — see LICENSE.md