{"id":50863625,"url":"https://github.com/dannote/folio","last_synced_at":"2026-06-14T23:05:36.779Z","repository":{"id":351979854,"uuid":"1211411642","full_name":"dannote/folio","owner":"dannote","description":"Print-quality PDF/SVG/PNG from Markdown + Elixir, powered by Typst","archived":false,"fork":false,"pushed_at":"2026-06-01T09:38:26.000Z","size":671,"stargazers_count":78,"open_issues_count":1,"forks_count":8,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-06-01T11:16:56.482Z","etag":null,"topics":["document-generation","elixir","layout-engine","markdown","nif","pdf","pdf-generation","report-generation","rustler","svg","typst"],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dannote.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-15T11:16:33.000Z","updated_at":"2026-06-01T09:38:29.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dannote/folio","commit_stats":null,"previous_names":["dannote/folio"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/dannote/folio","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Ffolio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Ffolio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Ffolio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Ffolio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dannote","download_url":"https://codeload.github.com/dannote/folio/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Ffolio/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34340834,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["document-generation","elixir","layout-engine","markdown","nif","pdf","pdf-generation","report-generation","rustler","svg","typst"],"created_at":"2026-06-14T23:05:36.020Z","updated_at":"2026-06-14T23:05:36.771Z","avatar_url":"https://github.com/dannote.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Folio\n\nPrint-quality PDF/SVG/PNG from Markdown + Elixir, powered by [Typst](https://typst.app)'s layout engine via Rustler NIF.\n\n[![Hex.pm](https://img.shields.io/hexpm/v/folio.svg)](https://hex.pm/packages/folio)\n[![Docs](https://img.shields.io/badge/docs-hex.pm-blue)](https://hexdocs.pm/folio)\n\n## Why Folio\n\n### Data-Driven Documents at Runtime\n\nTypst 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:\n\n```elixir\ndef invoice_pdf(order) do\n  ~MD\"\"\"\n  # Invoice #{order.number}\n\n  #{table([gutter: \"4pt\"], do: [\n    table_header([table_cell(\"Item\"), table_cell(\"Qty\"), table_cell(\"Price\")]),\n    for item \u003c- order.line_items do\n      table_row([table_cell(item.name), table_cell(\"#{item.quantity}\"), table_cell(Money.to_string(item.price))])\n    end\n  ])}\n  \"\"\"p\nend\n```\n\n### Composable Document Fragments\n\nDSL 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:\n\n```elixir\ndefmodule Reports.Components do\n  use Folio\n\n  def kpi_card(label, value, trend) do\n    block([above: \"12pt\", below: \"12pt\"], do: [\n      strong(label),\n      parbreak(),\n      text(\"#{value} (#{trend})\"),\n    ])\n  end\nend\n```\n\n### No Typst Language, No Typst Parser, No Typst Evaluator\n\nFolio 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:\n\n- **No template injection** — there's no string template to inject into\n- **No syntax errors** — content is structurally valid by construction\n- **Smaller attack surface** — the Typst evaluator (file I/O, package imports, plugin loading) is never invoked\n- **Faster for programmatic documents** — skipping parse + eval stages\n\n### Elixir-Native Concurrency for Batch Generation\n\nWith Typst CLI, generating 10,000 invoices means 10,000 process spawns. With Folio on dirty schedulers:\n\n```elixir\norders\n|\u003e Task.async_stream(\n  fn order -\u003e Folio.to_pdf(build_invoice(order)) end,\n  max_concurrency: System.schedulers_online()\n)\n|\u003e Stream.each(fn {:ok, pdf} -\u003e upload(pdf) end)\n|\u003e Stream.run()\n```\n\nFonts and layout data are loaded once and shared across compilations.\n\n## Quick start\n\nAdd Folio to your dependencies:\n\n```elixir\ndef deps do\n  [{:folio, \"~\u003e 0.3\"}]\nend\n```\n\nFolio ships with precompiled NIFs for macOS (Intel \u0026 Apple Silicon) and Linux (x86_64 \u0026 aarch64, glibc). No Rust toolchain is required.\n\nTo build from source instead (e.g. for a custom target or during development):\n\n```sh\nFOLIO_BUILD=1 mix compile\n```\n\nRender Markdown to PDF with math, tables, and Elixir interpolation:\n\n```elixir\nuse Folio\n\n{:ok, pdf} = Folio.to_pdf(\"# Hello\\n\\n**Bold** and $x^2$ math.\")\n```\n\nOr use the `~MD` sigil for multi-line documents — the `p` modifier returns `{:ok, pdf_binary}` directly:\n\n```elixir\n{:ok, pdf} = ~MD\"\"\"\n# Report\n\nSome **bold** content with inline $E = m c^2$ math.\n\n| Metric | Value |\n|--------|-------|\n| A      | 1     |\n| B      | 2     |\n\"\"\"p\n```\n\nFor full control, compose content with the DSL — every function returns a plain struct:\n\n```elixir\n{:ok, pdf} = Folio.to_pdf([\n  heading(1, \"Hello\"),\n  text(\"Normal \"),\n  strong(\"bold\"),\n  text(\" and \"),\n  emph(\"italic\"),\n])\n```\n\nStyle text inline, build shaped containers, and use full Typst track sizing in tables:\n\n```elixir\n{:ok, pdf} = Folio.to_pdf([\n  rect(width: \"100%\", fill: \"#6c63ff\", radius: \"8pt\", inset: \"20pt\",\n    body: [text(\"INVOICE\", size: \"24pt\", weight: \"bold\", fill: \"white\")]\n  ),\n  table([columns: [\"1fr\", \"1fr\", \"auto\"], gutter: \"8pt\", inset: \"10pt\", fill: \"#f8f8ff\"],\n    do: [\n      table_header([table_cell(\"Item\"), table_cell(\"Qty\"), table_cell(\"Price\")]),\n      for item \u003c- items do\n        table_row([table_cell(item.name), table_cell(\"#{item.qty}\"), table_cell(item.price)])\n      end\n    ]\n  ),\n])\n```\n\nExport to PDF, SVG, or PNG with configurable resolution:\n\n```elixir\n{:ok, pdf} = Folio.to_pdf(\"# Hello\")          # PDF binary\n{:ok, svgs} = Folio.to_svg(\"# Hello\")         # [String.t()] per page\n{:ok, pngs} = Folio.to_png(\"# Hello\", dpi: 3) # [binary()] per page\n```\n\nFull API documentation at [hexdocs.pm/folio](https://hexdocs.pm/folio).\n\n## Comparison with other Elixir PDF libraries\n\n| | **Folio** | [**ChromicPDF**](https://hex.pm/packages/chromic_pdf) | [**pdf_generator**](https://hex.pm/packages/pdf_generator) | [**Imprintor**](https://hex.pm/packages/imprintor) | [**pdf**](https://hex.pm/packages/pdf) | [**PrawnEx**](https://hex.pm/packages/prawn_ex) |\n|---|---|---|---|---|---|---|\n| **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 |\n| **Input format** | Markdown + Elixir DSL | HTML | HTML | Typst source strings | Programmatic API calls | Programmatic API calls |\n| **Layout engine** | Typst (print-quality typesetting) | Chrome (CSS box model) | Chrome / wkhtmltopdf (CSS) | Typst (full Typst language) | None (manual positioning) | None (manual positioning) |\n| **External deps** | None (precompiled NIFs) | Chromium + Ghostscript | Chromium/wkhtmltopdf + Node.js | Rust toolchain (compile-time only) | None | None |\n| **Runtime overhead** | In-process NIF | External Chrome process | External process per PDF | In-process NIF | In-process | In-process |\n| **Text layout** | Automatic (hyphenation, justification, ligatures, kerning) | Browser CSS | Browser CSS | Automatic (full Typst) | Manual `text_at(x, y)` | Manual `text_at(x, y)` |\n| **Math** | `$E = mc^2$` via Typst math parser | No | No | `$E = mc^2$` via Typst math parser | No | No |\n| **Tables** | Structured DSL with header/rowspan/colspan | HTML tables | HTML tables | Typst tables | Manual grid drawing | Basic row grid |\n| **Bibliography** | Built-in (`.bib`, `.yaml`) | No | No | Via Typst packages | No | No |\n| **Multi-page flow** | Automatic | Browser pagination | Browser pagination | Automatic | Manual page management | Manual page management |\n| **Output formats** | PDF, SVG, PNG | PDF, PDF/A | PDF | PDF | PDF | PDF |\n| **Template injection risk** | None (no string templates) | HTML injection possible | HTML injection possible | Typst code injection possible | N/A | N/A |\n| **Batch performance** | Fonts shared, in-process NIF | Chrome session pool | Process spawn per PDF | In-process NIF | In-process | In-process |\n\n### When to use what\n\n- **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.\n- **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.\n- **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.\n- **pdf / PrawnEx** — Simple PDFs with manual positioning (labels, receipts, badges) where you control every coordinate and don't need automatic text flow.\n\n## License\n\nMIT — see [LICENSE.md](LICENSE.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdannote%2Ffolio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdannote%2Ffolio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdannote%2Ffolio/lists"}