Skip to content

Generate PDF invoices in Rust with Typst templates

Oicana is written in Rust, and the oicana crate is its most direct integration. Compile Typst templates to typeset PDFs inside your service, with no subprocess and no browser.

Rust has solid low-level PDF crates, but hand-coding every table and page break gets old fast. And bundling a headless browser defeats the point of a lean Rust service.

Oicana is written in Rust, and the oicana crate is the most direct way to use it. Add it with cargo add oicana, load a packed template once, and compile documents from JSON:

use std::fs::File;
use oicana::Template;
use oicana::export::pdf::export_pdf;
use oicana::input::{CompilationConfig, JsonInput, TemplateInputs};
let mut template = Template::init(File::open("invoice-0.1.0.zip")?)?;
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::production());
inputs.with_input(JsonInput::new(
"invoice",
serde_json::json!({
"number": "2026-001",
"customer": "Acme GmbH",
"total": "€1,190.00"
})
.to_string(),
));
let document = template.compile(inputs)?;
let pdf = export_pdf(
&document.document,
&template,
template.manifest().pdf_standards(),
template.manifest().pdf_tagged(),
None,
)?;

A warmed up template produces a typeset PDF in single digit milliseconds, directly in your service.

The snippet loads invoice-0.1.0.zip, a packed Oicana template. Templates are plain Typst projects: a typst.toml manifest declares a JSON input named invoice, and main.typ uses it:

typst.toml
[package]
name = "invoice"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
[[tool.oicana.inputs]]
type = "json"
key = "invoice"
development = "invoice.json"

The development value points at a JSON file in the template that stands in for real data while you design the template. It is also what the warm-up compilation during registration uses by default.

invoice.json
{
"number": "2026-42",
"customer": "Acme GmbH",
"total": "€42.00"
}
main.typ
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Invoice #input.invoice.number
Billed to: #input.invoice.customer
*Total: #input.invoice.total*

Running oicana pack in the template directory produces the zip. Create a Template walks through the manifest and input definitions, and the open source example templates include a complete invoice.

Load the template once at startup and share it across handlers, for example via Arc in your Axum state; after that there is no file I/O on the hot path. For managing multiple templates, the example project demonstrates thread-safe template caching with a DashMap.

The Axum example project shows a complete service, including blob inputs, error handling, and OpenAPI documentation.

No headless browser

Rust all the way down. The compiler runs in your own process: no subprocess, no browser, no document service to operate.

Your data stays with you

Documents are generated inside your own service. No customer data is sent to a third party, which keeps GDPR and HIPAA compliance in your hands.

Flat price, unlimited documents

One flat license per application, no metering. A hundred invoices a month cost the same as a hundred thousand. Non-commercial use is free.