GitPedia

Folio

Print-quality PDF/SVG/PNG from Markdown + Elixir, powered by Typst

From dannote·Updated June 22, 2026·View on GitHub·

Print-quality PDF/SVG/PNG from Markdown + Elixir, powered by [Typst](https://typst.app)'s layout engine via Rustler NIF. The project is written primarily in Elixir, distributed under the MIT License license, first published in 2026. Key topics include: document-generation, elixir, layout-engine, markdown, nif.

Latest release: v0.3.1
May 12, 2026View Changelog →

Folio

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

Hex.pm
Docs

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:

elixir
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 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:

elixir
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

No Typst Language, No Typst Parser, No Typst Evaluator

Folio 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

Elixir-Native Concurrency for Batch Generation

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

elixir
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.

Quick start

Add Folio to your dependencies:

elixir
def deps do [{:folio, "~> 0.3"}] 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):

sh
FOLIO_BUILD=1 mix compile

Render Markdown to PDF with math, tables, and Elixir interpolation:

elixir
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:

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

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

elixir
{: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:

elixir
{: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:

elixir
{: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 (no string templates)HTML 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

  • 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.

License

MIT — see LICENSE.md

Contributors

Showing top 2 contributors by commit count.

View all contributors on GitHub →

This article is auto-generated from dannote/folio via the GitHub API.Last fetched: 6/27/2026