Typster is an Elixir wrapper for the Typst document preparation system, providing powerful and ergonomic functions for rendering Typst templates to PDF, SVG, and PNG formats.
- Multiple Output Formats: Render to PDF, SVG, or PNG
- Variable Binding: Inject Elixir data into templates with deep nesting support
- Package Support: Use Typst packages from the official registry
- PDF Metadata: Embed title, author, keywords, and more
- Type-Safe: Full typespecs for all public functions
- Fast: Powered by Rust via NIFs
- Ergonomic API: Simple, consistent interface with bang (
!) variants - Well-Tested: 58 comprehensive tests including property-based testing
Add typster to your list of dependencies in mix.exs:
def deps do
[
{:typster, "~> 0.4.0"}
]
endThen run:
mix deps.getNote: Precompiled binaries are available for macOS (ARM64/x86_64) and Linux (ARM64/x86_64). If a precompiled binary is not available for your platform, Typster will automatically compile from source, which requires Rust to be installed (see rustup.rs). You can force compilation from source by setting TYPSTER_BUILD=1.
# Create a simple template
template = """
#set page(width: 8.5in, height: 11in)
#set text(size: 11pt)
= Hello from Typster!
This is a simple document rendered with Typster.
"""
# Render to PDF
{:ok, pdf} = Typster.render_pdf(template)
# Save to file
File.write!("output.pdf", pdf)
# Or use the convenience function
Typster.render_to_file(template, "output.pdf")template = """
= Invoice for #customer_name
*Date:* #invoice_date
*Amount:* \\$#amount
Thank you for your business!
"""
variables = %{
customer_name: "Acme Corp",
invoice_date: "2025-10-03",
amount: 1234.56
}
{:ok, pdf} = Typster.render_pdf(template, variables: variables)template = """
= #user.name's Profile
*Email:* #user.email
*Location:* #user.address.city, #user.address.state
"""
variables = %{
user: %{
name: "Alice Johnson",
email: "alice@example.com",
address: %{
city: "Portland",
state: "OR"
}
}
}
{:ok, pdf} = Typster.render_pdf(template, variables: variables)template = """
= Shopping List
#for item in items [
- #item.name: \\$#item.price
]
*Total Items:* #items.len()
"""
variables = %{
items: [
%{name: "Apples", price: 3.99},
%{name: "Bread", price: 2.49},
%{name: "Milk", price: 4.29}
]
}
{:ok, pdf} = Typster.render_pdf(template, variables: variables)metadata = %{
title: "Annual Report 2025",
author: "Analytics Team",
description: "Comprehensive performance analysis",
keywords: "report, analytics, 2025",
date: "auto" # Use current date
}
{:ok, pdf} = Typster.render_pdf(template, variables: variables, metadata: metadata)# Render to SVG (returns list of SVG strings, one per page)
{:ok, svg_pages} = Typster.render_svg(template)
# Render to PNG with custom resolution
{:ok, png_pages} = Typster.render_png(template, pixel_per_pt: 4.0)
# Save first page
File.write!("page1.png", List.first(png_pages))# Use packages from the Typst registry
template = """
#import "@preview/tiaoma:0.3.0": qrcode
= Contact Information
#qrcode("https://example.com", width: 3cm)
"""
# Packages are automatically downloaded and cached
{:ok, pdf} = Typster.render_pdf(template)
# Use multiple packages together
template = """
#import "@preview/tiaoma:0.3.0": qrcode
#import "@preview/cetz:0.3.2": canvas, draw
= Document with Packages
#qrcode("https://example.com", width: 2cm)
#canvas({
import draw: *
rect((0, 0), (3, 2), fill: blue.lighten(80%))
})
"""
{:ok, pdf} = Typster.render_pdf(template)Package Features:
- Automatic download from the Typst package registry
- Local caching for fast subsequent renders
- Concurrent download protection with mutex locks
- Support for all packages in the @preview namespace
# Use bang (!) versions for cleaner code
# (raises Typster.CompileError on failure)
try do
pdf = Typster.render_pdf!(template, variables: variables)
File.write!("output.pdf", pdf)
rescue
e in Typster.CompileError ->
IO.puts("Compilation failed: #{e.message}")
endTypster.render_pdf(source, opts \\ [])- Render to PDFTypster.render_svg(source, opts \\ [])- Render to SVG (multi-page)Typster.render_png(source, opts \\ [])- Render to PNG (multi-page)Typster.render_to_file(source, path, opts \\ [])- Save to file
Typster.render_pdf!(source, opts \\ [])- Raises on errorTypster.render_svg!(source, opts \\ [])- Raises on errorTypster.render_png!(source, opts \\ [])- Raises on errorTypster.render_to_file!(source, path, opts \\ [])- Raises on error
All render functions accept the following options:
:metadata- Map of PDF metadata (%{title:, author:, description:, keywords:, date:}):package_paths- List of local package directories (for custom packages):pixel_per_pt- PNG resolution multiplier (default:2.0, higher = better quality):variables- Map of variables to be used in the template
See the examples/ directory for complete working examples:
examples/invoice.exs- Generate invoices with calculationsexamples/report.exs- Multi-page reports with chartsexamples/qrcode.exs- Generate QR codes using packagesexamples/reusable_templates.exs- Reusable template components without filesystem dependencies
Full documentation is available at HexDocs or can be generated locally:
mix docs
open doc/index.htmlRun the test suite:
mix testThe test suite includes:
- 42 unit and integration tests
- 9 property-based tests using StreamData
- 9 concurrent rendering tests (thread safety)
- 7 package download and import tests
- Realistic fixtures (invoice, report templates)
- Comprehensive error handling tests
Run concurrent tests separately:
mix test test/concurrent_test.exsTypster uses native Rust code via NIFs for high performance:
- Typical invoice rendering: < 50ms
- Multi-page reports: < 200ms
- Package downloads are cached locally
Typster is fully thread-safe and designed for concurrent use:
# Render multiple documents in parallel
tasks = for i <- 1..10 do
Task.async(fn ->
template = get_template(i)
Typster.render_pdf(template, variables: %{id: i})
end)
end
results = Task.await_many(tasks)Tested Performance:
- 50 concurrent renders: All successful
- 100 concurrent renders: Completed in ~22ms total
- Mixed format rendering (PDF/SVG/PNG): No conflicts
Thread Safety:
- Multiple processes can render simultaneously
- Concurrent package downloads are safely handled with mutex locks
- No resource conflicts or race conditions
- Suitable for Phoenix applications with multiple concurrent users
Contributions are welcome! Please feel free to submit a Pull Request.