This is the abridged developer documentation for Oicana
# Installation and Setup
> Install the Oicana CLI and set up an editor for template development with Typst.
No license needed to follow this guide
For commercial use, the first 30 days are free for evaluation. Follow this guide, build a proof of concept, and test Oicana without a subscription. See [Section 4.2 of our Terms of Service](/terms-of-service/#42-evaluation-use) for the full terms.
Non-commercial use of Oicana like personal projects, hobby use, research, education, and use by charitable, educational, public research, and government institutions is free under the [PolyForm Noncommercial License](https://polyformproject.org/licenses/noncommercial/1.0.0).
To get started, you need the Oicana CLI and an editor for Typst. Both are only necessary on machines used for Oicana template development. An end-user machine, running software that uses an Oicana integration, does not require any additional installations.
## Oicana CLI
[Section titled “Oicana CLI”](#oicana-cli)
Install a [prebuilt binary from GitHub](https://github.com/oicana/oicana/releases/tag/oicana_cli-v0.9.0) or let a script do that for you.
Bash script:
Shell script to install Oicana CLI
```bash
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/oicana/oicana/releases/download/oicana_cli-v0.9.0/oicana_cli-installer.sh | sh
```
Powershell script:
Powershell script to install Oicana CLI
```powershell
powershell -ExecutionPolicy Bypass -c "irm https://github.com/oicana/oicana/releases/download/oicana_cli-v0.9.0/oicana_cli-installer.ps1 | iex"
```
After the installation, run `oicana --version` to make sure the installation succeeded.
The CLI can manage Oicana templates. The most relevant commands for this guide are `oicana pack` to package a template and `oicana compile` if you want to test template compilation. See the [CLI chapter](/docs/cli/) for more information.
## Editor for Typst
[Section titled “Editor for Typst”](#editor-for-typst)
You can edit Typst files in any text editor, but syntax highlighting and live previews make development significantly easier. Here are some suggestions:
* Several IDEs have Typst plugins with live previews and syntax highlighting.
* Visual Studio Code: [Tinymist](https://marketplace.visualstudio.com/items?itemName=myriad-dreamin.tinymist) offers a complete experience with live preview, syntax/error highlighting and more.
* JetBrains IDEs: [Kvasir](https://plugins.jetbrains.com/plugin/25061-kvasir)
* [Official Typst editor in the browser](https://typst.app/).
* This editor has a [significant free tier and a pro tier for $8 per month](https://typst.app/pricing/). It is maintained by the Typst company.
* Web based; No installation required.
* You can work on the same document with multiple people and sync your templates to GitHub or GitLab.
## For this Guide
[Section titled “For this Guide”](#for-this-guide)
In the following chapters, you will create a basic Oicana template that can be used with any integration. After creating the template, you’ll be able to choose which integration to explore based on your preferred tech stack.
# Create a Template
> Create your first Oicana template with static content and package it.
The simplest Oicana template consists of a `typst.toml` file and a Typst source file with some static content. The `oicana new` command scaffolds both for you in one step.
## Template with static content
[Section titled “Template with static content”](#template-with-static-content)
We will use the `new` command of the Oicana CLI to create a starter template. If you are using the online Typst editor, manually create the files and paste their content.
```bash
oicana new example
```
This creates an `example/` directory with two files. Open it in your IDE with Typst support.
main.typ
```typst
#set document(date: datetime.today())
= Hello
Edit `main.typ` to design your template.
```
Note
This Typst document uses [markup mode](https://typst.app/docs/reference/syntax/#markup), where `=` starts a heading, and plain text is just text. `#` switches to code mode, where you can call functions like `#set document(title: "Invoice")` or `#text(fill: blue)[...]` to style content.
Refer to [Typst’s online documentation](https://typst.app/docs/) for the full reference.
The preview of `main.typ` should show the heading and text.
Note
In an IDE, you might need to open the preview. For example, if you use VS Code with `Tinymist`, press `Ctrl` + `K` followed by `V` while the `main.typ` file is open.
The companion `example/typst.toml` is the template manifest:
typst.toml
```toml
[package]
name = "example"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
```
It gives our template the name `example`, the semantic version `0.1.0`, and defines the entrypoint as `main.typ`. The `tool.oicana` section only configures the Oicana manifest version for now.
To check that the manifest is well-formed, run `oicana validate` in the template directory. It will report any issues with `typst.toml` before you try to package or compile the template.
## Packaging
[Section titled “Packaging”](#packaging)
To compile the template through Oicana integrations, like shown in later chapters of this guide, it has to be packaged. Navigate into the template directory in a terminal and execute `oicana pack`. This will create a file called `example-0.1.0.zip`.
Complete code at the end of this chapter
main.typ
```typst
#set document(date: datetime.today())
= Hello
Edit `main.typ` to design your template.
```
typst.toml
```toml
[package]
name = "example"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
```
# Defining Inputs
> Learn how to define JSON and blob inputs for your Oicana template.
There are two types of inputs. A `json` input contains structured data while a `blob` input passes bytes and optionally metadata to the template. For example, in an invoice the items and customer data could be a `json` input and the company logo could be a `blob` input.
## The Oicana Typst package
[Section titled “The Oicana Typst package”](#the-oicana-typst-package)
Template inputs are configured in the manifest file `typst.toml`. The Oicana Typst package determines the current values of inputs.
Add the following to the top of your `main.typ` file to initialize the package:
Beginning of main.typ
```typst
#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())
```
The `setup` method reads the manifest file and resolves all inputs. It returns three values:
1. *input* - dictionary of resolved input values, keyed by input key
2. *oicana-image* - a utility method, that takes the key of a blob input and returns the input as an image
3. *oicana-config* - dictionary with compilation metadata
We only need the inputs for now. You can take a look at [the example templates](https://github.com/oicana/oicana-example-templates) to see the `oicana-image` helper in action.
Why the \`read-project-file\` wrapper?
The `setup` call needs access to the files in the template project. Due to how Typst handles paths, this currently requires passing a `read` closure from the template itself, as done with `read-project-file`.
## Defining inputs
[Section titled “Defining inputs”](#defining-inputs)
We will use a `json` input to pass a name into the template. Add the following to the end of the `typst.toml` file:
typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "info"
```
The value of this input is now available in the template as `input.info`, where `info` is the key of the input as defined in `typst.toml`.
While we develop the template, there is no Oicana integration setting a value for it. Inputs are required unless configured otherwise, so the template does not compile yet: `setup` stops with `No value for the required input 'info' was supplied`, in the editor preview as well as in the CLI. We fix that in the next section by defining a `default` or `development` value for the input.
### Default and Development values
[Section titled “Default and Development values”](#default-and-development-values)
Inputs can define two different fallback values, `default` and `development`. These fallback values differ in priority based on which mode the template is compiled in.
When compiling a template in development mode, input values have the priority
1. Explicit input value (for example through an integration)
2. `development` value
3. `default` value
If you compile in production mode, the `development` value is ignored:
1. Explicit input value (for example through an integration)
2. `default` value
When to use each mode
**Development mode** is used in two scenarios:
* When developing templates in a Typst editor, live previews will use the development mode. The same goes for other tooling without Oicana integration (like the official Typst CLI).
* In most integrations, template registration (typically at application startup) uses development mode when warming up the Typst cache for the template to speed up later compilations.
**Production mode** is the default for document compilation in integrations. It ensures the compilation fails explicitly rather than accidentally using test data for the document. If an input value is missing in production mode and the input does not have a `default` value, the compilation will fail unless you marked the input as not required and your template handles `none` values for it.
While developing an Oicana template in a Typst editor, it will be compiled in development mode. It makes sense to define `development` values for all required inputs of your template to have a functioning preview.
Let’s extend our input with a `development` value. First create an `info.json` file in the template directory:
info.json
```json
{
"name": "Chuck Norris"
}
```
Then extend the input definition and set the `development` value to be `info.json`:
typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "info"
development = "info.json"
```
In our template we can now use `input.info.name` and the preview will show “Chuck Norris”. Update your template to the following:
main.typ
```typst
#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())
= Hello from Typst, #input.info.name
Now we can pass names into the template from any Oicana integration.
```
## Compiling with the CLI
[Section titled “Compiling with the CLI”](#compiling-with-the-cli)
After changing the manifest, run `oicana validate` to confirm the new input definition is valid. If the JSON input has a schema, `validate` also checks that the `development` and `default` values match it.
Running `oicana compile --development` should create a PDF file in an `output` directory. To set the input value from the CLI, copy `info.json` to `tests/input.json` and change the name in the new file. Then run `oicana compile -j info=tests/input.json` and the new PDF file in `output` will contain whatever name you configured.
With the input defined in your template, you’re ready to choose an integration and learn how to pass dynamic values from your application code. In preparation for that, you should pack the template again using `oicana pack` to have the latest state of the template at hand.
Complete code at the end of this chapter
main.typ
```typst
#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())
= Hello from Typst, #input.info.name
Now we can pass names into the template from any Oicana integration.
```
typst.toml
```toml
[package]
name = "example"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
[[tool.oicana.inputs]]
type = "json"
key = "info"
development = "info.json"
```
info.json
```json
{
"name": "Chuck Norris"
}
```
# Browser / React
> Integrate Oicana into a React application using WebAssembly.
In this chapter, you’ll integrate Oicana into a React application using WebAssembly. Unlike the server-side integrations, Oicana runs entirely in the browser here - no server required for PDF generation.
Note
This chapter assumes that you have a working Node.js 20.19+ (or 22.12+) setup with npm. If that is not the case, please follow [the official Node.js installation guide](https://nodejs.org/en/download/) to install Node.js on your machine.
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh React project using Vite. Run the following command:
```bash
npm create vite@latest my-pdf-app -- --template react-ts
cd my-pdf-app
npm install
```
You can test it by running `npm run dev` and navigating to in your browser.
## Adding Oicana
[Section titled “Adding Oicana”](#adding-oicana)
Install the Oicana browser packages:
```bash
npm install @oicana/browser @oicana/browser-wasm
```
Keep both packages on the same version, also when you upgrade. `@oicana/browser` only works with the WASM file of its own release, and the app loads the WASM file from its own `@oicana/browser-wasm` dependency.
Note
The WASM file included in `@oicana/browser-wasm` is about 40 MB uncompressed (\~17 MB gzipped, \~12 MB brotli). It’s fetched once and browser-cached. For more about handling the WASM file read the [Deploying the Browser WASM](/docs/guides/browser-deployment/) guide. It covers pre-compression, CDN caveats, Web Worker offload, and a few other things that are easy to get wrong.
1. Create a `public` directory (if it doesn’t exist) and copy `example-0.1.0.zip` into it. This makes the template accessible to the browser.
2. Replace the contents of `src/App.tsx` with the following:
src/App.tsx
```tsx
import { useEffect, useState } from 'react';
import { initialize, Template, CompilationMode, Pdf } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
function App() {
const [template, setTemplate] = useState(null);
useEffect(() => {
async function setup() {
await initialize(wasmUrl);
const response = await fetch('/example-0.1.0.zip');
const templateBytes = new Uint8Array(await response.arrayBuffer());
setTemplate(new Template(templateBytes));
}
void setup();
}, []);
function generatePdf() {
const pdf = template!.export(
new Map(),
new Map(),
Pdf,
CompilationMode.Development,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
}
if (!template) {
return
Template is being prepared...
;
}
return (
);
}
export default App;
```
Start the dev server with `npm run dev` and click the “Generate PDF” button. The generated `example.pdf` file should contain your template with the development value.
Template lifetime in React
A `Template` holds memory inside the WASM heap, and `dispose()` releases it. The effect above never disposes, which is fine for a single-component demo but not a pattern to carry into an app.
The scaffold wraps the app in ``, so React runs the effect twice in development. That is two fetches and two `Template` instances, of which the first is dropped without being released. Cleaning that up takes some care. For example, you could do the following:
```tsx
useEffect(() => {
let created: Template | undefined;
let cancelled = false;
async function setup() {
await initialize(wasmUrl);
const response = await fetch('/example-0.1.0.zip');
const templateBytes = new Uint8Array(await response.arrayBuffer());
created = new Template(templateBytes);
if (cancelled) {
created.dispose();
return;
}
setTemplate(created);
}
void setup();
return () => {
cancelled = true;
created?.dispose();
};
}, []);
```
In a production application the template would likely live in a Web Worker to keep rendering off the main thread (see the performance section below).
## About performance
[Section titled “About performance”](#about-performance)
PDF compilation is CPU-bound and runs on whichever thread calls it, so any non-trivial UI should run it from a Web Worker rather than the main thread. The [Deploying the Browser WASM](/docs/guides/browser-deployment/) guide covers the production setup, and the open source [React example project](https://github.com/oicana/oicana-example-typescript-react/) shows a complete shared Web Worker setup.
## Passing inputs from TypeScript
[Section titled “Passing inputs from TypeScript”](#passing-inputs-from-typescript)
Our code currently compiles with empty inputs and development mode. This means Oicana will use the development value for the input. Now we’ll provide an explicit input value and switch to production mode. Update the `generatePdf` function:
Part of src/App.tsx
```tsx
function generatePdf() {
const jsonInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
const pdf = template!.export(
jsonInputs,
new Map(),
Pdf,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
}
```
*With this change, you need to remove the now unused `CompilationMode` import, or the build will fail.*
Your explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. Notice that we removed the explicit `CompilationMode.Development` parameter. The `export()` method defaults to `CompilationMode.Production`. Production mode is the recommended default - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values for inputs. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles `none` values for that input.
Clicking the button now will download a PDF with “Baby Yoda” instead of “Chuck Norris”. For a more complete example including Web Workers, take a look at the [open source React example project on GitHub](https://github.com/oicana/oicana-example-typescript-react/).
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling errors
[Section titled “Handling errors”](#handling-errors)
The setup promise can reject. If it does, nothing catches it, so the UI shows “Template is being prepared…” forever and the only trace is a console error. Catching the rejection and keeping the reason in state can make the failure visible:
Part of src/App.tsx
```tsx
const [template, setTemplate] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
async function setup() {
await initialize(wasmUrl);
const response = await fetch('/example-0.1.0.zip');
const contentType = response.headers.get('content-type') ?? '';
if (!response.ok || contentType.includes('text/html')) {
throw new Error(
`Failed to fetch the template: ${response.status} (${contentType})`,
);
}
const templateBytes = new Uint8Array(await response.arrayBuffer());
setTemplate(new Template(templateBytes));
}
setup().catch((cause) => setError(String(cause)));
}, []);
```
A missing template does not 404
Checking `response.ok` alone is not enough here. Vite’s dev server, `vite preview`, and most SPA hosts fall back to `index.html` for any path they cannot resolve, so a template that was never copied into `public/` (or whose name has a typo) comes back as `200` with an HTML body.
Compiling can fail too, most often because an input is missing or fails schema validation. Wrap the `export()` call in a `try`/`catch` that writes to the same error state, and add a branch that renders it. Without that branch the error is stored but never shown:
Part of src/App.tsx
```tsx
function generatePdf() {
const jsonInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
try {
const pdf = template!.export(
jsonInputs,
new Map(),
Pdf,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
} catch (cause) {
setError(String(cause));
}
}
if (error) {
return
Something went wrong: {error}
;
}
if (!template) {
return
Template is being prepared...
;
}
```
Note
Compilation errors carry the full Typst diagnostic, including the template source path and excerpt. That is useful while building the app, and fine when the user owns the template. If you ship a template that your users should not see the internals of, show a generic message.
Complete code at the end of this chapter
src/App.tsx
```tsx
import { useEffect, useState } from 'react';
import { initialize, Template, Pdf } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
function App() {
const [template, setTemplate] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let created: Template | undefined;
let cancelled = false;
async function setup() {
await initialize(wasmUrl);
const response = await fetch('/example-0.1.0.zip');
const contentType = response.headers.get('content-type') ?? '';
if (!response.ok || contentType.includes('text/html')) {
throw new Error(
`Failed to fetch the template: ${response.status} (${contentType})`,
);
}
const templateBytes = new Uint8Array(await response.arrayBuffer());
created = new Template(templateBytes);
if (cancelled) {
created.dispose();
return;
}
setTemplate(created);
}
setup().catch((cause) => setError(String(cause)));
return () => {
cancelled = true;
created?.dispose();
};
}, []);
function generatePdf() {
const jsonInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
try {
const pdf = template!.export(
jsonInputs,
new Map(),
Pdf,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
} catch (cause) {
setError(String(cause));
}
}
if (error) {
return
Something went wrong: {error}
;
}
if (!template) {
return
Template is being prepared...
;
}
return (
);
}
export default App;
```
# C# / ASP.NET
> Integrate Oicana into a C# web service using ASP.NET Core.
In this chapter, you’ll integrate Oicana into a C# web service using [ASP.NET Core](https://dotnet.microsoft.com/en-us/apps/aspnet). ASP.NET Core is Microsoft’s modern, cross-platform framework for building web applications and APIs. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working .NET 10 setup. If that is not the case, please follow [the official Microsoft guide](https://learn.microsoft.com/en-us/dotnet/core/install/) to install .NET on your machine. The Oicana package works with .NET 8 and newer. With an older version than .NET 10, the generated project and the ASP.NET related code in this guide may differ a bit.
The following also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh ASP.NET project by executing `dotnet new webapi` in a new directory. The starter project has a single endpoint defined in `Program.cs` and exposes the OpenAPI document at `/openapi/v1.json`, but no interactive UI is bundled by default. To get an API explorer, add [Scalar](https://scalar.com/) with `dotnet add package Scalar.AspNetCore` and wire it up in `Program.cs` next to the existing OpenAPI calls:
Part of Program.cs
```cs
using Scalar.AspNetCore; // <-- new
// ...
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference(); // <-- new
}
```
Start up the service (`dotnet run`) and open the URL printed in the terminal followed by `/scalar`. Expand the `/weatherforecast` endpoint, press “Test Request”, then “Send”. This will send an HTTP request to the running ASP.NET service and return made up weather data.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Create a new directory in the .NET project called `templates` and copy `example-0.1.0.zip` into that directory. Then tell MSBuild to copy it to the build output.
Part of your .csproj
```xml
```
2. Add the [`Oicana` NuGet package](https://www.nuget.org/packages/Oicana#readme-body-tab) as a dependency with `dotnet add package Oicana`.
3. Add the Oicana usings next to the `using Scalar.AspNetCore;` you already have at the top of `Program.cs`:
Top of Program.cs
```cs
using System.Text.Json.Nodes;
using Oicana.Config;
using Oicana;
```
Then read the template file and prepare it for compilation, above `var builder = WebApplication.CreateBuilder(args);`:
Part of Program.cs
```cs
var templateFile = await File.ReadAllBytesAsync(
Path.Combine(AppContext.BaseDirectory, "templates", "example-0.1.0.zip"));
var template = new Template(templateFile);
```
Note
Template registration (the `new Template()` call) compiles the template once in development mode to warm up the Typst cache. This happens at startup and uses the `development` value of the input you defined previously.
4. Replace the generated `/weatherforecast` endpoint with the following:
Part of Program.cs
```cs
app.MapPost("/compile", () =>
{
var stream = template.ExportPdf(mode: CompilationMode.Development);
var now = DateTimeOffset.Now;
return Results.File(
fileStream: stream,
contentType: "application/pdf",
fileDownloadName: $"example_{now:yyyy_MM_dd_HH_mm_ss_ffff}.pdf"
);
});
```
This code defines a new POST endpoint at `/compile`. For every request, it compiles the template to PDF without any input values and returns the file. We use `CompilationMode.Development` here to demonstrate how the template falls back to the development value you defined for the `info` input (`{ "name": "Chuck Norris" }`). In a later step we will explicitly set a value for the input.
After restarting the service and refreshing the Scalar UI, you should see the new endpoint. Click “Test Request” and “Send” to get a preview of the returned PDF file.

## About performance
[Section titled “About performance”](#about-performance)
The PDF generation should not take longer than a couple of milliseconds. You can look at the request duration in the network tab of your browser’s debugging tools for an estimation. The first request to an ASP.NET service can be significantly slower than later ones, because ASP.NET does some preparation during the first request.
For a better measurement of the compilation speed on your machine, you can use a [`Stopwatch`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch) in the endpoint code.
Note
`Template` is safe to share across requests and you can register it as a singleton. Compilations of the same `Template` instance serialize internally; for more concurrency, create multiple `Template` instances from the same template file.
Repeated compilations are fast because Typst memoizes its work in a global cache. The [cache management guide](/docs/guides/cache-management/) explains how that cache is evicted and when it pays off to tune it.
## Passing inputs from C\#
[Section titled “Passing inputs from C#”](#passing-inputs-from-c)
Our `compile` endpoint is currently calling the template’s `ExportPdf` method without any explicit input values. `ExportPdf` optionally takes a dictionary of JSON inputs as its first argument. Now we’ll provide an input value and switch to production mode.
Change the endpoint to set the name input you defined earlier.
Part of Program.cs
```cs
app.MapPost("/compile", () =>
{
var jsonInputs = new Dictionary
{
["info"] = JsonNode.Parse("{ \"name\": \"Baby Yoda\" }")!
};
var stream = template.ExportPdf(jsonInputs);
var now = DateTimeOffset.Now;
return Results.File(
fileStream: stream,
contentType: "application/pdf",
fileDownloadName: $"example_{now:yyyy_MM_dd_HH_mm_ss_ffff}.pdf"
);
});
```
The explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. Notice that we removed the explicit `mode: CompilationMode.Development` argument, so the `using Oicana.Config;` directive is no longer needed. `ExportPdf` defaults to `CompilationMode.Production` when no mode is specified. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless the input is marked as `required: false` and your template handles `none` values for it.
Calling the endpoint now, will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, one could set input values based on database entries or the request payload. Take a look at the open source [ASP.NET example project on GitHub](https://github.com/oicana/oicana-example-csharp-asp-net/) for a more complete showcase of the Oicana C# integration.
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling compilation errors
[Section titled “Handling compilation errors”](#handling-compilation-errors)
A missing required input or an input that fails schema validation makes the compilation fail. The tutorial’s `example` template input declares no [JSON schema](/docs/templates/inputs/#json-schemas), so only the first case can happen here. `ExportPdf` throws an `OicanaException` that we can catch:
Part of Program.cs
```cs
app.MapPost("/compile", (ILogger logger) =>
{
var jsonInputs = new Dictionary
{
["info"] = JsonNode.Parse("{ \"name\": \"Baby Yoda\" }")!
};
Stream stream;
try
{
stream = template.ExportPdf(jsonInputs);
}
catch (OicanaException exception)
{
logger.LogError(exception, "Failed to compile template");
return Results.Problem("Failed to generate the document");
}
var now = DateTimeOffset.Now;
return Results.File(
fileStream: stream,
contentType: "application/pdf",
fileDownloadName: $"example_{now:yyyy_MM_dd_HH_mm_ss_ffff}.pdf"
);
});
```
Complete code at the end of this chapter
Program.cs
```cs
using Scalar.AspNetCore;
using System.Text.Json.Nodes;
using Oicana;
var templateFile = await File.ReadAllBytesAsync(
Path.Combine(AppContext.BaseDirectory, "templates", "example-0.1.0.zip"));
var template = new Template(templateFile);
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.MapPost("/compile", (ILogger logger) =>
{
var jsonInputs = new Dictionary
{
["info"] = JsonNode.Parse("{ \"name\": \"Baby Yoda\" }")!
};
Stream stream;
try
{
stream = template.ExportPdf(jsonInputs);
}
catch (OicanaException exception)
{
logger.LogError(exception, "Failed to compile template");
return Results.Problem("Failed to generate the document");
}
var now = DateTimeOffset.Now;
return Results.File(
fileStream: stream,
contentType: "application/pdf",
fileDownloadName: $"example_{now:yyyy_MM_dd_HH_mm_ss_ffff}.pdf"
);
});
app.Run();
```
# Java / Spring Boot
> Integrate Oicana into a Java web service using Spring Boot.
In this chapter, you’ll integrate Oicana into a Java web service using [Spring Boot](https://spring.io/projects/spring-boot). Spring Boot is a popular Java framework for building production-ready web services. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Java 17+ setup with Gradle. If that is not the case, please follow [the Adoptium installation guide](https://adoptium.net/) to install a JDK and [install Gradle.](https://gradle.org/install/) Spring Boot 4 needs Gradle 8.14 or newer. Make sure your Gradle version [supports your Java version.](https://docs.gradle.org/current/userguide/compatibility.html)
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh Spring Boot project. Create a new directory and initialize it with Gradle:
```bash
mkdir my-pdf-service
cd my-pdf-service
gradle init --type basic --dsl kotlin
```
Replace the generated `build.gradle.kts` with:
build.gradle.kts
```kotlin
plugins {
java
id("org.springframework.boot") version "4.1.1"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.example"
version = "1.0.0"
tasks.withType().configureEach {
options.release = 17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("com.oicana:oicana:0.9.0")
// The following are all the native implementations that Oicana for Java has.
// To save on bandwidth and project size, remove the platforms you won't run this project on.
runtimeOnly("com.oicana:oicana-linux-x86_64:0.9.0")
runtimeOnly("com.oicana:oicana-linux-aarch64:0.9.0")
runtimeOnly("com.oicana:oicana-macos-x86_64:0.9.0")
runtimeOnly("com.oicana:oicana-macos-aarch64:0.9.0")
runtimeOnly("com.oicana:oicana-windows-x86_64:0.9.0")
}
```
Note
Keep only the `runtimeOnly` lines for the platforms you build and deploy on. Available options: `oicana-linux-x86_64`, `oicana-linux-aarch64`, `oicana-macos-x86_64`, `oicana-macos-aarch64`, `oicana-windows-x86_64`. You can list several if your team uses different platforms - only the matching one is loaded at runtime.
Create the main application class:
src/main/java/com/example/Application.java
```java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
Start the service with `./gradlew bootRun`. It has no endpoints yet, so answers with Spring’s Whitelabel error page and a 404 - that is the expected result at this point and means the application is up.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Create the directory `src/main/resources/templates` in the project and copy `example-0.1.0.zip` into it. Everything under `src/main/resources` is packed into the jar, so the template travels with the application.
2. Create a service to load and compile the template:
src/main/java/com/example/TemplateService.java
```java
package com.example;
import com.oicana.CompilationMode;
import com.oicana.ExportFormat;
import com.oicana.Template;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Service
public class TemplateService {
private Template template;
@PostConstruct
public void init() throws IOException {
var resource = new ClassPathResource("templates/example-0.1.0.zip");
try (var stream = resource.getInputStream()) {
template = new Template(stream.readAllBytes());
}
}
public byte[] compile() {
return template.export(
ExportFormat.pdf(),
CompilationMode.DEVELOPMENT
);
}
@PreDestroy
public void cleanup() {
template.close();
}
}
```
The `Template` constructor loads the template once. The `compile` method compiles it without inputs and `CompilationMode.DEVELOPMENT`, so the template uses the development value you defined for the `info` input (`{ "name": "Chuck Norris" }`). In a follow-up step, we will set an input value instead. The `@PreDestroy` cleanup releases native resources.
`Template` takes bytes, so if you would rather ship templates separately from the application, read them from a file, an object store, or a database instead.
3. Create a controller with a compile endpoint:
src/main/java/com/example/CompileController.java
```java
package com.example;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CompileController {
private final TemplateService templateService;
public CompileController(TemplateService templateService) {
this.templateService = templateService;
}
@PostMapping("/compile")
public ResponseEntity compile() {
byte[] pdf = templateService.compile();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"example.pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdf);
}
}
```
This code defines a new POST endpoint at `/compile`. For every request, it compiles the template and returns the PDF file.
After restarting the service, you can test the endpoint with curl:
```bash
curl -X POST http://localhost:8080/compile --output example.pdf
```
The generated `example.pdf` file should contain your template with the development value.
## Enabling native access
[Section titled “Enabling native access”](#enabling-native-access)
Oicana for Java loads a native library, and JDK 24 and newer restrict that. Running on such a JDK you get a warning like this at startup:
```plaintext
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by com.oicana.NativeLoader in an unnamed module (file:.../oicana-0.9.0.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
```
Other libraries that load native code trigger the same warning, but the JVM only names the first caller. The steps below enable native access for all of them.
Grant it in whichever way fits how you start the application:
* **Executable jar**: add `Enable-Native-Access: ALL-UNNAMED` to the jar manifest. This travels with the artifact and needs no launcher change, but only applies to `java -jar`.
Part of build.gradle.kts
```kotlin
tasks.named("bootJar") {
manifest { attributes("Enable-Native-Access" to "ALL-UNNAMED") }
}
```
* **Any other launch**, including `./gradlew bootRun` and containers: pass `--enable-native-access=ALL-UNNAMED` as a JVM argument.
Part of build.gradle.kts
```kotlin
tasks.named("bootRun") {
jvmArgs("--enable-native-access=ALL-UNNAMED")
}
```
* **On the module path**: the API jar is the named module `com.oicana`, so you can narrow the grant to `--enable-native-access=com.oicana` instead of opening up the whole classpath.
## About performance
[Section titled “About performance”](#about-performance)
The PDF generation should not take longer than a couple of milliseconds. The `Template` instance is thread-safe and can be shared across requests - Spring Boot’s singleton service scope handles this naturally.
Repeated compilations are fast because Typst memoizes its work in a global cache. The [cache management guide](/docs/guides/cache-management/) explains how that cache is evicted and when it pays off to tune it.
## Passing inputs from Java
[Section titled “Passing inputs from Java”](#passing-inputs-from-java)
Our `compile` method is currently calling `template.export` without inputs in development mode. Now we’ll provide an explicit input value and call the overload that takes inputs:
Part of TemplateService.java
```java
public byte[] compile() {
return template.export(
Map.of("info", "{\"name\": \"Baby Yoda\"}"),
Map.of()
);
}
```
*With this change, add `import java.util.Map;` and remove the now unused `CompilationMode` and `ExportFormat` imports.*
Your explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. We now pass JSON inputs and an empty blob inputs map. The `export(Map, Map)` overload defaults to `CompilationMode.PRODUCTION` and PDF output. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values for inputs. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles `none` values for that input.
Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the [open source Spring Boot example project on GitHub](https://github.com/oicana/oicana-example-java-spring-boot/) for a more complete showcase of the Oicana Java integration.
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling compilation errors
[Section titled “Handling compilation errors”](#handling-compilation-errors)
A missing required input or an input that fails schema validation makes the compilation fail. The tutorial’s `example` template declares no [JSON schema](/docs/templates/inputs/#json-schemas), so only the first case can happen here. `export` throws an `OicanaException` that we can catch:
Part of CompileController.java
```java
import com.oicana.OicanaException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger log =
LoggerFactory.getLogger(CompileController.class);
@PostMapping("/compile")
public ResponseEntity compile() {
byte[] pdf;
try {
pdf = templateService.compile();
} catch (OicanaException exception) {
log.error("Failed to compile template", exception);
return ResponseEntity.internalServerError()
.contentType(MediaType.TEXT_PLAIN)
.body("Failed to generate the document".getBytes());
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"example.pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdf);
}
```
Complete code at the end of this chapter
build.gradle.kts
```kotlin
plugins {
java
id("org.springframework.boot") version "4.1.1"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.example"
version = "1.0.0"
tasks.withType().configureEach {
options.release = 17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("com.oicana:oicana:0.9.0")
// Since this is an example project, we add all native implementations.
// In your project, only add what you need.
runtimeOnly("com.oicana:oicana-linux-x86_64:0.9.0")
runtimeOnly("com.oicana:oicana-linux-aarch64:0.9.0")
runtimeOnly("com.oicana:oicana-macos-x86_64:0.9.0")
runtimeOnly("com.oicana:oicana-macos-aarch64:0.9.0")
runtimeOnly("com.oicana:oicana-windows-x86_64:0.9.0")
}
tasks.named("bootJar") {
manifest { attributes("Enable-Native-Access" to "ALL-UNNAMED") }
}
tasks.named("bootRun") {
jvmArgs("--enable-native-access=ALL-UNNAMED")
}
```
settings.gradle.kts
```kotlin
rootProject.name = "my-pdf-service"
```
src/main/java/com/example/Application.java
```java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
src/main/java/com/example/TemplateService.java
```java
package com.example;
import com.oicana.Template;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;
@Service
public class TemplateService {
private Template template;
@PostConstruct
public void init() throws IOException {
var resource = new ClassPathResource("templates/example-0.1.0.zip");
try (var stream = resource.getInputStream()) {
template = new Template(stream.readAllBytes());
}
}
public byte[] compile() {
return template.export(
Map.of("info", "{\"name\": \"Baby Yoda\"}"),
Map.of()
);
}
@PreDestroy
public void cleanup() {
template.close();
}
}
```
src/main/java/com/example/CompileController.java
```java
package com.example;
import com.oicana.OicanaException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CompileController {
private static final Logger log =
LoggerFactory.getLogger(CompileController.class);
private final TemplateService templateService;
public CompileController(TemplateService templateService) {
this.templateService = templateService;
}
@PostMapping("/compile")
public ResponseEntity compile() {
byte[] pdf;
try {
pdf = templateService.compile();
} catch (OicanaException exception) {
log.error("Failed to compile template", exception);
return ResponseEntity.internalServerError()
.contentType(MediaType.TEXT_PLAIN)
.body("Failed to generate the document".getBytes());
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"example.pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdf);
}
}
```
# Node.js / NestJS
> Integrate Oicana into a Node.js web service using NestJS.
In this chapter, you’ll integrate Oicana into a Node.js web service using [NestJS](https://nestjs.com/). NestJS is a progressive Node.js framework for building efficient, scalable server-side applications. It uses TypeScript by default and provides a modular architecture. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Node.js 20.19+ (or 22.12+) setup with npm. If that is not the case, please follow [the official Node.js installation guide](https://nodejs.org/en/download/) to install Node.js on your machine. `@oicana/node` is ESM-only.
It also assumes you have the packed template `example-0.1.0.zip` from the previous getting started chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh NestJS project by executing `npx @nestjs/cli new oicana-demo` in a new directory. This will create a new NestJS application with a basic structure. The starter project has a single endpoint defined in the controller. We can test it by starting the service with `npm run start:dev` and navigating to in a browser.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Add the [`@oicana/node` npm package](https://www.npmjs.com/package/@oicana/node) as a dependency with `npm install @oicana/node`.
2. Generate a new controller and service for templates:
```bash
npx nest generate module templates
npx nest generate service templates --no-spec
npx nest generate controller templates --no-spec
```
3. Copy `example-0.1.0.zip` into the generated `src/templates` directory, then tell the Nest CLI to carry it into the build output:
Part of nest-cli.json
```json
"compilerOptions": {
"deleteOutDir": true,
"assets": ["**/*.zip"]
}
```
4. Update the templates service to load the template at startup:
src/templates/templates.service.ts
```typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Template, CompilationMode, Pdf, type BlobInput } from '@oicana/node';
import { promises as fs } from 'fs';
import { join } from 'path';
@Injectable()
export class TemplatesService implements OnModuleInit {
private template: Template;
async onModuleInit() {
const templatePath = join(import.meta.dirname, 'example-0.1.0.zip');
const buffer = await fs.readFile(templatePath);
// Creating the template runs a warm-up compilation in development mode,
// which uses the development value of the input.
this.template = new Template(buffer);
}
compile(): Uint8Array {
const jsonInputs = new Map();
const blobInputs = new Map();
return this.template.export(
jsonInputs,
blobInputs,
Pdf,
CompilationMode.Development,
);
}
}
```
`import.meta.dirname` needs ES module output. If your project compiles to CommonJS, use `__dirname` instead.
In the `compile` function, we pass empty input maps and explicitly set `CompilationMode.Development` so the template uses the development value you defined for the `info` input (`{ "name": "Chuck Norris" }`). In a follow-up step, we will set an input value instead.
5. Update the templates controller to add a compile endpoint:
src/templates/templates.controller.ts
```typescript
import { Controller, Post, Res } from '@nestjs/common';
import type { Response } from 'express';
import { TemplatesService } from './templates.service.js';
@Controller('templates')
export class TemplatesController {
constructor(private readonly templatesService: TemplatesService) {}
@Post('compile')
compile(@Res() res: Response) {
const pdf = this.templatesService.compile();
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="example.pdf"',
'Content-Length': pdf.length,
});
res.status(200).end(Buffer.from(pdf));
}
}
```
This code defines a new POST endpoint at `/templates/compile`. For every request, it compiles the template and returns the PDF file.
After restarting the service, you can test the endpoint with curl:
```bash
curl -X POST http://localhost:3000/templates/compile --output example.pdf
```
The generated `example.pdf` file should contain your template with the development value.
## About performance
[Section titled “About performance”](#about-performance)
The PDF generation should not take longer than a couple of milliseconds.
Compilation and export are CPU-bound, so the synchronous `export` used above blocks the Node.js event loop until the document is ready. Under load, prefer the async methods (`exportPdfAsync`, `compileAsync`, and friends), which run on Node.js’ libuv thread pool and keep the event loop free. The [async Node.js compilation guide](/docs/guides/nodejs-async/) covers the async API, thread pool sizing, and running compilations concurrently.
Repeated compilations are fast because Typst memoizes its work in a global cache. The [cache management guide](/docs/guides/cache-management/) explains how that cache is evicted and when it pays off to tune it.
## Passing inputs from Node.js
[Section titled “Passing inputs from Node.js”](#passing-inputs-from-nodejs)
Our `compile` method is currently calling `template.export()` with empty input maps and development mode. Now we’ll provide an explicit input value and switch to production mode:
Part of src/templates/templates.service.ts
```typescript
compile(): Uint8Array {
const jsonInputs = new Map();
const blobInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
return this.template.export(jsonInputs, blobInputs);
}
```
*With this change, you can remove the now unused `CompilationMode` and `Pdf` imports.*
Your explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. Notice that we removed the explicit `CompilationMode.Development` parameter. The `export()` method defaults to `CompilationMode.Production` when no mode is specified. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles `none` values for that input.
Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the [open source NestJS example project on GitHub](https://github.com/oicana/oicana-example-typescript-nestjs/) for a more complete showcase of the Oicana Node.js integration, including blob inputs, error handling, and Swagger documentation.
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling compilation errors
[Section titled “Handling compilation errors”](#handling-compilation-errors)
A missing required input or an input that fails schema validation makes the compilation fail. The tutorial’s `example` template declares no [JSON schema](/docs/templates/inputs/#json-schemas), so only the first case can happen here. `export()` throws exceptions that we can catch:
Part of src/templates/templates.controller.ts
```typescript
import {
Controller,
InternalServerErrorException,
Logger,
Post,
Res,
} from '@nestjs/common';
// previous imports...
@Controller('templates')
export class TemplatesController {
private readonly logger = new Logger(TemplatesController.name);
// constructor stays as it is...
@Post('compile')
compile(@Res() res: Response) {
let pdf: Uint8Array;
try {
pdf = this.templatesService.compile();
} catch (error) {
this.logger.error('Failed to compile template', error);
throw new InternalServerErrorException('Failed to generate the document');
}
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="example.pdf"',
'Content-Length': pdf.length,
});
res.status(200).end(Buffer.from(pdf));
}
}
```
Complete code at the end of this chapter
src/templates/templates.service.ts
```typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Template, type BlobInput } from '@oicana/node';
import { promises as fs } from 'fs';
import { join } from 'path';
@Injectable()
export class TemplatesService implements OnModuleInit {
private template: Template;
async onModuleInit() {
const templatePath = join(import.meta.dirname, 'example-0.1.0.zip');
const buffer = await fs.readFile(templatePath);
// Creating the template runs a warm-up compilation in development mode,
// which uses the development value of the input.
this.template = new Template(buffer);
}
compile(): Uint8Array {
const jsonInputs = new Map();
const blobInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
return this.template.export(jsonInputs, blobInputs);
}
}
```
src/templates/templates.controller.ts
```typescript
import {
Controller,
InternalServerErrorException,
Logger,
Post,
Res,
} from '@nestjs/common';
import type { Response } from 'express';
import { TemplatesService } from './templates.service.js';
@Controller('templates')
export class TemplatesController {
private readonly logger = new Logger(TemplatesController.name);
constructor(private readonly templatesService: TemplatesService) {}
@Post('compile')
compile(@Res() res: Response) {
let pdf: Uint8Array;
try {
pdf = this.templatesService.compile();
} catch (error) {
this.logger.error('Failed to compile template', error);
throw new InternalServerErrorException('Failed to generate the document');
}
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="example.pdf"',
'Content-Length': pdf.length,
});
res.status(200).end(Buffer.from(pdf));
}
}
```
# Rust / Axum
> Integrate Oicana into a Rust web service using axum.
In this chapter, you’ll integrate Oicana into a Rust web service using [axum](https://github.com/tokio-rs/axum). axum is a web application framework built on top of [Tokio](https://tokio.rs/) and [Tower](https://github.com/tower-rs/tower), designed for building fast, reliable HTTP services. We’ll create a simple async web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Rust setup with cargo. If that is not the case, please follow [the official Rust installation guide](https://www.rust-lang.org/tools/install) to install Rust on your machine.
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh Axum project. First, create a new binary project with `cargo new my-pdf-service` and change into the new `my-pdf-service` directory. Then add the necessary dependencies to your `Cargo.toml`:
Part of Cargo.toml
```toml
[dependencies]
oicana = "0.9.0"
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde_json = "1.0"
```
Run `cargo build` to download and compile the dependencies. This might take a few minutes on first run.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Create a new directory in the Rust project called `templates` and copy `example-0.1.0.zip` into that directory.
2. Replace the contents of `src/main.rs` with a basic Axum server that loads and compiles the template:
src/main.rs
```rust
use axum::{
Router,
body::Body,
extract::State,
http::{StatusCode, header},
response::{IntoResponse, Response},
routing::post,
};
use oicana::{
Template,
export::pdf::export_pdf,
files::packed::PackedTemplate,
input::{CompilationConfig, TemplateInputs},
};
use std::io::Cursor;
use std::sync::{Arc, Mutex};
const TEMPLATE: &[u8] = include_bytes!("../templates/example-0.1.0.zip");
#[tokio::main]
async fn main() {
let template = Template::init(Cursor::new(TEMPLATE)).expect("Failed to initialize template");
let template = Arc::new(Mutex::new(template));
let app = Router::new()
.route("/compile", post(compile))
.with_state(template);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("Server running at http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn compile(
State(template): State>>>,
) -> impl IntoResponse {
let mut template = template.lock().unwrap();
// Compile with development mode for demonstration
// (uses development fallback values for inputs)
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::development());
let result = match template.compile(inputs) {
Ok(result) => result,
Err(error) => {
eprintln!("Failed to compile template: {error}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
let pdf = match export_pdf(
&result.document,
&*template,
template.manifest().pdf_standards(),
template.manifest().pdf_tagged(),
None,
) {
Ok(pdf) => pdf,
Err(error) => {
eprintln!("Failed to export PDF: {error}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/pdf")
.header(
header::CONTENT_DISPOSITION,
"attachment; filename=\"example.pdf\"",
)
.body(Body::from(pdf))
.unwrap()
}
```
This code loads the template once at startup and wraps it in `Arc>>`. The `Arc` (Atomic Reference Counted pointer) allows sharing across threads, while `Mutex` provides the mutable access needed by `compile()`. When parallel requests come in, they share the same template - each request locks the mutex (one at a time), compiles, then releases the lock.
The `/compile` endpoint compiles the template and returns a PDF. We explicitly use `CompilationConfig::development()` here to demonstrate how the template uses the development value you defined for the `info` input (“Chuck Norris”). We will set an input value in a later step.
Start the service with `cargo run` and test the endpoint. You can use curl to download the PDF:
```bash
curl -X POST http://127.0.0.1:3000/compile --output example.pdf
```
The generated `example.pdf` file should contain your template with the development value.
## About performance
[Section titled “About performance”](#about-performance)
PDF generation should typically take only a few milliseconds per request. Since we’re loading the template once at startup and sharing it via `Arc`, there’s no file I/O overhead on subsequent requests.
No automatic warm-up compilation
`Template::init` only reads the template, it does not compile it. The first `compile()` call therefore pays the full compilation cost, and a required input without a `development` or `default` value fails there rather than at startup. Call `compile()` once after `init` if you want the same warm-up the other integrations run at template registration.
For managing multiple templates, the [open source Axum example project on GitHub](https://github.com/oicana/oicana-example-rust-axum/) demonstrates using a `DashMap` for thread-safe template caching.
Repeated compilations are fast because Typst memoizes its work in a global cache. The [cache management guide](/docs/guides/cache-management/) explains how that cache is evicted and when it pays off to tune it.
## Passing inputs from Rust
[Section titled “Passing inputs from Rust”](#passing-inputs-from-rust)
Our `compile` function currently does not set a value for the template input. Since we use `CompilationConfig::development()`, the development value of `{ "name": "Chuck Norris" }` is used. Now we’ll provide an explicit input value and switch to production mode:
Part of src/main.rs
```rust
async fn compile(
State(template): State>>>,
) -> impl IntoResponse {
let mut template = template.lock().unwrap();
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::production());
let json_value = serde_json::json!({ "name": "Baby Yoda" });
inputs.with_input(oicana::input::JsonInput::new(
"info".to_string(),
json_value.to_string(),
));
let result = match template.compile(inputs) {
Ok(result) => result,
Err(error) => {
eprintln!("Failed to compile template: {error}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
// ... PDF export and response code from before
}
```
Your explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. Notice that we switched to `CompilationConfig::production()` now that we’re providing explicit input values. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values for inputs. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles `none` values for that input.
Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the [open source Axum example project on GitHub](https://github.com/oicana/oicana-example-rust-axum/) for a more complete showcase of the Oicana Rust integration, including blob inputs, error handling, and OpenAPI documentation.
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling compilation errors
[Section titled “Handling compilation errors”](#handling-compilation-errors)
Inputs are checked before Typst runs. An input the template does not declare, an input supplied as the wrong kind, or a JSON input that fails its [JSON schema](/docs/templates/inputs/#json-schemas) returns `ValidationFailed`. Everything else fails during the Typst compilation and returns `CompilationFailed`, including a missing required input and unparsable JSON for an input without a schema.
Add `CompileError` to the `use oicana::{...}` list at the top of `src/main.rs`, then match on its variants in `compile`:
Part of src/main.rs
```rust
let result = match template.compile(inputs) {
Ok(result) => result,
Err(error @ CompileError::ValidationFailed(_)) => {
eprintln!("Rejected inputs: {error}");
return StatusCode::BAD_REQUEST.into_response();
}
Err(error @ CompileError::CompilationFailed(_)) => {
eprintln!("Failed to compile template: {error}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
```
`ValidationFailed` is the caller’s mistake and deserves a 4xx. `CompilationFailed` can be the template’s fault as well as the caller’s, so this handler answers with a 500. Compilation errors are multi-line and quote the offending lines of your template. They contain ANSI color codes by default; call `template.set_diagnostic_color(oicana::DiagnosticColor::None)` to turn those off.
A compilation can also produce warnings without failing, for example when a template asks for a font that is not available and Typst falls back. Those do not reach `stderr` on their own: check `result.warnings` and handle them accordingly. See [compilation warnings](/docs/integrations/#compilation-warnings) for why that matters.
Part of src/main.rs
```rust
if let Some(warnings) = &result.warnings {
eprintln!("Template compiled with warnings: {warnings}");
}
```
Complete code at the end of this chapter
Part of Cargo.toml
```toml
[dependencies]
oicana = "0.9.0"
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde_json = "1.0"
```
src/main.rs
```rust
use axum::{
Router,
body::Body,
extract::State,
http::{StatusCode, header},
response::{IntoResponse, Response},
routing::post,
};
use oicana::{
CompileError, Template,
export::pdf::export_pdf,
files::packed::PackedTemplate,
input::{CompilationConfig, TemplateInputs},
};
use std::io::Cursor;
use std::sync::{Arc, Mutex};
const TEMPLATE: &[u8] = include_bytes!("../templates/example-0.1.0.zip");
#[tokio::main]
async fn main() {
let template = Template::init(Cursor::new(TEMPLATE)).expect("Failed to initialize template");
let template = Arc::new(Mutex::new(template));
let app = Router::new()
.route("/compile", post(compile))
.with_state(template);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("Server running at http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn compile(
State(template): State>>>,
) -> impl IntoResponse {
let mut template = template.lock().unwrap();
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::production());
let json_value = serde_json::json!({ "name": "Baby Yoda" });
inputs.with_input(oicana::input::JsonInput::new(
"info".to_string(),
json_value.to_string(),
));
let result = match template.compile(inputs) {
Ok(result) => result,
Err(error @ CompileError::ValidationFailed(_)) => {
eprintln!("Rejected inputs: {error}");
return StatusCode::BAD_REQUEST.into_response();
}
Err(error @ CompileError::CompilationFailed(_)) => {
eprintln!("Failed to compile template: {error}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
if let Some(warnings) = &result.warnings {
eprintln!("Template compiled with warnings: {warnings}");
}
let pdf = match export_pdf(
&result.document,
&*template,
template.manifest().pdf_standards(),
template.manifest().pdf_tagged(),
None,
) {
Ok(pdf) => pdf,
Err(error) => {
eprintln!("Failed to export PDF: {error}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/pdf")
.header(
header::CONTENT_DISPOSITION,
"attachment; filename=\"example.pdf\"",
)
.body(Body::from(pdf))
.unwrap()
}
```
# Python / FastAPI
> Integrate Oicana into a Python web service using FastAPI.
In this chapter, you’ll integrate Oicana into a Python web service using [FastAPI](https://fastapi.tiangolo.com/). FastAPI is a modern, high-performance web framework for building APIs with Python based on standard Python type hints. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Python 3.10+ setup with uv. If that is not the case, please follow [the official Python installation guide](https://www.python.org/downloads/) and [get uv.](https://docs.astral.sh/uv/#installation) The `oicana` package itself supports Python 3.9, but current FastAPI versions do not.
`oicana` ships prebuilt wheels only, without a source distribution, so check the [supported platforms](/docs/integrations/#supported-platforms).
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh FastAPI project. First, create a new directory for your project, then initialize it and install FastAPI with `uv init && uv add "fastapi[standard]"`. Replace the content of `main.py` with the following basic FastAPI application:
main.py
```python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
```
You can test it by running `uv run fastapi dev main.py` and navigating to in your browser.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Create a new directory in the Python project called `templates` and copy `example-0.1.0.zip` into that directory.
2. Add the [`oicana` PyPI package](https://pypi.org/project/oicana/) as a dependency with `uv add oicana`.
3. Update `main.py` to load the template at startup and add a compile endpoint:
main.py
```python
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import Response
from oicana import Template, CompilationMode
template: Template
@asynccontextmanager
async def lifespan(app: FastAPI):
global template
template_path = Path(__file__).parent / "templates" / "example-0.1.0.zip"
template_bytes = template_path.read_bytes()
# Creating the template runs a warm-up compilation in development mode,
# which uses the development value of the input.
template = Template(template_bytes)
yield
app = FastAPI(lifespan=lifespan)
@app.post(
"/compile",
response_class=Response,
responses={200: {"content": {"application/pdf": {}}}},
)
def compile_template():
pdf = template.export_pdf(mode=CompilationMode.DEVELOPMENT)
return Response(
content=pdf,
media_type="application/pdf",
headers={
"Content-Disposition": "attachment; filename=example.pdf"
},
)
```
This code loads the template once at application startup using FastAPI’s lifespan context manager. The `/compile` endpoint compiles the template and returns the PDF file. We explicitly pass the compilation mode with `template.export_pdf(mode=CompilationMode.DEVELOPMENT)` so the template uses the development value you defined for the `info` input (`{ "name": "Chuck Norris" }`). In a follow-up step, we will set an input value instead.
After restarting the service, you can test the endpoint with curl:
```bash
curl -X POST http://localhost:8000/compile --output example.pdf
```
The generated `example.pdf` file should contain your template with the development value.
You can also explore the automatically generated API documentation by navigating to in your browser. FastAPI provides interactive API documentation out of the box.
## About performance
[Section titled “About performance”](#about-performance)
The PDF generation should not take longer than a couple of milliseconds.
Note that the endpoint uses plain `def` instead of `async def`. PDF compilation is synchronous native code, so it would block the event loop in an `async def` endpoint. FastAPI runs `def` endpoints in a thread pool, and Oicana releases the GIL during compilation, so the service stays responsive.
Compilations of the same `Template` instance serialize internally. For more concurrency under heavy load, create multiple `Template` instances from the same template file or run multiple worker processes with `fastapi run --workers 4` or `uvicorn main:app --workers 4`.
Repeated compilations are fast because Typst memoizes its work in a global cache. The [cache management guide](/docs/guides/cache-management/) explains how that cache is evicted and when it pays off to tune it.
## Passing inputs from Python
[Section titled “Passing inputs from Python”](#passing-inputs-from-python)
Our `compile_template` function is currently calling `template.export_pdf()` with development mode. Now we’ll provide explicit input values and switch to production mode:
Part of main.py
```python
# add to the imports at the top of the file
import json
# ... everything else stays as it is, but replace the compile_template
# function with this version:
@app.post(
"/compile",
response_class=Response,
responses={200: {"content": {"application/pdf": {}}}},
)
def compile_template():
pdf = template.export_pdf(
json_inputs={"info": json.dumps({"name": "Baby Yoda"})}
)
return Response(
content=pdf,
media_type="application/pdf",
headers={
"Content-Disposition": "attachment; filename=example.pdf"
},
)
```
*With this change, `CompilationMode` is no longer used and can be dropped from the `oicana` import.*
Your explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. Notice that we removed the explicit `mode=CompilationMode.DEVELOPMENT` parameter. The `export_pdf()` method defaults to `CompilationMode.PRODUCTION` when no mode is specified. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles `none` values for that input.
Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the [open source FastAPI example project on GitHub](https://github.com/oicana/oicana-example-python-fastapi/) for a more complete showcase of the Oicana Python integration, including blob inputs, error handling, and request models.
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling compilation errors
[Section titled “Handling compilation errors”](#handling-compilation-errors)
A missing required input or an input that fails schema validation makes the compilation fail. The tutorial’s `example` template declares no [JSON schema](/docs/templates/inputs/#json-schemas), so only the first case can happen here. Wrap the call so you can log the details and answer with something useful:
Part of main.py
```python
import json
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from oicana import OicanaError, Template
# previous code...
@app.post(
"/compile",
response_class=Response,
responses={200: {"content": {"application/pdf": {}}}},
)
def compile_template():
try:
pdf = template.export_pdf(
json_inputs={"info": json.dumps({"name": "Baby Yoda"})}
)
except OicanaError as error:
logging.exception("Failed to compile template")
raise HTTPException(
status_code=500, detail="Failed to generate the document"
) from error
return Response(
content=pdf,
media_type="application/pdf",
headers={
"Content-Disposition": "attachment; filename=example.pdf"
},
)
```
Note the three changes to the imports: `logging`, `HTTPException` alongside `FastAPI`, and `OicanaError` alongside `Template`. `OicanaError` is a subclass of `RuntimeError`.
Complete code at the end of this chapter
main.py
```python
import json
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from oicana import OicanaError, Template
template: Template
@asynccontextmanager
async def lifespan(app: FastAPI):
global template
template_path = Path(__file__).parent / "templates" / "example-0.1.0.zip"
template_bytes = template_path.read_bytes()
# Creating the template runs a warm-up compilation in development mode,
# which uses the development value of the input.
template = Template(template_bytes)
yield
app = FastAPI(lifespan=lifespan)
@app.post(
"/compile",
response_class=Response,
responses={200: {"content": {"application/pdf": {}}}},
)
def compile_template():
try:
pdf = template.export_pdf(
json_inputs={"info": json.dumps({"name": "Baby Yoda"})}
)
except OicanaError as error:
logging.exception("Failed to compile template")
raise HTTPException(
status_code=500, detail="Failed to generate the document"
) from error
return Response(
content=pdf,
media_type="application/pdf",
headers={
"Content-Disposition": "attachment; filename=example.pdf"
},
)
```
# PHP / Slim
> Integrate Oicana into a PHP web service using Slim.
In this chapter, you’ll integrate Oicana into a PHP web service using [Slim](https://www.slimframework.com/). Slim is a lightweight PHP micro-framework well-suited for building APIs and web services. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working PHP 8.3+ setup with Composer. If that is not the case, please follow [the official PHP installation guide](https://www.php.net/manual/en/install.php) and [the Composer installation guide](https://getcomposer.org/doc/00-intro.md) to set up your environment.
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh Slim project. Create a new directory for your PHP project (separate from your template directory) and initialize it with Composer:
```bash
mkdir my-pdf-service
cd my-pdf-service
composer init
```
Install Slim with its PSR-7 implementation:
```bash
composer require slim/slim slim/psr7
```
Create an `index.php` file with the following basic Slim application:
index.php
```php
get('/', function (Request $request, Response $response) {
$response->getBody()->write('Hello World');
return $response;
});
$app->run();
```
You can test it by running `php -S localhost:8000 index.php` and navigating to in your browser. You should see “Hello World”.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Create a new directory in the PHP project called `templates` and copy `example-0.1.0.zip` into that directory.
2. The following commands add the `oicana/oicana` Composer package as a dependency. The package is hosted on a custom Composer repository, so we need to register it, allow the Oicana installer plugin, and finally install the package:
```bash
composer config repositories.oicana composer https://composer.oicana.com
composer config allow-plugins.oicana/installer true
composer require oicana/oicana:^0.9.0
```
Note
The installer downloads the native extension binary into `vendor/` and writes a PHP ini file at `vendor/oicana/installer/php/oicana.ini`. At the end of the install output it prints the exact command to activate the extension, already filled in with your project’s absolute path. Copy that line and run it before starting PHP.
Lost the install output? Run `vendor/bin/oicana-env` to print the same command again. Verify the extension is loaded with `php -m | grep oicana`.
3. Update `index.php` to load the template at startup and replace the `GET /` route with a compile endpoint:
index.php
```php
post('/compile', function (Request $request, Response $response) use ($template) {
$pdf = $template->export(mode: CompilationMode::Development);
$response->getBody()->write($pdf);
return $response
->withHeader('Content-Type', 'application/pdf')
->withHeader('Content-Disposition', 'attachment; filename="example.pdf"');
});
$app->run();
```
This code loads the template once at application startup. The `/compile` endpoint compiles the template and returns the PDF file. We explicitly pass the compilation mode with `$template->export(mode: CompilationMode::Development)` so the template uses the development value you defined for the `info` input (`{ "name": "Chuck Norris" }`). In a follow-up step, we will set an input value instead.
The PHP development server needs the `PHP_INI_SCAN_DIR` line printed by the installer to be active in its shell. If you switch to a new shell, run the line that `vendor/bin/oicana-env` prints first. Then start the server:
```bash
php -S localhost:8000 index.php
```
If a request answers with an empty HTTP 500, check the server console. `The oicana PHP extension is not loaded` means that `PHP_INI_SCAN_DIR` is not set in this shell.
Note
PHP’s built-in development server (`php -S`) is a single long-lived process, but it re-executes the script from scratch for every request, so state like loaded templates is not preserved between requests. This is fine for quick testing, but for production deployments, use a long-running PHP application server like [RoadRunner](https://roadrunner.dev/). RoadRunner keeps your PHP worker process alive, so templates are loaded once at startup and stay in memory across all requests. See the [example project on GitHub](https://github.com/oicana/oicana-example-php-slim/) for a RoadRunner setup.
Test the endpoint with curl:
```bash
curl -X POST http://localhost:8000/compile --output example.pdf
```
The generated `example.pdf` file should contain your template with the development value.
## About performance
[Section titled “About performance”](#about-performance)
The PDF generation should not take longer than a couple of milliseconds. You can measure the compilation time with PHP’s built-in `microtime(true)` around the `export()` call.
Repeated compilations are fast because Typst memoizes its work in a global cache. The [cache management guide](/docs/guides/cache-management/) explains how that cache is evicted and when it pays off to tune it. This matters on a long-running server like RoadRunner, where the process stays alive across requests.
## Passing inputs from PHP
[Section titled “Passing inputs from PHP”](#passing-inputs-from-php)
Our `compile` endpoint is currently calling `$template->export()` with development mode. Now we’ll provide explicit input values and switch to production mode:
Part of index.php
```php
$app->post('/compile', function (Request $request, Response $response) use ($template) {
$pdf = $template->export(
jsonInputs: ['info' => ['name' => 'Baby Yoda']]
);
$response->getBody()->write($pdf);
return $response
->withHeader('Content-Type', 'application/pdf')
->withHeader('Content-Disposition', 'attachment; filename="example.pdf"');
});
```
*With this change, you can remove the now unused `use Oicana\CompilationMode;` statement.*
Your explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. Notice that we removed the explicit `mode: CompilationMode::Development` parameter. The `export()` method defaults to `CompilationMode::Production` when no mode is specified. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles `none` values for that input.
Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the [open source PHP example project on GitHub](https://github.com/oicana/oicana-example-php-slim/) for a more complete showcase of the Oicana PHP integration, including blob inputs, error handling, and request validation.
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling compilation errors
[Section titled “Handling compilation errors”](#handling-compilation-errors)
A missing required input or an input that fails schema validation makes the compilation fail. The tutorial’s `example` template declares no [JSON schema](/docs/templates/inputs/#json-schemas), so only the first case can happen here. `export()` throws an `Oicana\OicanaException`, a `\RuntimeException`, that we can catch after adding `use Oicana\OicanaException;` to the imports:
Part of index.php
```php
$app = AppFactory::create();
$app->addErrorMiddleware(false, true, true);
$app->post('/compile', function (Request $request, Response $response) use ($template) {
try {
$pdf = $template->export(
jsonInputs: ['info' => ['name' => 'Baby Yoda']]
);
} catch (OicanaException $error) {
error_log('Failed to compile template: ' . $error->getMessage());
$response->getBody()->write('Failed to generate the document');
return $response->withStatus(500);
}
$response->getBody()->write($pdf);
return $response
->withHeader('Content-Type', 'application/pdf')
->withHeader('Content-Disposition', 'attachment; filename="example.pdf"');
});
```
The first argument of `addErrorMiddleware()` is `displayErrorDetails`. Keep it `false` in production so an unexpected exception anywhere in your app returns a plain error instead of a stack trace. Set it to `true` only while developing locally.
Complete code at the end of this chapter
index.php
```php
addErrorMiddleware(false, true, true);
$app->post('/compile', function (Request $request, Response $response) use ($template) {
try {
$pdf = $template->export(
jsonInputs: ['info' => ['name' => 'Baby Yoda']]
);
} catch (OicanaException $error) {
error_log('Failed to compile template: ' . $error->getMessage());
$response->getBody()->write('Failed to generate the document');
return $response->withStatus(500);
}
$response->getBody()->write($pdf);
return $response
->withHeader('Content-Type', 'application/pdf')
->withHeader('Content-Disposition', 'attachment; filename="example.pdf"');
});
$app->run();
```
# Go / net/http
> Integrate Oicana into a Go web service using net/http.
In this chapter, you’ll integrate Oicana into a Go web service using [net/http](https://pkg.go.dev/net/http) from the standard library. Since Go 1.22, its router matches HTTP methods and path patterns, so many Go services need no web framework at all. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Go 1.22+ setup. If that is not the case, please follow [the official Go installation guide](https://go.dev/doc/install).
The Oicana module links a native library through cgo, so you also need a C toolchain: GCC or Clang on Linux and macOS, and [MinGW-w64](https://www.mingw-w64.org/) on Windows. Check the [supported platforms](/docs/integrations/#supported-platforms).
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh Go module. Create a new directory for your project (separate from your template directory) and initialize the module:
```bash
mkdir my-pdf-service
cd my-pdf-service
go mod init example.com/my-pdf-service
```
Create a `main.go` file with the following basic web service:
main.go
```go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
You can test it by running `go run .` and navigating to in your browser. You should see “Hello World”.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Create a new directory in the Go project called `templates` and copy `example-0.1.0.zip` into that directory.
2. Add the [Oicana module](https://github.com/oicana/oicana/tree/main/integrations/go) as a dependency and fetch the native library for your platform:
```bash
go get go.oicana.com/oicana
export CGO_LDFLAGS="$(go run go.oicana.com/oicana/cmd/oicana-install)"
```
Note
The installer downloads the native library into your user cache directory, verifies its checksum, and prints the linker flags that link it into your program. Run it inside your module, without a version, so that it installs the library matching the version in your `go.mod`. In PowerShell, use `$env:CGO_LDFLAGS = go run go.oicana.com/oicana/cmd/oicana-install`.
`go build` and `go run` read the linker flags from `CGO_LDFLAGS`, so set the variable again in every new shell. If a build fails with `undefined reference to ffi_oicana_uniffi_...`, the variable is not set.
The library is linked statically, so the built binary does not need the installer or the cache directory at runtime. It still needs cgo at build time, so don’t build with `CGO_ENABLED=0`, and it links the C runtime dynamically, so run it on an image with a C library.
3. Update `main.go` to load the template at startup and replace the `GET /` route with a compile endpoint:
main.go
```go
package main
import (
"log"
"net/http"
"os"
oicana "go.oicana.com/oicana"
)
func main() {
templateBytes, err := os.ReadFile("templates/example-0.1.0.zip")
if err != nil {
log.Fatalf("Failed to load the template file: %v", err)
}
// Creating the template runs a warm-up compilation in development mode,
// which uses the development value of the input.
template, err := oicana.NewTemplate(templateBytes)
if err != nil {
log.Fatalf("Failed to create the template: %v", err)
}
defer template.Close()
http.HandleFunc("POST /compile", func(w http.ResponseWriter, r *http.Request) {
pdf, err := template.ExportPdf(oicana.Inputs{}, oicana.WithMode(oicana.Development))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", `attachment; filename="example.pdf"`)
w.Write(pdf)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
Run `go mod tidy` to record the Oicana module as a direct dependency now that `main.go` imports it.
This code loads the template once at application startup. The `/compile` endpoint compiles the template and returns the PDF file. We pass no inputs and explicitly set the compilation mode with `oicana.WithMode(oicana.Development)`, so the template uses the development value you defined for the `info` input (`{ "name": "Chuck Norris" }`). In a follow-up step, we will set an input value instead.
After restarting the service with `go run .`, you can test the endpoint with curl:
```bash
curl -X POST http://localhost:8080/compile --output example.pdf
```
The generated `example.pdf` file should contain your template with the development value.
## About performance
[Section titled “About performance”](#about-performance)
The PDF generation should not take longer than a couple of milliseconds.
`net/http` serves every request in its own goroutine, and a `Template` is safe for concurrent use, so a single instance can serve all requests. Compilations of the same `Template` instance serialize internally. For more concurrency under heavy load, create multiple `Template` instances from the same template file.
Repeated compilations are fast because Typst memoizes its work in a global cache. The [cache management guide](/docs/guides/cache-management/) explains how that cache is evicted and when it pays off to tune it.
## Passing inputs from Go
[Section titled “Passing inputs from Go”](#passing-inputs-from-go)
Our compile endpoint is currently calling `template.ExportPdf()` with development mode. Now we’ll provide explicit input values and switch to production mode. Replace the `POST /compile` handler in `main.go` with this version:
Part of main.go
```go
http.HandleFunc("POST /compile", func(w http.ResponseWriter, r *http.Request) {
pdf, err := template.ExportPdf(oicana.Inputs{
JSON: map[string]any{"info": map[string]any{"name": "Baby Yoda"}},
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", `attachment; filename="example.pdf"`)
w.Write(pdf)
})
```
JSON input values are encoded with `json.Marshal`, so you can also pass your own structs. A Go `string` becomes a JSON string, so pass a `json.RawMessage` for JSON you already have.
Your explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. Notice that we removed the explicit `oicana.WithMode(oicana.Development)` option. `ExportPdf()` defaults to `oicana.Production` when no mode is specified. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles `none` values for that input.
Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the [open source Go example project on GitHub](https://github.com/oicana/oicana-example-go-net-http/) for a more complete showcase of the Oicana Go integration, including blob inputs, error handling, and request validation.
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling compilation errors
[Section titled “Handling compilation errors”](#handling-compilation-errors)
A missing required input or an input that fails schema validation makes the compilation fail. The tutorial’s `example` template declares no [JSON schema](/docs/templates/inputs/#json-schemas), so only the first case can happen here. Our endpoint currently sends the error message to the client, which can leak details of your template. Replace the handler again to log the error and answer with a generic message:
Part of main.go
```go
http.HandleFunc("POST /compile", func(w http.ResponseWriter, r *http.Request) {
pdf, err := template.ExportPdf(oicana.Inputs{
JSON: map[string]any{"info": map[string]any{"name": "Baby Yoda"}},
})
if err != nil {
log.Printf("Failed to compile template: %v", err)
http.Error(w, "Failed to generate the document", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", `attachment; filename="example.pdf"`)
w.Write(pdf)
})
```
Every error returned by the Oicana module is an `*oicana.Error`. Check its kind with `errors.Is`. `oicana.ErrInvalidInput` covers inputs that fail schema validation or that the template does not declare, so you can answer those with HTTP 400. A missing required input fails the compilation itself and is an `oicana.ErrCompilation`.
Complete code at the end of this chapter
main.go
```go
package main
import (
"log"
"net/http"
"os"
oicana "go.oicana.com/oicana"
)
func main() {
templateBytes, err := os.ReadFile("templates/example-0.1.0.zip")
if err != nil {
log.Fatalf("Failed to load the template file: %v", err)
}
// Creating the template runs a warm-up compilation in development mode,
// which uses the development value of the input.
template, err := oicana.NewTemplate(templateBytes)
if err != nil {
log.Fatalf("Failed to create the template: %v", err)
}
defer template.Close()
http.HandleFunc("POST /compile", func(w http.ResponseWriter, r *http.Request) {
pdf, err := template.ExportPdf(oicana.Inputs{
JSON: map[string]any{"info": map[string]any{"name": "Baby Yoda"}},
})
if err != nil {
log.Printf("Failed to compile template: %v", err)
http.Error(w, "Failed to generate the document", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", `attachment; filename="example.pdf"`)
w.Write(pdf)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
# Ruby / Rails
> Integrate Oicana into a Ruby web service using Rails.
In this chapter, you’ll integrate Oicana into a Ruby web service using [Ruby on Rails](https://rubyonrails.org/). Rails is the most widely used Ruby web framework, and its API mode is a lean setup for web services. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Ruby 3.2+ setup with Rails 8. If that is not the case, please follow [the official Ruby installation guide](https://www.ruby-lang.org/en/documentation/installation/) and install Rails with `gem install rails`.
The `oicana` gem ships prebuilt platform gems that contain the native library, so check the [supported platforms](/docs/integrations/#supported-platforms).
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh Rails project in API mode. Create it in a new directory, separate from your template directory:
```bash
rails new my-pdf-service --api
cd my-pdf-service
```
Create a controller with a basic action:
app/controllers/hello\_controller.rb
```ruby
class HelloController < ApplicationController
def index
render plain: "Hello World"
end
end
```
Then route the root path to it by adding a line to `config/routes.rb`. Keep the `up` route that Rails generated, the health check of the generated Docker and Kamal setup:
config/routes.rb
```ruby
Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check
get "/", to: "hello#index"
end
```
You can test it by running `bin/rails server` and navigating to in your browser. You should see “Hello World”.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Create a new directory in the Rails project called `templates` and copy `example-0.1.0.zip` into that directory.
2. Add the [`oicana` gem](https://rubygems.org/gems/oicana) as a dependency with `bundle add oicana`.
3. Define the template in an initializer. It is created once per process, on first use:
config/initializers/oicana.rb
```ruby
module ExampleTemplate
MUTEX = Mutex.new
def self.instance
MUTEX.synchronize do
@instance ||= Oicana::Template.new(Rails.root.join("templates/example-0.1.0.zip").binread)
end
end
end
```
4. Replace the hello controller with a controller for the compile endpoint. You can delete `app/controllers/hello_controller.rb`.
app/controllers/documents\_controller.rb
```ruby
class DocumentsController < ApplicationController
def compile
pdf = ExampleTemplate.instance.export_pdf(mode: :development)
send_data pdf, type: "application/pdf", filename: "example.pdf"
end
end
```
5. Replace the root route with a route to the new action:
config/routes.rb
```ruby
Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check
post "/compile", to: "documents#compile"
end
```
The first request loads the template, and every later request of the same process reuses it. The `/compile` endpoint compiles the template and returns the PDF file. We explicitly pass the compilation mode with `export_pdf(mode: :development)` so the template uses the development value you defined for the `info` input (`{ "name": "Chuck Norris" }`). In a follow-up step, we will set an input value instead.
After restarting the server with `bin/rails server`, you can test the endpoint with curl:
```bash
curl -X POST http://localhost:3000/compile --output example.pdf
```
The generated `example.pdf` file should contain your template with the development value.
## About performance
[Section titled “About performance”](#about-performance)
The PDF generation should not take longer than a couple of milliseconds.
Puma, the Rails application server, handles requests on a pool of threads. A `Template` is safe to use from multiple threads, and Oicana releases the GVL while it compiles, so the other threads keep serving requests in the meantime. Compilations of the same `Template` instance serialize internally. For more concurrency under heavy load, create multiple `Template` instances from the same template file.
Caution
Oicana cannot compile in a process forked from a process that already compiled a template, and raises an `Oicana::Error` there. Puma’s cluster mode (`WEB_CONCURRENCY` above 1) and Solid Queue, the default job backend of Rails, fork their workers after the application booted. Creating the template on first use, as above, gives every worker its own template. Don’t create templates in an initializer or anywhere else during boot.
Repeated compilations are fast because Typst memoizes its work in a global cache. The [cache management guide](/docs/guides/cache-management/) explains how that cache is evicted and when it pays off to tune it.
## Passing inputs from Ruby
[Section titled “Passing inputs from Ruby”](#passing-inputs-from-ruby)
Our `compile` action is currently calling `export_pdf` with development mode. Now we’ll provide explicit input values and switch to production mode:
app/controllers/documents\_controller.rb
```ruby
class DocumentsController < ApplicationController
def compile
pdf = ExampleTemplate.instance.export_pdf(json_inputs: { "info" => { name: "Baby Yoda" } })
send_data pdf, type: "application/pdf", filename: "example.pdf"
end
end
```
JSON input values can be Hashes and any other value `JSON.generate` accepts, or Strings that already hold JSON. To pass a JSON string value, encode it first, for example `"Baby Yoda".to_json`.
Your explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. Notice that we removed the explicit `mode: :development` argument. `export_pdf` defaults to `:production` when no mode is specified. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles `none` values for that input.
Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the [open source Rails example project on GitHub](https://github.com/oicana/oicana-example-ruby-rails/) for a more complete showcase of the Oicana Ruby integration, including blob inputs, error handling, and request validation.
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling compilation errors
[Section titled “Handling compilation errors”](#handling-compilation-errors)
A missing required input or an input that fails schema validation makes the compilation fail. The tutorial’s `example` template declares no [JSON schema](/docs/templates/inputs/#json-schemas), so only the first case can happen here. Oicana raises subclasses of `Oicana::Error`, which a controller can handle with `rescue_from`:
app/controllers/documents\_controller.rb
```ruby
class DocumentsController < ApplicationController
rescue_from Oicana::Error do |error|
Rails.logger.error("Failed to compile template: #{error.message}")
render plain: "Failed to generate the document", status: :internal_server_error
end
def compile
pdf = ExampleTemplate.instance.export_pdf(json_inputs: { "info" => { name: "Baby Yoda" } })
send_data pdf, type: "application/pdf", filename: "example.pdf"
end
end
```
To answer invalid request payloads with HTTP 400 instead, rescue `Oicana::InvalidInputError` separately. It covers inputs that fail schema validation or that the template does not declare. A missing required input fails the compilation itself and raises `Oicana::CompilationError`.
Complete code at the end of this chapter
config/initializers/oicana.rb
```ruby
module ExampleTemplate
MUTEX = Mutex.new
def self.instance
MUTEX.synchronize do
@instance ||= Oicana::Template.new(Rails.root.join("templates/example-0.1.0.zip").binread)
end
end
end
```
app/controllers/documents\_controller.rb
```ruby
class DocumentsController < ApplicationController
rescue_from Oicana::Error do |error|
Rails.logger.error("Failed to compile template: #{error.message}")
render plain: "Failed to generate the document", status: :internal_server_error
end
def compile
pdf = ExampleTemplate.instance.export_pdf(json_inputs: { "info" => { name: "Baby Yoda" } })
send_data pdf, type: "application/pdf", filename: "example.pdf"
end
end
```
config/routes.rb
```ruby
Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check
post "/compile", to: "documents#compile"
end
```
# Choose Your Integration
> Pick the integration that matches your tech stack.
You’ve created a working Oicana template with a dynamic input! Now it’s time to integrate it into an application.
Have your template ready
All integration guides expect you to have the packed Oicana template `example-0.1.0.zip` ready, that we built in the previous sections.
## One Template, Multiple Platforms
[Section titled “One Template, Multiple Platforms”](#one-template-multiple-platforms)
A key strength of Oicana is that the exact same template works across all integrations. The `example-0.1.0.zip` file you created can be used in C#, Java, Node.js, Rust, Python, PHP, Go, Ruby, or browser environments.
Develop templates once and use them everywhere.
## Available Integrations
[Section titled “Available Integrations”](#available-integrations)
The following chapters provide step-by-step guides for using your template with different programming languages and frameworks. You only need to follow one path to get started - pick the one that matches your tech stack.
If you’re working on a multi-language project or want to compare approaches, feel free to explore multiple paths. Each guide is self-contained and uses the same template you created earlier.
[Browser / React](/docs/getting-started/4-1-browser/)Prerequisites: Node.js 20.19+
[C# / ASP.NET](/docs/getting-started/4-2-csharp/)Prerequisites: .NET 8+; preferably version 10
[Java / Spring Boot](/docs/getting-started/4-3-java/)Prerequisites: Java 17+ and Gradle
[Node.js / NestJS](/docs/getting-started/4-4-nodejs/)Prerequisites: Node.js 20.19+
[Rust / Axum](/docs/getting-started/4-5-rust/)Prerequisites: Rust toolchain (cargo)
[Python / FastAPI](/docs/getting-started/4-6-python/)Prerequisites: Python 3.10+ and uv
[PHP / Slim](/docs/getting-started/4-7-php/)Prerequisites: PHP 8.3+ and Composer
[Go / net/http](/docs/getting-started/4-8-go/)Prerequisites: Go 1.22+ and a C toolchain
[Ruby / Rails](/docs/getting-started/4-9-ruby/)Prerequisites: Ruby 3.2+ and Rails 8
## Next Steps
[Section titled “Next Steps”](#next-steps)
Choose one of the integration guides above to continue. Each guide will show you how to:
1. Set up a basic web service in your chosen language/framework
2. Load and compile your Oicana template
3. Pass dynamic input values from your application code
4. Serve the generated PDFs to users
After completing one integration guide, you’ll have a working service that can generate PDFs on demand!
# Introduction
> Oicana offers seamless PDF templating across multiple platforms.
Oicana offers seamless PDF templating across multiple platforms. Define templates using the modern and open source typesetter [Typst](https://typst.app). Then specify dynamic inputs and generate high quality PDFs from any environment - whether it’s a web browser, server application, or desktop software.
## What Oicana offers
[Section titled “What Oicana offers”](#what-oicana-offers)
* **Runs in your infrastructure**: PDFs are generated inside your own application. No data is sent to a third-party service.
* **Multi-platform**: the same template works in the browser, Node.js, C#, Java, Rust, Python, and PHP.
* **Powerful layouting**: templates have all of Typst, including its package ecosystem.
* **Performant**: a warmed up template renders a PDF in single-digit milliseconds.
* **AI and version control ready**: templates are text files. They live next to your code, and AI can help write them.
* **No proprietary format**: templates are plain Typst projects. The Typst compiler is open source.
## The Typst version behind Oicana
[Section titled “The Typst version behind Oicana”](#the-typst-version-behind-oicana)
Oicana compiles templates with a patched Typst 0.15.1. Everything that version of Typst supports, your templates can use: its syntax, its standard library, and any package on [Typst Universe](https://typst.app/universe/) that works with that version. The patch adds support for custom PDF metadata, which is what lets Oicana write the XMP an [e-invoice](/docs/guides/zugferd-factur-x/) needs.
The `oicana` Typst package your templates import (`@preview/oicana:0.2.0`) is versioned separately from the integrations and the CLI.
# CLI Reference
> Oicana CLI reference for packaging, testing, and compiling templates.
[CLI builds are published on GitHub](https://github.com/oicana/oicana/releases/tag/oicana_cli-v0.9.0). You can pick and install the correct binary yourself or let a script do it for you.
Bash script:
Script to install Oicana CLI
```bash
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/oicana/oicana/releases/download/oicana_cli-v0.9.0/oicana_cli-installer.sh | sh
```
Powershell script:
Powershell script to install Oicana CLI
```powershell
powershell -ExecutionPolicy Bypass -c "irm https://github.com/oicana/oicana/releases/download/oicana_cli-v0.9.0/oicana_cli-installer.ps1 | iex"
```
Run `oicana -h` for a list of all commands and options.
## Scaffold a new template
[Section titled “Scaffold a new template”](#scaffold-a-new-template)
`oicana new ` creates a new directory containing a minimal `typst.toml` and `main.typ`. The generated manifest configures the [PDF/A-3b standard](/docs/templates/export/#pdf-export) for PDF exports. The name doubles as the directory name and the Typst package name, so it must be a valid identifier (letters, digits, `_`, `-`; starting with a letter or `_`).
```bash
oicana new invoice
oicana new invoice --version 1.0.0 # default version is 0.1.0
```
The command refuses to overwrite an existing directory. The generated template passes `oicana validate` out of the box and is ready for `oicana pack`.
## Package a template
[Section titled “Package a template”](#package-a-template)
The command `oicana pack` can package an oicana template to be usable in all supported environments. It will bundle everything in a compressed archive.
While packing, all required dependencies will be bundled into the archive. Packages from the `preview` namespace are resolved from the local Typst cache, or downloaded from Typst universe if missing. Local packages are resolved from the local package registry. You can install packages with any namespace by copying them in the correct location (see [the documentation on template dependencies](/docs/templates/dependencies/)).
The pack command will follow symlinks and include copies of the linked files in the template, including links that point outside the template directory. When compiling a template with an integration, a template can only read content from its own archive.
Packed archives have limits to protect resources of the process that later unpacks them. By default, an archive may hold at most 10,000 entries and 512 MiB of decompressed content. `oicana pack` warns when the template it produces exceeds these limits, so you notice before it fails to load in an integration.
The test directory (configured via `tool.oicana.tests`, defaults to `tests/`), the `output/` directory, `.git`, `.DS_Store` and `*.zip` files are always excluded from the packed template. You can extend the exclusions with the [`exclude`](https://github.com/typst/packages/blob/main/docs/manifest.md) field in your `typst.toml` manifest:
typst.toml
```toml
[package]
name = "invoice"
version = "0.1.0"
entrypoint = "main.typ"
exclude = ["docs/*.pdf", "/assets*/"]
```
The patterns use [gitignore semantics](https://git-scm.com/docs/gitignore). Patterns are applied recursively by default; prepend `/` to anchor a pattern to the template root. Defaults are applied first, so a leading `!` re-includes them. For example, `exclude = ["!/tests/"]` packs the test directory anyway. Keep in mind that all files required during compilation need to be packed!
Example commands:
* `oicana pack` - package the template in the current directory
* `oicana pack templates/invoice` - package a specific template
* `oicana pack -a` - package all templates found in the current directory and all child directories
* `oicana pack -o dist` - write the archive to the `dist` directory instead of the current directory
* `oicana pack -n {template}-{version}-release.zip` - use a custom archive name
The `--name` (`-n`) flag supports the following variables:
| Variable | Description |
| ------------ | ---------------------------------- |
| `{template}` | Name of the template |
| `{version}` | Version from the template manifest |
## Testing
[Section titled “Testing”](#testing)
Example commands to test templates:
* `oicana test` - run all tests of the template in the current directory
* `oicana test templates/invoice` - run the tests of the template in the directory `templates/invoice`
* `oicana test -a` - run all tests of all templates found in the current directory and all child directories
* `oicana test --watch` (or `-w`) - run tests once, then re-run affected tests whenever a source file changes
Every test compares the output against a snapshot image and exports the document with the [PDF standards from the template manifest](/docs/templates/export/), so a document that violates its declared standard fails the test.
Learn more about testing Oicana templates in the [testing chapter](/docs/templates/tests/).
## Compilation
[Section titled “Compilation”](#compilation)
For testing purposes, the CLI can compile not-packed Oicana templates. Inputs can be given as relative paths to files.
Example commands to compile templates:
* `oicana compile -f pdf -j invoice=invoice.json -b logo=oicana.png` - compile the template in the current directory to pdf with the given inputs.
* `oicana compile templates/table -j input=templates/table/data.json` - compile the template at `templates/table` to pdf with the given inputs.
* `oicana compile templates/table -j input=templates/table/data.json -o out -n output.pdf` - same as above, but with a custom output directory and output file name. The defaults are `output` and `{template}.{format}` respectively.
* `oicana compile -d` - compile in development mode, which makes development fallback values defined in the template available as inputs.
* `oicana compile -b logo=company.png -m logo=meta.json` - pass metadata for a blob input via a JSON file. Each blob metadata entry (`-m`) must correspond to a blob value (`-b`) with the same key.
* `oicana compile --pdf-standards a-3b` - enforce a PDF standard. Multiple standards can be comma-separated (e.g. `2.0,a-4`). This overrides any standards configured in the template manifest.
The `--name` (`-n`) flag supports the following variables:
| Variable | Description |
| ------------- | -------------------------------------------------------------------------- |
| `{template}` | Name of the template |
| `{version}` | Version from the template manifest |
| `{timestamp}` | Current timestamp in milliseconds |
| `{format}` | File extension for the output format, without the leading dot (e.g. `pdf`) |
## Host fonts
[Section titled “Host fonts”](#host-fonts)
`oicana compile`, `oicana watch`, and `oicana test` can provide fonts to a template the same way an application does at runtime.
* `--font-file ` - add a single font file, repeat the flag for several files
* `--font-path ` - add every `.ttf`, `.ttc`, `.otf`, and `.otc` file in a directory tree, repeat the flag for several directories
Directories are also read from the `OICANA_FONT_PATHS` environment variable, separated like `PATH`. Files that cannot be read or hold no font are skipped with a warning, so pointing at a system font directory is safe.
```bash
oicana compile --font-file fonts/corporate.ttf
oicana watch --font-path fonts
oicana test --font-path /usr/share/fonts
```
See the [host-provided fonts guide](/docs/guides/host-fonts/) for the runtime side.
## Watch mode
[Section titled “Watch mode”](#watch-mode)
The `oicana watch` command works like `oicana compile`, but recompiles the template automatically whenever a source file changes. This includes changes to local Typst packages used by the template.
Example commands:
* `oicana watch` - watch and recompile the template in the current directory
* `oicana watch templates/invoice -j invoice=invoice.json -b logo=oicana.png` - watch a template with inputs
The watch command accepts the same arguments as `oicana compile`.
## Validation
[Section titled “Validation”](#validation)
Example commands:
* `oicana validate` - validate the template in the current directory
* `oicana validate templates/table` - validate the manifest of the table template
* `oicana validate -a` - validate all templates found in the current directory and all child directories
* `oicana validate -a --deny-warnings` - also fail on warnings, for example in CI
If JSON inputs have schemas defined, the `validate` command will make sure that any default or development values are valid according to the schema. This is the only place these fallback values are checked against their schemas; `oicana compile` and the integrations only validate explicit input values, because the fallbacks are loaded from inside Typst while schema validation happens outside. Run `oicana validate` for every template in CI to make sure broken fallbacks do not ship.
## Update
[Section titled “Update”](#update)
`oicana update` checks for a newer release and replaces the current binary if one is found.
This only works when the CLI was installed through one of the installer scripts. If you installed the binary manually, download the new release from GitHub and replace the binary yourself.
# Oicana vs Commercial Services
> How Oicana compares to commercial PDF generation services like DocRaptor and PSPDFKit.
Commercial PDF generation services like DocRaptor, PSPDFKit Document Engine, Anvil, and similar platforms offer managed APIs for creating PDFs. You send a template and data to their API, and they return a PDF.
These services can be a quick way to get started, but they come with trade-offs around cost, data privacy, and control.
## How commercial PDF services work
[Section titled “How commercial PDF services work”](#how-commercial-pdf-services-work)
1. Design a template in the service’s editor or upload your own (HTML, DOCX, or proprietary format)
2. Call the service’s API with your data
3. Receive the generated PDF in the response
## Challenges with commercial PDF services
[Section titled “Challenges with commercial PDF services”](#challenges-with-commercial-pdf-services)
### Data leaves your infrastructure
[Section titled “Data leaves your infrastructure”](#data-leaves-your-infrastructure)
Every PDF generation request sends your data like personal information, financial data, or business-critical content to a third-party server. This can be a compliance issue for regulations like GDPR, HIPAA, or SOC 2.
Oicana runs entirely in your infrastructure. Data never leaves your servers (or your users’ browsers, when using the WASM integration).
### Ongoing costs
[Section titled “Ongoing costs”](#ongoing-costs)
Commercial services typically charge per document or per API call. At scale, these costs add up significantly. A service charging $0.01 per document costs $10,000 for a million documents.
Commercial use of Oicana comes with license costs. The costs are independent from the amount of documents you generate.
### Vendor lock-in
[Section titled “Vendor lock-in”](#vendor-lock-in)
Templates are often stored in a proprietary format or tied to the service’s editor. Migrating away means rebuilding all your templates from scratch.
Oicana templates are standard Typst files. They can easily be ported to other Typst-based tools. The Typst compiler is open source!
### Latency
[Section titled “Latency”](#latency)
Every PDF generation requires a network round-trip to the service’s API. This adds latency, especially for applications that generate PDFs on user interaction.
Oicana generates PDFs locally in milliseconds. With the WASM integration, PDFs can be generated directly in the user’s browser with zero network latency.
### Availability dependency
[Section titled “Availability dependency”](#availability-dependency)
Your PDF generation depends on the service’s uptime. If they have an outage, your application cannot generate PDFs.
Oicana has no external dependencies at runtime.
## When commercial services might still be the right choice
[Section titled “When commercial services might still be the right choice”](#when-commercial-services-might-still-be-the-right-choice)
* Your volume is low enough that per-document pricing is cheaper than developer time
* You need features beyond PDF generation (e.g. e-signatures, form filling, document workflows)
## Comparison at a glance
[Section titled “Comparison at a glance”](#comparison-at-a-glance)
| | **Commercial Services** | **Oicana** |
| --------------- | --------------------------- | ---------------------------- |
| Data privacy | Data sent to third party | Stays in your infrastructure |
| Cost model | Per document / per API call | Fixed cost |
| Latency | Network round-trip | Local |
| Availability | Depends on service uptime | No external dependency |
| Template format | Often proprietary | Typst files |
| Vendor lock-in | High | Little, Typst is open source |
## Next steps
[Section titled “Next steps”](#next-steps)
Keep PDF generation in your own infrastructure: follow the [getting started guide](/docs/getting-started/1-setup/) and take a look at [pricing](/#pricing). Commercial evaluation is free for 30 days.
Looking at a specific service? There are dedicated comparisons for [DocRaptor](/compare/docraptor/), [PDFMonkey](/compare/pdfmonkey/), and [Carbone](/compare/carbone/), and [all comparisons](/compare/) are collected on one page.
# Oicana vs HTML-to-PDF
> How Oicana compares to HTML-to-PDF conversion tools like Puppeteer, wkhtmltopdf, and WeasyPrint.
HTML-to-PDF conversion is one of the most popular approaches for generating PDFs in web applications. Tools like wkhtmltopdf, Puppeteer, WeasyPrint, and Gotenberg render HTML/CSS in a browser engine and can produce a PDF from the result.
While this approach leverages existing web development skills, it comes with significant trade-offs.
## How PDFs get generated from HTML
[Section titled “How PDFs get generated from HTML”](#how-pdfs-get-generated-from-html)
The typical HTML-to-PDF workflow looks like this:
1. Construct an HTML document (often using a templating engine like Handlebars, Jinja2, or Razor)
2. Render it in a headless browser (Chromium, WebKit) or a dedicated rendering engine
3. Export the rendered page as a PDF
## Challenges with HTML-to-PDF
[Section titled “Challenges with HTML-to-PDF”](#challenges-with-html-to-pdf)
### Heavy runtime dependency
[Section titled “Heavy runtime dependency”](#heavy-runtime-dependency)
Most HTML-to-PDF tools require a full browser engine. Chromium alone adds hundreds of megabytes to your deployment. This impacts container sizes, cold start times, and resource consumption.
Oicana’s Typst-based compiler is lightweight. The native libraries are a few megabytes, and the WASM build can run directly in the browser without any server-side dependency.
### Inconsistent layout
[Section titled “Inconsistent layout”](#inconsistent-layout)
CSS was designed for screens, not for paged documents. Getting reliable page breaks, headers, footers, and precise positioning requires fighting against the CSS box model. Different browser versions can produce different results.
Typst was designed for document layout from the ground up. Page breaks, headers, footers, margins, and multi-column layouts work predictably and consistently.
### Performance
[Section titled “Performance”](#performance)
Spinning up a headless browser, loading HTML, and rendering to PDF typically takes hundreds of milliseconds to seconds per document. Under load, browser instances compete for memory and CPU.
Oicana compiles templates to PDF in single digit milliseconds. No browser process, no DOM rendering, no waiting for fonts to load.
### Security concerns
[Section titled “Security concerns”](#security-concerns)
Running a full browser engine to process potentially untrusted content introduces a large attack surface. Headless Chrome has had numerous security vulnerabilities, and sandboxing it properly in a server environment is non-trivial.
Oicana templates are compiled by Typst, which has a minimal attack surface and does not execute arbitrary code.
## When HTML-to-PDF might still be the right choice
[Section titled “When HTML-to-PDF might still be the right choice”](#when-html-to-pdf-might-still-be-the-right-choice)
* You already have complex HTML templates and the migration cost is too high
* You need to render content that is inherently web-based (e.g. screenshots of dashboards)
* Your team has deep CSS/HTML expertise but no capacity to learn a new tool
## Comparison at a glance
[Section titled “Comparison at a glance”](#comparison-at-a-glance)
| | **HTML-to-PDF** | **Oicana** |
| ----------------- | ------------------------ | ------------------------ |
| Runtime size | 100+ MB (browser engine) | \~15 MB (native library) |
| Compilation speed | 100ms up to seconds | milliseconds |
| Layout model | CSS | Typst styling |
| Page breaks | Fragile | Built-in, predictable |
| Headers/Footers | Limited | First-class support |
| Template language | HTML + templating engine | Typst markup |
## Next steps
[Section titled “Next steps”](#next-steps)
Ready to drop the headless browser? Follow the [getting started guide](/docs/getting-started/1-setup/) to generate your first PDF in minutes. Commercial evaluation is free for 30 days, see [pricing](/#pricing).
Looking at a specific tool? There is a dedicated [Gotenberg comparison](/compare/gotenberg/), and [all comparisons](/compare/) are collected on one page.
# Oicana vs LaTeX
> How Oicana compares to LaTeX for application PDF generation.
LaTeX is a long-established typesetting system widely used in academia and publishing. Some applications use LaTeX (or derived engines like XeTeX, LuaLaTeX) to generate PDFs by compiling `.tex` files with injected data.
Oicana uses [Typst](https://typst.app), a modern typesetting system that shares many of LaTeX’s goals but takes a fundamentally different approach to syntax, compilation, and developer experience.
## How LaTeX-based PDF generation works
[Section titled “How LaTeX-based PDF generation works”](#how-latex-based-pdf-generation-works)
1. Create a `.tex` template with placeholders (often using a text templating engine)
2. Inject data into the template by replacing placeholders
3. Run the LaTeX compiler (`pdflatex`, `xelatex`, or `lualatex`) to produce a PDF
## Challenges with LaTeX for application PDF generation
[Section titled “Challenges with LaTeX for application PDF generation”](#challenges-with-latex-for-application-pdf-generation)
### Installation size
[Section titled “Installation size”](#installation-size)
A typical LaTeX distribution (TeX Live) is 4–7 GB. Even minimal installations are hundreds of megabytes. This makes LaTeX impractical for containerized deployments, serverless functions, or client-side generation.
The Typst compiler (wrapped by Oicana) is a few megabytes. The WASM build runs directly in the browser.
### Compilation speed
[Section titled “Compilation speed”](#compilation-speed)
LaTeX compilation is slow. It often takes several seconds per document. This can be acceptable for academic papers but problematic for on-demand PDF generation in applications.
Typst, and thus Oicana, can compile templates in single digit milliseconds.
### Steep learning curve
[Section titled “Steep learning curve”](#steep-learning-curve)
LaTeX’s syntax is notoriously difficult to learn. Error messages are often cryptic, and debugging layout issues requires deep knowledge of the TeX engine.
Typst has a modern, readable syntax and produces clear error messages. Developers who have never used a typesetting system can be productive in very short time frames.
### Escaping and injection
[Section titled “Escaping and injection”](#escaping-and-injection)
Injecting dynamic data into LaTeX templates is risky. LaTeX has many special characters (`\`, ``{`, `}``, `%`, `$`, `&`, `#`, `_`, `^`, `~`) that need careful escaping. Improper escaping can break compilation or lead to unexpected output.
Oicana templates define explicit typed inputs. Data is passed through a structured API, not through string interpolation, eliminating injection issues entirely.
## When LaTeX might still be the right choice
[Section titled “When LaTeX might still be the right choice”](#when-latex-might-still-be-the-right-choice)
* Your team already has deep LaTeX expertise and a large library of existing templates
* You need very specific typographic features that LaTeX’s ecosystem uniquely provides
## Comparison at a glance
[Section titled “Comparison at a glance”](#comparison-at-a-glance)
| | **LaTeX** | **Oicana** |
| ----------------- | --------------------------- | ------------------------ |
| Install size | 4–7 GB (TeX Live) | \~15 MB (native library) |
| Compilation speed | seconds | milliseconds |
| Syntax | Backslash commands, complex | Markup, modern |
| Error messages | Cryptic | Clear and actionable |
| Data injection | String replacement | Typed inputs |
| Browser support | Difficult | Yes |
| Package ecosystem | Vast (CTAN) | Growing (Typst Universe) |
## Next steps
[Section titled “Next steps”](#next-steps)
Get LaTeX-quality output without the TeX toolchain: follow the [getting started guide](/docs/getting-started/1-setup/) to build your first template. Commercial evaluation is free for 30 days, see [pricing](/#pricing).
# Oicana vs PDF Libraries
> How Oicana compares to PDF libraries like iText, PDFKit, FPDF, and ReportLab.
PDF libraries let you construct PDF documents programmatically by calling API methods to draw text, shapes, and images on a page. Popular libraries include iText (Java/.NET), PDFKit (Node.js), FPDF/TCPDF (PHP), ReportLab (Python), and Apache PDFBox (Java).
This approach gives you full control over every pixel, but comes at a cost.
## How PDF libraries work
[Section titled “How PDF libraries work”](#how-pdf-libraries-work)
With a PDF library, you typically write code like this:
1. Create a document object
2. Add pages and set dimensions
3. Position text, images, tables, and shapes using coordinates or a layout API
4. Write the resulting bytes to a file or response stream
The template *is* the code. There is no separate template file.
## Challenges with PDF libraries
[Section titled “Challenges with PDF libraries”](#challenges-with-pdf-libraries)
### Templates live in code
[Section titled “Templates live in code”](#templates-live-in-code)
Since the layout is defined in application code, changing a template means changing, testing, and redeploying your application. Designers and non-developers cannot edit templates without developer involvement.
With Oicana, templates are standalone Typst files. They can be edited, previewed, and tested independently of the application.
### Language lock-in
[Section titled “Language lock-in”](#language-lock-in)
An iText template written in Java cannot be reused in a Node.js service. If your organization uses multiple languages, you end up maintaining separate PDF generation code for each stack.
Oicana templates are language-agnostic. The same template works with the Java, C#, Node.js, Rust, Python, PHP, and browser integrations.
### Tedious layout work
[Section titled “Tedious layout work”](#tedious-layout-work)
Positioning elements with coordinates or building table layouts through API calls is time-consuming and error-prone. Simple changes like adjusting spacing often require recompiling and inspecting the output.
Typst provides a high-level markup language with good layout control, reusable components, and a package ecosystem. You can build and reuse different layouts or rely on existing ones.
### Limited previewing
[Section titled “Limited previewing”](#limited-previewing)
Most PDF libraries require you to run your code to see the output. There is no live preview during development, which slows down the design iteration cycle.
Oicana templates can be previewed in any Typst editor, giving you instant feedback.
## When PDF libraries might still be the right choice
[Section titled “When PDF libraries might still be the right choice”](#when-pdf-libraries-might-still-be-the-right-choice)
You might need them if you have to manipulate existing PDFs (merge, split, annotate, fill forms). There are other options though and you could still use a Typst based tool for the document generation.
## Comparison at a glance
[Section titled “Comparison at a glance”](#comparison-at-a-glance)
| | **PDF Libraries** | **Oicana** |
| --------------------- | ----------------------- | ------------------ |
| Template format | Application code | Typst markup files |
| Editable by designers | No | Yes |
| Cross-language reuse | No | Yes |
| Live preview | No | Yes |
| Layout approach | Coordinates / API calls | Declarative markup |
| Learning curve | Library API + PDF spec | Typst markup |
## Next steps
[Section titled “Next steps”](#next-steps)
Move layout code out of your application: follow the [getting started guide](/docs/getting-started/1-setup/) to build your first template. Commercial evaluation is free for 30 days, see [pricing](/#pricing).
# Credits and Acknowledgments
> Acknowledgments for the projects and tools that make Oicana possible.
Oicana stands on the shoulders of giants.
A list of dependencies of the Oicana project with their licenses [can be found in the repository](https://github.com/oicana/oicana/blob/main/NOTICE).
## Typst
[Section titled “Typst”](#typst)
The modern and open source typesetter that made Oicana possible.
Oicana is not affiliated with the company behind Typst, that said, they are constantly improving the project and deserve support for that! If you would like to use their official Typst editor, but keep all data on your own infrastructure, [there is an option to run it on-premises](https://typst.app/pricing/).
## Typst community projects
[Section titled “Typst community projects”](#typst-community-projects)
The development of Oicana and the creation of Oicana templates uses several open source tools from the Typst community. A few of them are:
* [Typship](https://github.com/jassielof/typship) - for local development of Typst packages.
* [Typstyle](https://github.com/Enter-tainer/typstyle) - formatting of Typst files.
* [Tytanic](https://github.com/typst-community/tytanic) - snapshot tests for Typst packages.
# Guides
> Concrete problems and suggestions for solving them in Oicana templates.
Some concrete problems and suggestions for solving them in Oicana templates.
[Cache Management](/docs/guides/cache-management/)Understanding and configuring Oicana's template compilation cache.
[Async Node.js Compilation](/docs/guides/nodejs-async/)Run compilation and export off the Node.js event loop and size the libuv thread pool.
[Styled Inputs](/docs/guides/styled-inputs/)How to support styled and rich-text inputs in Oicana templates.
[Host-provided Fonts](/docs/guides/host-fonts/)Registering fonts in your application so that the templates it loads can use them.
[ZUGFeRD and Factur-X e-invoices](/docs/guides/zugferd-factur-x/)Creating e-invoices with the ZUGFeRD and Factur-X standards.
[Deploying the Browser WASM](/docs/guides/browser-deployment/)Production checklist for the browser integration: pre-compression, CDN caveats, Web Worker offload.
# Deploying the Browser WASM
> Production checklist for shipping the Oicana browser integration covering pre-compression, MIME types, CDN caveats, and Web Worker offload.
The browser integration ships a single \~40 MB WebAssembly module as part of `@oicana/browser-wasm`. It is fetched once and lives in the browser cache afterwards, so the goal in production is to make that first fetch fast and let the runtime use it efficiently. This guide covers what to set up beyond the Vite-based getting-started chapter.
## Pre-compress with brotli
[Section titled “Pre-compress with brotli”](#pre-compress-with-brotli)
The WASM file compresses very well. It comes in at about 40 MB uncompressed, \~17 MB gzip, \~12 MB brotli. But several popular CDNs cap on-the-fly compression at around 10 MB and silently serve the uncompressed file when an asset crosses that limit.
The fix is to pre-compress at build time and serve the `.wasm.br` (and `.wasm.gz`) file directly. Most static-asset hosts pick up compressed siblings automatically when they exist, and almost all bundlers can produce them in one step:
* **Vite**: add [`vite-plugin-compression2`](https://www.npmjs.com/package/vite-plugin-compression2) and list both algorithms.
* **Webpack / Next.js**: [`compression-webpack-plugin`](https://www.npmjs.com/package/compression-webpack-plugin) with two instances (one per algorithm).
* **Self-hosted nginx**: `brotli_static on; gzip_static on;` and ship `.wasm.br` / `.wasm.gz` next to `.wasm` in your build output.
Compression plugins ship a default file filter built for text assets, and `.wasm` is not in it. E.g. for vite:
vite.config.ts
```ts
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import { compression } from 'vite-plugin-compression2';
export default defineConfig({
plugins: [
react(),
compression({
include: /\.(js|mjs|json|css|html|wasm)$/i,
algorithms: ['gzip', 'brotliCompress'],
}),
],
});
```
Brotli at its default quality is slow on a file this size: expect the production build to take over a minute longer.
## Loading the WASM with non-Vite bundlers
[Section titled “Loading the WASM with non-Vite bundlers”](#loading-the-wasm-with-non-vite-bundlers)
The getting-started chapter uses Vite’s `?url` suffix to import the module as an asset URL:
```ts
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
```
That syntax is Vite-specific. Other bundlers don’t recognize `?url` and fail the build. In webpack 5, use the standard `import.meta.url` form instead:
```ts
const wasmUrl = new URL(
'@oicana/browser-wasm/oicana_browser_wasm_bg.wasm',
import.meta.url,
).href;
await initialize(wasmUrl);
```
webpack 5 emits the WASM as a separate asset and rewrites the URL to point at it.
For other bundlers, copy `node_modules/@oicana/browser-wasm/oicana_browser_wasm_bg.wasm` into the directory your app serves static files from as part of the build, and pass its public URL to `initialize()`:
```ts
await initialize('/oicana_browser_wasm_bg.wasm');
```
## Serve `Content-Type: application/wasm`
[Section titled “Serve Content-Type: application/wasm”](#serve-content-type-applicationwasm)
The browser will use the streaming `WebAssembly.instantiateStreaming` path only when the response is served as `application/wasm`. Some hosts default to `application/octet-stream`, which forces a slower fallback that downloads the full module before instantiation can start.
Static-asset hosts usually map the extension correctly. If you’re behind a custom server or proxy, double-check the response headers in the browser’s network tab:
```plaintext
Content-Type: application/wasm
Content-Encoding: br # if you're pre-compressing
Cache-Control: public, max-age=31536000, immutable
```
The `immutable` cache hint is safe when your bundler emits the WASM as a content-hashed asset (Vite’s `?url` and webpack’s `import.meta.url` form do this) so that the URL changes every time the file changes. The package itself ships the file under a fixed name, so if you copy it or serve it directly from `node_modules`, skip `immutable` or you risk pinning a stale module for up to a year.
## Show a loading state on first visit
[Section titled “Show a loading state on first visit”](#show-a-loading-state-on-first-visit)
Even with brotli and a fast CDN, the first visit will spend a couple of seconds downloading and instantiating the module on slower connections. Render a skeleton or “preparing PDF engine…” placeholder until your `await initialize(wasmUrl)` resolves; subsequent visits are typically instant because the browser cache absorbs the cost.
A common pattern is to call `initialize()` early at app boot rather than on the first user interaction so the module is warmed up by the time someone clicks the button that triggers compilation.
## Move compilation to a Web Worker
[Section titled “Move compilation to a Web Worker”](#move-compilation-to-a-web-worker)
PDF compilation is CPU-bound and runs synchronously inside the WASM module. On the main thread that means the UI freezes for the duration of `template.export(...)`. That can be OK for a quick test, but is painful for a real app.
The fix is the standard one: instantiate `@oicana/browser` inside a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) and post messages from the UI thread to request compilations. The worker keeps the same `Template` instance alive across calls, so the WASM module is initialized once and the per-request cost is just the message round-trip plus the actual compile.
Note
The [open source React example](https://github.com/oicana/oicana-example-typescript-react/) ships a complete worker setup, including the compile-request message protocol and the bundler config to ship the worker as a separate chunk. It’s the fastest way to get a production-shaped baseline.
# Cache Management
> Understanding and configuring Oicana's template compilation cache.
Typst uses [comemo](https://github.com/typst/comemo), a memoized function cache. This significantly speeds up repeated compilations.
## How Cache Eviction Works
[Section titled “How Cache Eviction Works”](#how-cache-eviction-works)
The comemo cache is global and shared across all template instances. To prevent unbounded memory growth, Oicana provides configurable cache eviction based on an aging mechanism:
* Each cache entry has an age counter
* Age increases by 1 during each eviction call
* Age resets to 0 when the entry is accessed (during a template compilation)
* Entries with age ≥ max\_age are removed when running cache eviction
## Default Behavior
[Section titled “Default Behavior”](#default-behavior)
By default, Oicana integrations automatically evict the cache after each compilation with a maximum age of 10.
## Configuring Cache Eviction
[Section titled “Configuring Cache Eviction”](#configuring-cache-eviction)
All integrations provide two APIs for cache management: one to configure or disable automatic eviction after each compilation, and one to manually trigger cache eviction with a specific age threshold.
| Integration | Configure automatic eviction | Manual eviction |
| ----------------- | ----------------------------------------------------------- | ------------------------------------ |
| Browser / Node.js | `configureAutomaticCacheEviction(maxAge)` | `evictCache(maxAge)` |
| C# | `Configuration.ConfigureAutomaticCacheEviction(maxAge)` | `Configuration.EvictCache(maxAge)` |
| Java | `Configuration.configureAutomaticCacheEviction(maxAge)` | `Configuration.evictCache(maxAge)` |
| PHP | `Configuration::configureAutomaticCacheEviction($maxAge)` | `Configuration::evictCache($maxAge)` |
| Python | `oicana.configure_automatic_cache_eviction(max_age)` | `oicana.evict_cache(max_age)` |
| Rust | `oicana::configure_automatic_cache_eviction(Some(max_age))` | `oicana::evict_cache(max_age)` |
`maxAge` is the age threshold described above: `0` clears the whole cache after every compilation, `n` keeps entries used within the last `n` evictions. The default is 10.
Consider adjusting the default cache settings to a higher maximum entry age if you have a large number of templates and enough available memory to support a larger cache.
## Disabling automatic eviction
[Section titled “Disabling automatic eviction”](#disabling-automatic-eviction)
You can also turn automatic eviction off. How exactly depends on the language:
| Integration | Disable automatic eviction |
| ----------------- | ------------------------------------------------------ |
| Browser / Node.js | `configureAutomaticCacheEviction(null)` |
| C# | `Configuration.ConfigureAutomaticCacheEviction(null)` |
| Java | `Configuration.disableAutomaticCacheEviction()` |
| PHP | `Configuration::configureAutomaticCacheEviction(null)` |
| Python | `oicana.configure_automatic_cache_eviction(None)` |
| Rust | `oicana::configure_automatic_cache_eviction(None)` |
The cache is then never cleared until you call the manual eviction API.
Sometimes, it makes sense to disable automatic cache eviction and only run it manually. For example, during large batch compilations, you can disable automatic eviction and only evict between batches for better performance. The default should work well in most scenarios, but you can experiment with different approaches for fine-tuning.
# Host-provided Fonts
> Registering fonts in your application so that templates can use them.
Instead of packing a font into every template that needs it, your application can register the font once. Every template it loads afterwards can use it. The [fonts page](/docs/templates/fonts/) compares this with packed fonts and explains how a template declares the families it expects. This guide is the host side.
Register fonts before templates
Outside of Rust, the font registry is process-global, and a template takes a snapshot of it when it is registered. Fonts registered later do not reach templates that already exist. Register your fonts during application startup, before loading any template.
Nothing deduplicates fonts. Registering the same file twice adds its faces twice, so register each font once per process.
## Registering fonts
[Section titled “Registering fonts”](#registering-fonts)
All integrations can register fonts from raw bytes. Everywhere except the browser, fonts can also be registered by path: either a font file, or a directory that adds every `.ttf`, `.ttc`, `.otf`, and `.otc` file in its tree. Both return the number of font faces that were added; data that holds no font Typst can read is ignored and counts as zero, and so does a path that does not exist. Rust works differently and is described in its tab.
* TS (Browser)
```typescript
import { initialize, registerFonts, Template } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
await initialize(wasmUrl);
const response = await fetch('/fonts/corporate.ttf');
const font = new Uint8Array(await response.arrayBuffer());
registerFonts([font]);
const templateResponse = await fetch('/template.zip');
const template = new Template(
new Uint8Array(await templateResponse.arrayBuffer()),
);
```
There is no path-based variant in the browser, since there is no filesystem to read fonts from.
* C#
```csharp
using Oicana;
Configuration.RegisterFontPaths("fonts/corporate.ttf");
// another font, from memory
Configuration.RegisterFonts(File.ReadAllBytes("fonts/corporate-bold.ttf"));
var template = new Template(File.ReadAllBytes("template.zip"));
```
Both methods take `params`, so you can pass several fonts in one call.
* Java
```java
import com.oicana.Configuration;
import com.oicana.Template;
import java.nio.file.Files;
import java.nio.file.Path;
Configuration.registerFontPaths("fonts/corporate.ttf");
// another font, from memory
Configuration.registerFonts(Files.readAllBytes(Path.of("fonts/corporate-bold.ttf")));
try (var template = new Template(Files.readAllBytes(Path.of("template.zip")))) {
// ...
}
```
Both methods are varargs, so you can pass several fonts in one call.
* TS (Node.js)
```typescript
import { readFile } from 'node:fs/promises';
import { registerFontPaths, registerFonts, Template } from '@oicana/node';
registerFontPaths(['fonts/corporate.ttf']);
// another font, from memory
registerFonts([await readFile('fonts/corporate-bold.ttf')]);
const template = new Template(await readFile('template.zip'));
```
* PHP
```php
use Oicana\Configuration;
use Oicana\Template;
Configuration::registerFontPaths(['fonts/corporate.ttf']);
// another font, from memory
Configuration::registerFonts([file_get_contents('fonts/corporate-bold.ttf')]);
$template = new Template(file_get_contents('template.zip'));
```
* Python
```python
from pathlib import Path
from oicana import Template, register_font_paths, register_fonts
register_font_paths("fonts/corporate.ttf")
# another font, from memory
register_fonts(Path("fonts/corporate-bold.ttf").read_bytes())
with Template(Path("template.zip").read_bytes()) as template:
...
```
Both functions accept a single value or any iterable of values.
* Rust
```rust
use std::fs::File;
use oicana::Template;
use oicana::fonts::{font_files_at, FontSource};
let fonts: Vec = font_files_at("fonts")
.iter()
.filter_map(FontSource::from_path)
.collect();
let template_file = File::open("template.zip")?;
let mut template = Template::init_with_fonts(template_file, &fonts)?;
```
The Rust integration has no global font registry. Fonts are passed to the template that should use them, so create each `FontSource` once and share it with every template that needs it. `FontSource::from_path` takes a single file; `font_files_at` expands a file or a directory into the files to read, which is what the other integrations do for you. `FontSource::from_bytes` builds a source from data you already hold.
## Memory
[Section titled “Memory”](#memory)
Fonts are registered per process and shared by every template, so a large font costs memory once instead of once per template.
## Inspecting and clearing the registry
[Section titled “Inspecting and clearing the registry”](#inspecting-and-clearing-the-registry)
Every integration except Rust can list what is currently registered and drop it again. Each entry has a `family`, and a `path` for fonts registered by path.
| Integration | List registered fonts | Clear the registry |
| ----------------- | ---------------------------------- | ----------------------------- |
| Browser / Node.js | `registeredFonts()` | `clearFonts()` |
| C# | `Configuration.RegisteredFonts()` | `Configuration.ClearFonts()` |
| Java | `Configuration.registeredFonts()` | `Configuration.clearFonts()` |
| PHP | `Configuration::registeredFonts()` | `Configuration::clearFonts()` |
| Python | `oicana.registered_fonts()` | `oicana.clear_fonts()` |
Clearing only affects templates registered afterwards. Templates that already exist keep the fonts they were created with.
The `family` values are the names to use in Typst’s `text(font: ...)`. Listing them after startup is a quick way to check that a font file provides the family you expected.
# Async Node.js Compilation
> Run compilation and export off the Node.js event loop and size the libuv thread pool.
A warmed up template produces a PDF in single digit milliseconds, but compilation and export are CPU-bound work. The synchronous methods (`export`, `compile`, and their format variants) run that work on the main thread and block the Node.js event loop until the document is ready. Under heavy load, or for a single slow document, the blocked event loop stalls every other request in the meantime.
## Async methods
[Section titled “Async methods”](#async-methods)
Every compilation and export method has an `Async` counterpart that runs the work on a background thread from Node.js’ libuv thread pool and returns a `Promise`. The event loop stays free while the document is generated, so your service keeps serving other requests:
```typescript
const jsonInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
const pdf = await template.exportPdfAsync(jsonInputs, new Map());
```
The async variants take the same parameters as their synchronous counterparts; only the return type changes to a `Promise` of the bytes (or, for `compileAsync`, of the compiled document):
* `Template.exportAsync` / `exportPdfAsync` / `exportPngAsync` / `exportSvgAsync` compile and export in a single call, then free the document.
* `Template.compileAsync` returns a `CompiledDocument` you can export more than once.
* `CompiledDocument.exportAsync` / `exportPdfAsync` / `exportPngAsync` / `exportSvgAsync` export an already compiled document.
Compile once and export several times when you need multiple formats or page ranges from the same inputs:
```typescript
using document = await template.compileAsync(jsonInputs, new Map());
const pdf = await document.exportPdfAsync();
const thumbnail = await document.exportPngAsync(2.0);
```
`using` is TypeScript syntax here, and `tsc` compiles it down to a `try`/`finally` for you. It needs `ESNext.Disposable` in your `tsconfig.json`’s `lib`. Plain JavaScript only understands `using` from Node 24 on, so outside a TypeScript build call `document.dispose()` in a `finally` block instead:
```javascript
const document = await template.compileAsync(jsonInputs, new Map());
try {
const pdf = await document.exportPdfAsync();
} finally {
document.dispose();
}
```
## Registering templates
[Section titled “Registering templates”](#registering-templates)
The `Template` constructor reads the template and runs its warm-up compilation on the main thread. `Template.create` does the same work on the thread pool and resolves to the ready template. It takes the same parameters as the constructor:
```typescript
const template = await Template.create(templateBytes);
```
For templates you only render once, `Template.exportOnceAsync` is the async counterpart of `exportOnce`. It compiles and exports without registering anything, and resolves to the document bytes and its warnings:
```typescript
const { document, warnings } = await Template.exportOnceAsync(templateBytes, jsonInputs);
```
## Configuring the thread pool
[Section titled “Configuring the thread pool”](#configuring-the-thread-pool)
The libuv thread pool defaults to four threads, shared with Node.js’ own file system, DNS, and crypto work. Each in-flight async compilation or export occupies one thread for its duration.
If you generate many documents concurrently, raise the pool size with the `UV_THREADPOOL_SIZE` environment variable:
```bash
UV_THREADPOOL_SIZE=8 node dist/main.js
```
Caution
The pool is created the first time it is used, and its size is fixed from then on. Set `UV_THREADPOOL_SIZE` in the environment before the process starts.
Because the work is CPU-bound, there is little benefit to sizing the pool much beyond the number of CPU cores available to your service. Extra threads just compete for the same cores. Start at the core count and measure under your real load.
Bun
Bun does not run on libuv; it implements Node-API on its own thread pool. The async methods still run off the main thread and keep the event loop free, but `UV_THREADPOOL_SIZE` has no effect there. Bun sizes its pool itself.
## Concurrency
[Section titled “Concurrency”](#concurrency)
Async calls are safe to run concurrently. Independent compilations and exports proceed in parallel across the pool, up to the thread count, and share the compilation cache. Fire several off with `Promise.all` when a request needs more than one document:
```typescript
const [invoice, receipt] = await Promise.all([
invoiceTemplate.exportPdfAsync(invoiceInputs, new Map()),
receiptTemplate.exportPdfAsync(receiptInputs, new Map()),
]);
```
## When to still reach for worker threads
[Section titled “When to still reach for worker threads”](#when-to-still-reach-for-worker-threads)
With the async API you rarely need worker threads. If you want to isolate compilation in a separate thread pool, or keep it off the pool your other libuv work depends on, a library like [piscina](https://github.com/piscinajs/piscina) can help
# Styled Inputs
> How to support styled and rich-text inputs in Oicana templates.
Document templates in applications often need to be customizable. For example, a user might want to customize the footer of a given document. A common requirement is that these customizations need to be styled. In the footer, the users might want multiple blocks of text in a grid and some bold or underlined sections. We can support this by passing Typst code into the template instead of plain text and using [`#eval`](https://typst.app/docs/reference/foundations/eval/) in the template to render the given Typst code as the footer.
## A styled footer
[Section titled “A styled footer”](#a-styled-footer)
Declare a regular JSON input for the customization and evaluate its Typst code in markup mode:
Part of typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "footer"
required = false
```
Part of main.typ
```typst
#let styled(code) = eval(code, mode: "markup")
#set page(footer: context {
if input.footer != none {
styled(input.footer.typst)
} else {
align(center)[#counter(page).display()]
}
})
```
The caller passes Typst markup as an ordinary JSON value:
```json
{ "typst": "*Acme Inc.* #h(1fr) #link(\"https://acme.example\")[acme.example]" }
```
`mode: "markup"` is what makes this work like a document body: the string is interpreted the way template markup is, so `*bold*`, `#link(...)`, grids, and tables all behave as they do in a `.typ` file.
Only evaluate Typst you trust
`#eval` compiles the string as Typst source with the full standard library available. Whoever controls that input controls what the compilation does, so treat it exactly like the code in your template. `#eval`’s `scope` parameter only *adds* bindings; it does not take the standard library away.
* **Everything packed into the template is readable.** `#read` is available inside the evaluated code, and reads resolve against the template archive. A supplied `#read("config.toml")` renders that file into the document. A read cannot escape the archive (e.g.`#read("/etc/passwd")` fails), but `oicana pack` bundles the whole template directory by default. Keep credentials, notes, and scratch files out of the archive with [the manifest’s `exclude` field](/docs/cli/#package-a-template).
* **There is no compute budget.** A supplied string can loop and keep a worker busy. If the input can come from an end user, treat the compilation like any other untrusted workload.
## Editing without knowing Typst
[Section titled “Editing without knowing Typst”](#editing-without-knowing-typst)
If our users know Typst, we are done at this point. But users often don’t know Typst and might not be technical at all. In these cases, the input used to customize the template should likely be a WYSIWYG editor.
Currently, there is no production-ready WYSIWYG editor for Typst (though keep an eye on ). A well supported format for WYSIWYG editors is HTML. You can use a tool like pandoc to convert from the format your editor exports to Typst and then pass the generated Typst code into the template.
# ZUGFeRD and Factur-X e-invoices
> How to create a template for e-invoices and use it from Oicana integrations.
A ZUGFeRD / Factur-X e-invoice is a regular PDF with the structured invoice data embedded as XML, plus XMP metadata declaring which profile that XML follows. Oicana produces all three parts from a Typst template. You supply the invoice XML, and Oicana renders the PDF, embeds the XML, and writes the metadata.
This guide builds the smallest setup that still produces a valid e-invoice, so you have a working baseline to iterate on. The visible PDF is almost empty on purpose. The focus is the plumbing: exporting the right PDF standards, embedding the XML, and passing data from an integration.
Background reading
For the *why* behind ZUGFeRD / Factur-X, read the post [E-invoices with ZUGFeRD and Factur-X in Oicana](/news/2026-06-30-zugferd-factur-x-e-invoices/). The [`invoice_zugferd` example template](https://github.com/oicana/oicana-example-templates/tree/main/templates/invoice_zugferd) is a more complete version of the Oicana template we build here.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
This guide uses the [`oicana` CLI](/docs/cli/) to compile and pack the template, so [install it](/docs/cli/) first.
The file embedding and custom metadata is wrapped in the open source [`invoice-harness`](https://github.com/oicana/invoice-harness) package, which gives you a single `factur-x(...)` call. `invoice-harness` is not on the Typst package registry yet, so install it as a [local package](/docs/templates/dependencies/#local-packages) following [the instructions in its repository](https://github.com/oicana/invoice-harness). After that, `@local/invoice-harness:0.1.1` is importable from your template.
## The invoice XML
[Section titled “The invoice XML”](#the-invoice-xml)
A valid e-invoice needs a Cross Industry Invoice (CII) XML document that conforms to the profile you declare. Hand-writing one is error prone, so start from an official reference sample.
The `invoice-harness` repository ships one validated sample per profile under [`tests/invoices//factur-x.xml`](https://github.com/oicana/invoice-harness/tree/main/tests/invoices), taken from the [ZUGFeRD corpus](https://github.com/ZUGFeRD/corpus). We use the **EN 16931** profile, the European baseline most national mandates build on. Copy that sample into your template directory:
```plaintext
tests/invoices/en16931/factur-x.xml -> factur-x.xml
```
Profile and XML must match
The profile you pass in the template (`profiles.en16931`) has to match the profile the XML actually conforms to. Embedding an EN 16931 XML but declaring `MINIMUM` produces a file that validators reject.
## The manifest
[Section titled “The manifest”](#the-manifest)
The manifest wires up two things: the PDF standards and the inputs.
typst.toml
```toml
[package]
name = "minimal_e_invoice"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
[tool.oicana.export.pdf]
standards = ["ua-1", "a-3b"]
# Structured invoice fields drive the visible PDF.
[[tool.oicana.inputs]]
type = "json"
key = "invoice"
development = "invoice.json"
# The CII document that gets embedded into the PDF.
[[tool.oicana.inputs]]
type = "blob"
key = "zugferd"
required = false
development = { file = "factur-x.xml" }
```
The choices behind this manifest:
* **`standards = ["ua-1", "a-3b"]`**: `a-3b` is the archivable PDF/A profile that allows embedded files, which makes the file a valid e-invoice. Adding `ua-1` makes the same document [accessible (PDF/UA-1)](/news/2026-06-17-accessible-pdfs-pdf-ua/). Both build on PDF 1.7, so they combine. See [Export Formats](/docs/templates/export/#combining-standards).
* **`zugferd` is an optional `blob` input** with a `development` value. The sample XML lets the template compile on its own during development, while in production an integration passes a fresh XML per invoice. `required = false` means a missing XML is not an error: the template just skips the embedding and renders a plain PDF.
* **`invoice` is a `json` input** with a `development` value, so the editor preview has data to show. It feeds the human-readable side of the PDF.
Create the `invoice.json` development value next to the manifest:
invoice.json
```json
{
"id": "2026-0001",
"customer": "ACME Corp",
"total": "1190.00 EUR"
}
```
## The template
[Section titled “The template”](#the-template)
The template stays deliberately bare. It embeds the XML with one call and renders just enough to be a recognizable document.
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#import "@local/invoice-harness:0.1.1": *
#let read-project-file(path) = read(path, encoding: none)
#let (input, _, _) = setup(read-project-file)
#set document(title: "Invoice " + input.invoice.id, date: datetime.today())
#if input.zugferd != none {
factur-x(input.zugferd.bytes, profiles.en16931)
}
= Invoice #input.invoice.id
Billed to #input.invoice.customer.
*Total: #input.invoice.total*
```
What each part does:
* `factur-x(input.zugferd.bytes, profiles.en16931)` takes the bytes of the `zugferd` blob input, embeds them as the associated `factur-x.xml`, and declares the EN 16931 profile in the XMP. That call is the whole e-invoice machinery. It is guarded by `if input.zugferd != none` so the template still produces a plain PDF when no XML is passed.
* `set document(title: ...)` is required for PDF/UA-1. If you add images later, give each one `alt` text for the same reason.
* Everything below is ordinary Typst. Grow it into a real invoice layout at your own pace.
The visible PDF and the XML are independent here
In this minimal setup the visible text comes from the `invoice` JSON input while the embedded data is a fixed sample XML, so they do not describe the same invoice. That is fine for getting the pipeline working, but a real e-invoice is only correct when the human-readable PDF and the embedded XML match. The next step is to **generate the XML from the same data** you render, so changing one changes both.
## Preview and compile locally
[Section titled “Preview and compile locally”](#preview-and-compile-locally)
With the `oicana` CLI in the template directory:
```bash
oicana validate # check the manifest and that fallbacks fit their schemas
oicana compile --development # render a PDF into ./output using the development values
oicana pack # produce minimal_e_invoice-0.1.0.zip
```
## Validate the result
[Section titled “Validate the result”](#validate-the-result)
Let’s make sure the PDF created by `oicana compile --development` is a valid e-invoice.
Three things have to line up: PDF/A-3 conformance, the embedded XML, and the XMP metadata. A mistake in any of them makes a file that systems could reject, so validate every change with a tool. Here some options:
* **[portinvoice.com](https://www.portinvoice.com/)**: a free, vendor-neutral online validator. Upload the PDF and it checks the embedded XML against EN 16931 and reports the detected profile. Convenient while iterating.
* **[KoSIT validator](https://github.com/itplr-kosit/validator)**: the official German government reference validator. Run it locally when you need the authoritative verdict.
* **[Mustangproject](https://www.mustangproject.org/)**: open source, runs offline, and validates both the PDF/A side (via veraPDF) and the CII schema plus EN 16931 Schematron. This can be a good choice to run in CI.
* **[veraPDF](https://verapdf.org/)**: the industry-standard PDF/A validator if you only want to check the archival conformance of the PDF.
## Pass data from an integration
[Section titled “Pass data from an integration”](#pass-data-from-an-integration)
In an application you register the packed template once, then per request pass the invoice fields as the `invoice` JSON input and the XML bytes as the `zugferd` blob input.
* TS (Browser)
```typescript
import { initialize, Template, type BlobInput } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
await initialize(wasmUrl);
const templateResponse = await fetch('/minimal_e_invoice-0.1.0.zip');
const template = new Template(new Uint8Array(await templateResponse.arrayBuffer()));
const xmlResponse = await fetch('/factur-x.xml');
const xml = new Uint8Array(await xmlResponse.arrayBuffer());
const jsonInputs = new Map();
jsonInputs.set('invoice', JSON.stringify({
id: '2026-0001',
customer: 'ACME Corp',
total: '1190.00 EUR',
}));
const blobInputs = new Map();
blobInputs.set('zugferd', { data: xml });
const pdf = template.export(jsonInputs, blobInputs);
```
* C#
```csharp
using System.Text.Json.Nodes;
using Oicana;
using Oicana.Config;
using Oicana.Inputs;
var template = new Template(File.ReadAllBytes("minimal_e_invoice-0.1.0.zip"));
var xml = File.ReadAllBytes("factur-x.xml");
var jsonInputs = new Dictionary
{
["invoice"] = JsonNode.Parse(
"""{ "id": "2026-0001", "customer": "ACME Corp", "total": "1190.00 EUR" }""")!,
};
var blobInputs = new Dictionary
{
["zugferd"] = new BlobInput(xml),
};
var pdf = template.ExportPdf(jsonInputs, blobInputs);
```
* Java
```java
import com.oicana.BlobInput;
import com.oicana.CompilationMode;
import com.oicana.ExportFormat;
import com.oicana.Template;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
byte[] templateBytes = Files.readAllBytes(Path.of("minimal_e_invoice-0.1.0.zip"));
try (var template = new Template(templateBytes)) {
byte[] xml = Files.readAllBytes(Path.of("factur-x.xml"));
String invoice = """
{"id":"2026-0001","customer":"ACME Corp","total":"1190.00 EUR"}""";
byte[] pdf = template.export(
Map.of("invoice", invoice),
Map.of("zugferd", new BlobInput(xml)));
}
```
* TS (Node.js)
```typescript
import { readFile } from 'node:fs/promises';
import { Template, Pdf, type BlobInput } from '@oicana/node';
const template = new Template(await readFile('minimal_e_invoice-0.1.0.zip'));
const xml = await readFile('factur-x.xml');
const jsonInputs = new Map();
jsonInputs.set('invoice', JSON.stringify({
id: '2026-0001',
customer: 'ACME Corp',
total: '1190.00 EUR',
}));
const blobInputs = new Map();
blobInputs.set('zugferd', { data: xml });
const pdf = template.export(jsonInputs, blobInputs, Pdf);
```
* PHP
```php
use Oicana\CompilationMode;
use Oicana\Inputs\BlobInput;
use Oicana\Template;
$template = new Template(file_get_contents('minimal_e_invoice-0.1.0.zip'));
try {
$xml = file_get_contents('factur-x.xml');
$pdf = $template->export(
jsonInputs: [
'invoice' => ['id' => '2026-0001', 'customer' => 'ACME Corp', 'total' => '1190.00 EUR'],
],
blobInputs: [
'zugferd' => new BlobInput($xml),
],
mode: CompilationMode::Production,
);
} finally {
$template->close();
}
```
* Python
```python
import json
from pathlib import Path
from oicana import BlobInput, CompilationMode, Template
template_bytes = Path("minimal_e_invoice-0.1.0.zip").read_bytes()
with Template(template_bytes) as template:
xml = Path("factur-x.xml").read_bytes()
pdf = template.export_pdf(
json_inputs={
"invoice": json.dumps(
{"id": "2026-0001", "customer": "ACME Corp", "total": "1190.00 EUR"}
),
},
blob_inputs={
"zugferd": BlobInput(data=xml),
},
mode=CompilationMode.PRODUCTION,
)
```
* Rust
```rust
use std::fs::File;
use oicana::Template;
use oicana::export::pdf::export_pdf;
use oicana::input::{BlobInput, CompilationConfig, JsonInput, TemplateInputs};
let template_file = File::open("minimal_e_invoice-0.1.0.zip")?;
let mut template = Template::init(template_file)?;
let xml = std::fs::read("factur-x.xml")?;
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::production());
inputs.with_input(JsonInput::new(
"invoice",
serde_json::json!({
"id": "2026-0001",
"customer": "ACME Corp",
"total": "1190.00 EUR",
})
.to_string(),
));
inputs.with_input(BlobInput::new("zugferd", xml));
let result = template.compile(inputs)?;
let pdf = export_pdf(
&result.document,
&template,
template.manifest().pdf_standards(),
template.manifest().pdf_tagged(),
None,
)?;
```
For the framework around these calls (loading the template at startup, serving the PDF over HTTP) follow the [getting started chapter](/docs/getting-started/4-integrations/) for your stack. The code snippets are not complete and in most cases are missing required boilerplate that is unrelated to Oicana.
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
You now have code that can produce an e-invoice that passes validation. To turn it into a real e-invoice setup:
1. **Generate the XML from your data**, so the embedded invoice and the visible PDF always describe the same thing instead of embedding a fixed sample. There are many libraries that can produce these xml files in the different language ecosystems.
2. **Flesh out the visible layout** into a proper invoice. The [`invoice_zugferd` example template](https://github.com/oicana/oicana-example-templates/tree/main/templates/invoice_zugferd) can be a more complete reference.
3. **Pick the right profile** for your obligations (`MINIMUM`, `BASIC`, `EN 16931`, `XRECHNUNG`, and more) and pass the matching value to `factur-x`.
# Integration Libraries
> Libraries and packages for integrating Oicana into different tech stacks.
The following libraries and packages integrate Oicana into different tech stacks. The usual functionality is registration of a template and compilation to different output formats with given inputs. Registering a template runs a warm-up compilation everywhere except in Rust; see [required inputs](/docs/templates/inputs/#required-inputs) for when that fails.
Help us prioritize
If you are missing support for a specific tech stack, please reach out at !
For all integrations, you can find open source example applications on GitHub.
## Compilation warnings
[Section titled “Compilation warnings”](#compilation-warnings)
Typst reports warnings next to errors. For example for an unknown font family that falls back, a deprecated function, or an unused import. A warning does not fail the compilation, so an export succeeds and hands you a document that may not look like you expect. `oicana compile` prints these warnings; the integrations collect them instead and leave it up to you to look. The browser integration also logs them with `console.warn`; call `setLogLevel('error')` to silence that.
Every integration except Rust exposes the warnings of the most recent compilation, and every compiled document carries the warnings of the compilation that produced it. When there were none you get the language’s “nothing”: `undefined` in TypeScript, `None` in Python, `null` in C# and PHP, `Optional.empty()` in Java, and `None` in Rust.
| Integration | Warnings of the last compilation | Warnings of a compiled document |
| ----------------- | -------------------------------- | ------------------------------- |
| Browser / Node.js | `template.warnings()` | `document.warnings` |
| C# | `template.Warnings` | `document.Warnings` |
| Java | `template.warnings()` | `document.warnings()` |
| PHP | `$template->warnings()` | `$document->warnings` |
| Python | `template.warnings` | `document.warnings` |
| Rust | – | `compiled_document.warnings` |
```typescript
const pdf = template.exportPdf(jsonInputs);
const warnings = template.warnings();
if (warnings) {
logger.warn(`Template compiled with warnings: ${warnings}`);
}
```
Caution
Prefer reading warnings from a [compiled document](/docs/templates/export/#reusing-a-compilation), since every successful compilation overwrites the warnings on the template. A failed compilation leaves the previous warnings in place.
Templates that must not fall back to a different font can declare the families they need with [`[tool.oicana.fonts] require`](/docs/templates/fonts/), which turns the warning into an error at registration time.
## Beyond exporting
[Section titled “Beyond exporting”](#beyond-exporting)
The export methods cover the common case. Every integration also carries a handful of methods that the getting-started chapters do not use:
| API | What it does |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `exportOnce` (`export_once` in Python) | Compile and export a template once without registering it. Nothing is cached and no warm-up compilation runs, which fits stateless and serverless callers. Not available in Rust, where `Template::init` is cheap enough. |
| `Template.create` (Node.js) | Register a template on a background thread instead of blocking the event loop. See the [Node.js async guide](/docs/guides/nodejs-async/). |
| `manifest` | The template’s manifest with its Typst package section and Oicana configuration, including the input definitions. |
| `source` / `file` | Read a text or binary file out of the packed template. |
| `setValidateInputs` (`set_validate_inputs`) | Turn JSON schema validation on or off for one template instance at runtime, overriding the manifest’s `validate_json_inputs_by_default`. |
| `ZipLimits` | Raise the limits used when reading a template archive (10,000 entries and 512 MiB of decompressed content by default). Pass it to the template constructor or `exportOnce` when `oicana pack` warns that your template is larger. In Java, the constructor that takes limits also takes the warm-up mode; pass `CompilationMode.DEVELOPMENT` to keep the default warm-up. |
| Diagnostic coloring | Whether error and warning messages carry ANSI color codes: `configureDiagnosticColor` (Browser, Node.js), `Configuration.ConfigureDiagnosticColor` (C#), `Configuration.configureDiagnosticColor` (Java), `Configuration::configureDiagnosticColor` (PHP), `configure_diagnostic_color` (Python), and `template.set_diagnostic_color` (Rust). Only Rust colors by default. |
## Supported platforms
[Section titled “Supported platforms”](#supported-platforms)
The integrations support the following platforms:
| Integration | Linux x64 | Linux arm64 | Linux musl (Alpine) | macOS x64 / arm64 | Windows x64 | Windows arm64 |
| ----------- | --------- | ----------- | ------------------- | ----------------- | ----------- | ------------- |
| Node.js | ✓ | ✓ | ✓ (x64 and arm64) | ✓ | ✓ | ✓ |
| Python | ✓ | ✓ | ✓ (x64 and arm64) | ✓ | ✓ | – |
| C# | ✓ | ✓ | – | ✓ | ✓ | – |
| Java | ✓ | ✓ | – | ✓ | ✓ | – |
| PHP | ✓ | ✓ | – | ✓ | ✓ | – |
| Go | ✓ | ✓ | ✓ (x64 and arm64) | ✓ | ✓ | – |
| Ruby | ✓ | ✓ | ✓ (x64 and arm64) | ✓ | ✓ | ✓ |
The C#, Java, Go, and Ruby Linux libraries need glibc 2.28 or newer. The browser integration runs in any browser that supports WebAssembly, and the Rust crate builds for any target Rust and Typst support.
## Browser
[Section titled “Browser”](#browser)
Oicana can run in browsers as WebAssembly. The `@oicana/browser` npm package contains a typed interface for interaction with the `.wasm` file. To not block the UI, it’s advisable to compile templates in a web worker.
An example application using Oicana in a React app [can be found on GitHub](https://github.com/oicana/oicana-example-typescript-react/).
### Initializing the WASM file
[Section titled “Initializing the WASM file”](#initializing-the-wasm-file)
Oicana’s WebAssembly file has to be hosted as part of your frontend application. The initialization method expects the path to the hosted file. If your bundler supports it, the easiest way to get that URL is via `import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url'`.
## C\#
[Section titled “C#”](#c)
The nuget package `Oicana` has a native interface to work with Oicana templates from C#.
An example ASP.NET project using the package [can be found on GitHub](https://github.com/oicana/oicana-example-csharp-asp-net/).
## Java
[Section titled “Java”](#java)
The `com.oicana:oicana` Maven package provides a native JNI interface to work with Oicana templates from Java. It uses native bindings for optimal performance on the server.
In addition to the main package, you need to add the native dependency for your target platform(s):
| **Platform** | **Artifact** |
| --------------- | ---------------------------------- |
| Linux x86\_64 | `com.oicana:oicana-linux-x86_64` |
| Linux aarch64 | `com.oicana:oicana-linux-aarch64` |
| macOS x86\_64 | `com.oicana:oicana-macos-x86_64` |
| macOS aarch64 | `com.oicana:oicana-macos-aarch64` |
| Windows x86\_64 | `com.oicana:oicana-windows-x86_64` |
For example, in Gradle for Linux x86\_64:
```kotlin
dependencies {
implementation("com.oicana:oicana:0.9.0")
runtimeOnly("com.oicana:oicana-linux-x86_64:0.9.0")
}
```
You can add multiple native dependencies if your team uses different platforms. Only the matching native library will be loaded at runtime.
An example Spring Boot application using the package [can be found on GitHub](https://github.com/oicana/oicana-example-java-spring-boot/).
## Node.js
[Section titled “Node.js”](#nodejs)
The npm package `@oicana/node` provides a native Node.js interface to work with Oicana templates. It uses native bindings for optimal performance on the server.
The package is ESM-only and requires Node.js 20.19+ or 22.12+. CommonJS projects can still load it through `require(esm)`.
An example NestJS application using the package [can be found on GitHub](https://github.com/oicana/oicana-example-typescript-nestjs/).
## Rust
[Section titled “Rust”](#rust)
The `oicana` crate allows you to compile Oicana templates directly in Rust projects. This integration provides the most direct access to Oicana’s core functionality.
An example Axum application using this crate [can be found on GitHub](https://github.com/oicana/oicana-example-rust-axum/).
## Python
[Section titled “Python”](#python)
The `oicana` Python package provides native bindings to work with Oicana templates from Python. It uses native extensions for optimal performance.
An example FastAPI application using the package [can be found on GitHub](https://github.com/oicana/oicana-example-python-fastapi/).
## PHP
[Section titled “PHP”](#php)
The `oicana/oicana` Composer package provides a native PHP extension to work with Oicana templates. It uses native bindings for optimal performance.
The package is not on Packagist. It is served from Oicana’s own Composer repository at `https://composer.oicana.com`, which you register once in your project. The [PHP getting-started chapter](/docs/getting-started/4-7-php/) shows how.
An example PHP Slim application using the package [can be found on GitHub](https://github.com/oicana/oicana-example-php-slim/).
# Template Dependencies
> Use Typst packages in Oicana templates.
An Oicana template can use any Typst package. Public packages can be found in [the Typst Universe](https://typst.app/universe). You can also install private packages locally and use them in Oicana templates. Using dependencies works just as for any other Typst document.
Example
Take a look at the [example Oicana template `dependency`](https://github.com/oicana/oicana-example-templates/tree/main/templates/dependency). It uses the awesome [`cetz` package to draw a diagram](https://typst.app/universe/package/cetz/) based on an input.
### Local packages
[Section titled “Local packages”](#local-packages)
A locally installed package can have any namespace. A common one is `@local`, but feel free to use your company name or any other identifier. To install a local Typst package, you can use a community developed tool or manually copy files to the right place.
#### Typship
[Section titled “Typship”](#typship)
[Typship](https://github.com/jassielof/typship) is a tool for Typst package development and publishing. Its CLI can install local Typst packages for you. To install a package into the `@local` namespace, run `typship install local` in the package directory.
#### Manual
[Section titled “Manual”](#manual)
Installing a Typst package means that Typst can find it at `{data-dir}/typst/packages/{namespace}/{name}/{version}`. Here, `{data-dir}` is:
* `XDG_DATA_HOME` or `~/.local/share` on Linux
* `~/Library/Application Support` on macOS
* `%APPDATA%` on Windows
For example, on Linux:
1. Store a package in `~/.local/share/typst/packages/local/my-package/1.0.0`
2. Import all items from the package with `#import "@local/my-package:1.0.0": *` in a Typst document
For more information on packages, please refer to [Typst’s packages repository](https://github.com/typst/packages).
## Bundling at pack time
[Section titled “Bundling at pack time”](#bundling-at-pack-time)
When you run [`oicana pack`](/docs/cli/#package-a-template), the CLI scans every `.typ` file in your template (and recursively inside each resolved package) for `import` statements with package specs, and copies the matching package sources into the archive. The result is a self-contained zip. At runtime, integrations resolve imports from the bundle, not from your local Typst cache or registry.
Packages land at `.dependencies////` inside the archive. For example, a template that imports `@preview/cetz:0.4.2` and `@local/my-helpers:1.0.0` will contain `.dependencies/preview/cetz/0.4.2/` and `.dependencies/local/my-helpers/1.0.0/` next to the template’s own files. Files matching a package’s own `exclude` list in its `typst.toml` are left out, as they would be when the package gets published.
How each package is sourced:
* **`@preview` packages** are resolved from your local Typst cache, or downloaded from [Typst Universe](https://typst.app/universe) on the fly if missing.
* **Local packages** (any other namespace, including `@local`) are resolved from `{data-dir}/typst/packages/{namespace}/{name}/{version}` on disk, the same directory described in [Manual](#manual) above.
If a package can’t be resolved, like a local package that isn’t installed in the registry, or an `@preview` package whose download fails, `oicana pack` aborts with `Failed to resolve package ` and writes no archive. Otherwise, a partial archive could fail at runtime in production. Make sure every imported package is installed (or reachable from Typst Universe) before packing.
# Export Formats
> Export Oicana templates to PDF, SVG, and PNG.
Templates can be exported to PDF, SVG, and PNG.
## PDF export
[Section titled “PDF export”](#pdf-export)
The PDF export supports [all standards that Typst has to offer](https://typst.app/docs/reference/pdf/#pdf-standards).
You can configure the PDF standards to use when exporting a given template in the template’s manifest file.
```toml
[tool.oicana.export.pdf]
standards = ["ua-1"]
```
The manifest above would configure this template to [produce PDF files for Universal Access](https://typst.app/docs/reference/pdf/#pdf-ua).
Strict standards have prerequisites
Strict standards require additional metadata in your template. PDF/A profiles need a document date. PDF/UA-1 needs a document title and language, plus alt text on every image and equation.
```typst
#set document(title: "Invoice 2026-001", date: datetime.today())
#set text(lang: "en")
```
Typst surfaces an error per missing detail (e.g. `PDF/A-3b error: missing document date`); see [Typst’s PDF reference](https://typst.app/docs/reference/pdf/) for the full list of prerequisites for each standard.
[Template tests](/docs/templates/tests/#pdf-standards) export every test case with the standards from the manifest, so a missing prerequisite fails `oicana test` instead of your production export.
### Tagged PDFs
[Section titled “Tagged PDFs”](#tagged-pdfs)
By default, Oicana produces tagged (accessible) PDFs. You can turn tagging off in the template’s manifest:
```toml
[tool.oicana.export.pdf]
tagged = false
```
Note
Oicana turns tagging off when you export only a subset of a document’s pages, because Typst cannot tag partial exports. Export the whole document when you need the tags. Standards that require tags (`ua-1`, `a-1a`, `a-2a` and `a-3a`) therefore fail for partial exports and for templates with `tagged = false`, instead of producing a file that claims a conformance it does not meet.
### Combining standards
[Section titled “Combining standards”](#combining-standards)
The `standards` array accepts three kinds of entries (see [the Typst docs for a complete list of supported standards](https://typst.app/docs/reference/pdf)):
* **Versions**: `1.4`, `1.5`, `1.6`, `1.7`, `2.0`
* **PDF/A profiles**: every `a-*` entry (e.g. `a-2b`, `a-3b`, `a-4`)
* **PDF/UA**: `ua-1`
You can list at most one of each kind, and they all have to share a compatible base PDF version. In particular, a PDF/A profile and PDF/UA-1 combine into an accessible, archivable document when both build on the same PDF version. For example, PDF/A-3b and PDF/UA-1 (both based on PDF 1.7):
```toml
[tool.oicana.export.pdf]
standards = ["ua-1", "a-3b"]
```
This is exactly the combination [our `invoice` example template](https://github.com/oicana/oicana-example-templates/tree/main/templates/invoice) exports, and what an accessible e-invoice needs.
Incompatible combinations are rejected. For example, `["a-4", "ua-1"]` fails because PDF/A-4 builds on PDF 2.0 while PDF/UA-1 requires PDF 1.7 or earlier. Listing two PDF/A profiles or two versions is rejected as well. `oicana validate` catches invalid combinations in your manifest, and `oicana test` catches documents that do not meet the standards they declare.
### Embedded files and e-invoices
[Section titled “Embedded files and e-invoices”](#embedded-files-and-e-invoices)
Oicana’s PDF export can attach files to the document and write custom XMP metadata. Together with the PDF/A-3b standard, this is what e-invoice formats such as [ZUGFeRD and Factur-X](https://www.ferd-net.de/en/standards/zugferd/factur-x) require.
## PNG export
[Section titled “PNG export”](#png-export)
In many scenarios, PNG export is an easy option for previews.
Oicana integrations allow configuring the pixels per point in a PNG export. A smaller ratio leads to faster file generation and smaller files, but lower resolution. The default is 1px/pt, except in Rust, where the value is required. The parameter is called `pixelsPerPt` (`pixels_per_pt` in Python and Rust) and comes before the page range on every PNG export method, as in the examples below.
Note
When a PNG export covers more than one page, the pages are merged into a single, vertically stacked image. Export pages individually (see below) if you need separate files.
Note
Typst’s PNG export is optimized for speed, not file size. Before you save the images or send them over the network, consider optimizing them.
To keep memory usage in check, a PNG export is limited to 256 million pixels by default, which is about 1 GB of memory. The budget covers the per-page buffers as well as the merged canvas they are combined into, so an export costs roughly twice its output pixels. That corresponds to roughly 14 A4 pages at 300 DPI, or a single A4 page at about 1150 DPI. A large document combined with a high pixels-per-point ratio can hit this limit. In that case you can still export pages individually.
## SVG export
[Section titled “SVG export”](#svg-export)
Like PNG, a multi-page export produces a single file with the pages stacked vertically, separated by the same gap. A two-page A4 document comes out as one SVG with a `viewBox` covering both pages. Export pages individually (see below) if you need one file per page.
SVG has no resolution setting, since it is vector output, and none of the PNG pixel budget applies to it.
## Exporting specific pages
[Section titled “Exporting specific pages”](#exporting-specific-pages)
Every export method accepts an optional page range, so you can export just a part of a document instead of the whole thing. Page indices are **0-based and inclusive**: the range `0`–`2` exports the first three pages. Both bounds are optional. Omitting the start exports from the first page, omitting the end exports through the last page.
Each integration exposes this as a `pages` argument on the export methods, together with a `PageRange` helper to construct the range. Method names follow each language’s conventions, for example `exportPdf` in Node.js, `export_pdf` in Python and Rust, and `ExportPdf` in C#.
* TS (Browser)
```typescript
import { CompilationMode, PageRange, Template } from '@oicana/browser';
// The first three pages
const excerpt = template.exportPdf(
jsonInputs,
blobInputs,
CompilationMode.Production,
PageRange.of(0, 2),
);
// The third page through the end of the document
const rest = template.exportPdf(
jsonInputs,
blobInputs,
CompilationMode.Production,
PageRange.of(2),
);
// The first page as a PNG at twice the default resolution
const preview = template.exportPng(
jsonInputs,
blobInputs,
CompilationMode.Production,
2.0,
PageRange.single(0),
);
```
* C#
```csharp
using Oicana.Config;
// The first three pages
var excerpt = template.ExportPdf(
jsonInputs, blobInputs, CompilationMode.Production, PageRange.Of(0, 2));
// The third page through the end of the document
var rest = template.ExportPdf(
jsonInputs, blobInputs, CompilationMode.Production, PageRange.Of(2));
// The first page as a PNG at twice the default resolution
var preview = template.ExportPng(
jsonInputs, blobInputs, CompilationMode.Production, 2.0f, PageRange.Single(0));
```
* Java
```java
import com.oicana.CompilationMode;
import com.oicana.PageRange;
// The first three pages
byte[] excerpt = template.exportPdf(
jsonInputs, blobInputs, CompilationMode.PRODUCTION, PageRange.of(0, 2));
// The third page through the end of the document
byte[] rest = template.exportPdf(
jsonInputs, blobInputs, CompilationMode.PRODUCTION, PageRange.of(2, null));
// The first page as a PNG at twice the default resolution
byte[] preview = template.exportPng(
jsonInputs, blobInputs, CompilationMode.PRODUCTION, 2.0f, PageRange.single(0));
```
* TS (Node.js)
```typescript
import { CompilationMode, PageRange, Template } from '@oicana/node';
// The first three pages
const excerpt = template.exportPdf(
jsonInputs,
blobInputs,
CompilationMode.Production,
PageRange.of(0, 2),
);
// The third page through the end of the document
const rest = template.exportPdf(
jsonInputs,
blobInputs,
CompilationMode.Production,
PageRange.of(2),
);
// The first page as a PNG at twice the default resolution
const preview = template.exportPng(
jsonInputs,
blobInputs,
CompilationMode.Production,
2.0,
PageRange.single(0),
);
```
* PHP
```php
use Oicana\PageRange;
// The first three pages
$excerpt = $template->exportPdf(
jsonInputs: $jsonInputs,
pages: PageRange::of(0, 2),
);
// The third page through the end of the document
$rest = $template->exportPdf(
jsonInputs: $jsonInputs,
pages: PageRange::of(2),
);
// The first page as a PNG at twice the default resolution
$preview = $template->exportPng(
jsonInputs: $jsonInputs,
pixelsPerPt: 2.0,
pages: PageRange::single(0),
);
```
* Python
```python
from oicana import PageRange
# The first three pages
excerpt = template.export_pdf(json_inputs=json_inputs, pages=PageRange.of(0, 2))
# The third page through the end of the document
rest = template.export_pdf(json_inputs=json_inputs, pages=PageRange.of(2))
# The first page as a PNG at twice the default resolution
preview = template.export_png(
json_inputs=json_inputs,
pixels_per_pt=2.0,
pages=PageRange.single(0),
)
```
* Rust
```rust
use oicana::export::pages::PageRange;
use oicana::export::pdf::export_pdf;
use oicana::export::png::export_png;
let result = template.compile(inputs)?;
// The first three pages
let excerpt = export_pdf(
&result.document,
&template,
template.manifest().pdf_standards(),
template.manifest().pdf_tagged(),
Some(&PageRange { start: Some(0), end: Some(2) }),
)?;
// The third page through the end of the document
let rest = export_pdf(
&result.document,
&template,
template.manifest().pdf_standards(),
template.manifest().pdf_tagged(),
Some(&PageRange::from(2)),
)?;
// The first page as a PNG at twice the default resolution
let preview = export_png(&result.document, 2.0, Some(&PageRange::single(0)))?;
```
Rust builds a range with the `PageRange` struct or its `single`, `from` and `to` constructors, and passes `None` for the whole document.
PNG and SVG export sit behind Cargo features:
Part of Cargo.toml
```toml
oicana = { version = "0.9.0", features = ["png", "svg"] }
```
## Reusing a compilation
[Section titled “Reusing a compilation”](#reusing-a-compilation)
The export methods above compile the template and export it in a single call. When you need several exports of the *same* inputs, for example a full PDF plus a per-page PNG preview, compiling once and exporting repeatedly avoids the redundant compilation work.
Call `compile` instead of an export method to get a compiled document handle. It exposes the same export methods and reports its page count, so you can export individual pages or page ranges without recompiling. Release the handle when you are done to free memory.
* TS (Browser)
```typescript
import { PageRange, Template } from '@oicana/browser';
const document = template.compile(jsonInputs, blobInputs);
try {
const pdf = document.exportPdf();
const thumbnails = Array.from({ length: document.pageCount }, (_, page) =>
document.exportPng(2.0, PageRange.single(page)),
);
} finally {
document.dispose();
}
```
`CompiledDocument` implements `Symbol.dispose`, so `using document = template.compile(...)` releases it for you. TypeScript needs `ESNext.Disposable` in the `lib` of your `tsconfig.json` for that; the Vite `react-ts` scaffold ships `"lib": ["ES2023", "DOM"]` and reports `Cannot find global type 'Disposable'` until you add it.
* C#
```csharp
using Oicana.Config;
using var document = template.Compile(jsonInputs, blobInputs);
var pdf = document.ExportPdf();
var thumbnails = Enumerable
.Range(0, document.PageCount)
.Select(page => document.ExportPng(2.0f, PageRange.Single(page)))
.ToList();
```
* Java
```java
import com.oicana.CompiledDocument;
import com.oicana.PageRange;
try (CompiledDocument document = template.compile(jsonInputs, blobInputs)) {
byte[] pdf = document.exportPdf();
for (int page = 0; page < document.pageCount(); page++) {
byte[] thumbnail = document.exportPng(2.0f, PageRange.single(page));
}
}
```
* TS (Node.js)
```typescript
import { PageRange, Template } from '@oicana/node';
using document = await template.compileAsync(jsonInputs, blobInputs);
const pdf = document.exportPdf();
const thumbnails = Array.from({ length: document.pageCount }, (_, page) =>
document.exportPng(2.0, PageRange.single(page)),
);
```
`using` frees the document at the end of the scope. It is TypeScript syntax here: `tsc` compiles it down for you, but plain JavaScript needs Node 24, so call `document.dispose()` in a `finally` block if you are not compiling through TypeScript. See the [Node.js async guide](/docs/guides/nodejs-async/) for when to prefer the `Async` variants.
* PHP
```php
use Oicana\PageRange;
$document = $template->compile(jsonInputs: $jsonInputs);
try {
$pdf = $document->exportPdf();
for ($page = 0; $page < $document->pageCount(); $page++) {
$thumbnail = $document->exportPng(2.0, PageRange::single($page));
}
} finally {
$document->close();
}
```
* Python
```python
from oicana import PageRange
with template.compile(json_inputs=json_inputs) as document:
pdf = document.export_pdf()
thumbnails = [
document.export_png(2.0, pages=PageRange.single(page))
for page in range(len(document.pages))
]
```
* Rust
```rust
use oicana::export::pages::PageRange;
use oicana::export::pdf::export_pdf;
use oicana::export::png::export_png;
let result = template.compile(inputs)?;
let pdf = export_pdf(
&result.document,
&template,
template.manifest().pdf_standards(),
template.manifest().pdf_tagged(),
None,
)?;
let cover = export_png(&result.document, 2.0, Some(&PageRange::single(0)))?;
```
# Fonts
> Where Oicana templates get their fonts.
A template can get its fonts from three places:
1. fonts packed into the template
2. fonts provided by the application that loads the template
3. the fonts built into Typst
| | Packed | Host-provided | Built into Typst |
| ------------------------ | ------------------------------------------------- | ---------------------------------- | ----------------------------------------------- |
| Available where | everywhere the template is loaded | only where the host registers them | everywhere |
| Copies | one per template archive | one per process | none, part of Oicana |
| Setup in the application | none | register fonts at startup | none |
| Good for | template-specific fonts, self-contained templates | shared corporate fonts | drafts and templates without brand requirements |
## Fonts packed with the template
[Section titled “Fonts packed with the template”](#fonts-packed-with-the-template)
Add a `.ttf`, `.ttc`, `.otf`, or `.otc` file to the template project. The location of the file in the template is not relevant, it can even be part of an imported package. `oicana pack` bundles every font it finds in the template directory.
Some Typst editors, like the official web app, also support font files as part of a Typst project and will use them in their preview. If you use an IDE plugin for Typst development, the settings of said plugin might support loading additional fonts for the preview.
Example
Take a look at the [example Oicana template `fonts`](https://github.com/oicana/oicana-example-templates/tree/main/templates/fonts). It demonstrates how to include and use custom fonts.
## Fonts provided by the host
[Section titled “Fonts provided by the host”](#fonts-provided-by-the-host)
The application loading a template can register fonts for it. Fonts are registered once per process and shared by every template that is registered afterwards. All integrations accept raw bytes, everywhere except the browser they also accept paths to a font file or to a directory of fonts. The downside of this approach is that the templates are no longer self-contained. But it can save you from significant duplication.
See the [host-provided fonts guide](/docs/guides/host-fonts/) for how to register fonts in each integration.
## Fonts built into Typst
[Section titled “Fonts built into Typst”](#fonts-built-into-typst)
The fonts “Libertinus Serif”, “New Computer Modern”, “DejaVu Sans Mono”, and “New Computer Modern Math” are included in Typst and always available in Oicana templates. They need no setup at all, but they are the only fonts you get for free.
## How fonts are resolved
[Section titled “How fonts are resolved”](#how-fonts-are-resolved)
When a template is registered, Oicana builds the list of fonts available to it in this order:
1. fonts packed with the template
2. fonts registered by the host
3. the fonts built into Typst
Typst resolves ties in font fallback by that order, so a template that packs a family always wins over a host font with the same name.
## Requiring fonts from the host
[Section titled “Requiring fonts from the host”](#requiring-fonts-from-the-host)
A template can declare the families it expects in its manifest:
Part of typst.toml
```toml
[tool.oicana.fonts]
require = ["Corporate Sans"]
```
Registering such a template fails with a clear error when a family is missing, instead of the compilation silently falling back to another font:
```plaintext
World creation error: The template requires font families that are not available: Corporate Sans. Either add the fonts to the template or register them with the host.
```
Matching follows Typst’s own family resolution and is case insensitive. Fonts packed with the template satisfy the requirement as well.
The check runs when the template is loaded, against the fonts actually available at that moment. `oicana validate` does not run it: whether a required family exists is a property of the host, not of the manifest, and validation has no host to ask. A template that declares a family it does not pack therefore passes `validate` and fails later at registration. To catch that in CI, compile the template with the fonts you expect the host to provide: `oicana compile --font-path ./brand-fonts` fails with the same error when a required family is missing.
# Helpful Packages
> An opinionated collection of useful Typst packages for Oicana template creation.
An opinionated collection of useful Typst packages for Oicana template creation.
## Data Visualization
[Section titled “Data Visualization”](#data-visualization)
### Lilaq
[Section titled “Lilaq”](#lilaq)
Scientific data visualization. Supports line plots, scatter plots, bar charts, boxplots, contour plots, error bars, dual-axis configurations, and more.
### Primaviz
[Section titled “Primaviz”](#primaviz)
A pure-Typst charting library with 50+ chart types, multiple themes, and zero dependencies. Includes bar charts, pie/donut charts, gauges, heatmaps, waterfall charts, funnel charts, Sankey diagrams, and more. Well-suited for business reports.
## Drawing
[Section titled “Drawing”](#drawing)
### Cetz
[Section titled “Cetz”](#cetz)
Drawing with Typst made easy, providing an API inspired by TikZ and Processing. Includes modules for plotting, charts and tree layout.
### Fletcher
[Section titled “Fletcher”](#fletcher)
Draw diagrams with nodes and arrows.
## Tables
[Section titled “Tables”](#tables)
### Tablem
[Section titled “Tablem”](#tablem)
Write tables using markdown-like syntax with pipe delimiters. Supports header detection, cell merging, and custom rendering.
## Barcodes & QR Codes
[Section titled “Barcodes & QR Codes”](#barcodes--qr-codes)
### Tiaoma
[Section titled “Tiaoma”](#tiaoma)
Barcode and QR code generator powered by the Zint library. Supports 40+ barcode standards including QR codes, Code 128, EAN/UPC, Data Matrix, PDF417, and Aztec codes.
### Zebra
[Section titled “Zebra”](#zebra)
QR code and Data Matrix generator using native Typst rendering. Optimizes paths for cleaner output and smaller file sizes.
# Template Inputs
> Define JSON and blob inputs for Oicana templates.
Oicana supports two types of inputs. A JSON input takes structured data while binary data can be passed into templates through a blob input.
Inputs are defined in the template manifest. Integrations can list all inputs of a template to, for example, validate input values or offer an editor.
## JSON inputs
[Section titled “JSON inputs”](#json-inputs)
The `type` property of the input definition must be `json`. The only other required property is `key`.
Part of typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "data"
development = "development-data.json"
```
Required inputs need a value at registration
Inputs are [required](#required-inputs) by default. Creating a template runs a warm-up compilation in every integration except Rust, and that fails if a required input has no value. Give each required input a [`development` or `default` value](#default-and-development-values), like the `development` value above, or pass warm-up inputs when creating the template. `development` values also let the template render outside of Oicana (e.g. in an IDE).
The following code snippet shows how to set the value for this input from your integration:
* TS (Browser)
```typescript
import { initialize, Template } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
await initialize(wasmUrl);
const response = await fetch('/template.zip');
const templateBytes = new Uint8Array(await response.arrayBuffer());
const template = new Template(templateBytes);
const jsonInputs = new Map();
jsonInputs.set('data', JSON.stringify({ name: 'Alice' }));
const pdf = template.export(jsonInputs, new Map());
```
* C#
```csharp
using System.Text.Json.Nodes;
using Oicana;
using Oicana.Config;
using Oicana.Inputs;
var templateBytes = File.ReadAllBytes("template.zip");
var template = new Template(templateBytes);
var jsonInputs = new Dictionary
{
["data"] = JsonNode.Parse("""{ "name": "Alice" }""")!,
};
var pdf = template.ExportPdf(jsonInputs);
```
* Go
```go
templateBytes, err := os.ReadFile("template.zip")
if err != nil {
return err
}
template, err := oicana.NewTemplate(templateBytes)
if err != nil {
return err
}
defer template.Close()
pdf, err := template.ExportPdf(oicana.Inputs{
JSON: map[string]any{"data": map[string]any{"name": "Alice"}},
})
```
Go encodes each JSON input value with `json.Marshal`, so a struct works as well. A Go `string` becomes a JSON string; pass a `json.RawMessage` for JSON you already have.
* Java
```java
import com.oicana.CompilationMode;
import com.oicana.ExportFormat;
import com.oicana.Template;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
byte[] templateBytes = Files.readAllBytes(Path.of("template.zip"));
try (var template = new Template(templateBytes)) {
String json = "{\"name\":\"Alice\"}";
byte[] pdf = template.export(
Map.of("data", json),
Map.of(),
ExportFormat.pdf(),
CompilationMode.PRODUCTION);
}
```
* TS (Node.js)
```typescript
import { readFile } from 'node:fs/promises';
import { Template, Pdf } from '@oicana/node';
const templateBytes = await readFile('template.zip');
const template = new Template(templateBytes);
const jsonInputs = new Map();
jsonInputs.set('data', JSON.stringify({ name: 'Alice' }));
const pdf = template.export(jsonInputs, new Map(), Pdf);
```
* PHP
```php
use Oicana\CompilationMode;
use Oicana\Template;
$templateBytes = file_get_contents('template.zip');
$template = new Template($templateBytes);
try {
$pdf = $template->export(
jsonInputs: ['data' => ['name' => 'Alice']],
mode: CompilationMode::Production,
);
} finally {
$template->close();
}
```
PHP accepts either an associative array (as shown) or a pre-encoded JSON string for each JSON input.
* Python
```python
import json
from pathlib import Path
from oicana import CompilationMode, Template
template_bytes = Path("template.zip").read_bytes()
with Template(template_bytes) as template:
pdf = template.export_pdf(
json_inputs={"data": json.dumps({"name": "Alice"})},
mode=CompilationMode.PRODUCTION,
)
```
* Ruby
```ruby
require "oicana"
Oicana::Template.open(File.binread("template.zip")) do |template|
pdf = template.export_pdf(json_inputs: { "data" => { name: "Alice" } })
end
```
Ruby accepts a Hash or any other value `JSON.generate` encodes, or a String that already holds JSON. To pass a JSON string value, encode it first, for example `"Alice".to_json`.
* Rust
```rust
use std::fs::File;
use oicana::Template;
use oicana::input::{CompilationConfig, JsonInput, TemplateInputs};
let template_file = File::open("template.zip")?;
let mut template = Template::init(template_file)?;
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::production());
inputs.with_input(JsonInput::new(
"data",
serde_json::json!({ "name": "Alice" }).to_string(),
));
let result = template.compile(inputs)?;
```
## Blob inputs
[Section titled “Blob inputs”](#blob-inputs)
Blob inputs can be used for binary data like images. Additional metadata can be used to further specify the type of binary data in the input.
Part of typst.toml
```toml
[[tool.oicana.inputs]]
type = "blob"
key = "logo"
development = { file = "development-logo.png" }
```
As a common use case for blob inputs, images have special support in the `oicana` Typst package.
To set the value for this input from your integration, pick your language. Each example reads a `logo.png` file and passes its bytes along with an `image_format` metadata entry so the `oicana-image` helper can pick the right decoder:
* TS (Browser)
```typescript
import { initialize, Template, type BlobInput } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
await initialize(wasmUrl);
const templateResponse = await fetch('/template.zip');
const templateBytes = new Uint8Array(await templateResponse.arrayBuffer());
const template = new Template(templateBytes);
const logoResponse = await fetch('/logo.png');
const logo = new Uint8Array(await logoResponse.arrayBuffer());
const blobInputs = new Map();
blobInputs.set('logo', {
data: logo,
metadata: { image_format: 'png' },
});
const pdf = template.export(new Map(), blobInputs);
```
* C#
```csharp
using System.Text.Json.Nodes;
using Oicana;
using Oicana.Config;
using Oicana.Inputs;
var templateBytes = File.ReadAllBytes("template.zip");
var template = new Template(templateBytes);
var logo = File.ReadAllBytes("logo.png");
var blobInputs = new Dictionary
{
["logo"] = new BlobInput(logo, new BlobMeta { ImageFormat = "png" }),
};
var pdf = template.ExportPdf(blobInputs: blobInputs);
```
* Go
```go
templateBytes, err := os.ReadFile("template.zip")
if err != nil {
return err
}
template, err := oicana.NewTemplate(templateBytes)
if err != nil {
return err
}
defer template.Close()
logo, err := os.ReadFile("logo.png")
if err != nil {
return err
}
pdf, err := template.ExportPdf(oicana.Inputs{
Blobs: map[string]oicana.Blob{
"logo": {Data: logo, Metadata: map[string]any{"image_format": "png"}},
},
})
```
* Java
```java
import com.oicana.BlobInput;
import com.oicana.CompilationMode;
import com.oicana.ExportFormat;
import com.oicana.Template;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
byte[] templateBytes = Files.readAllBytes(Path.of("template.zip"));
try (var template = new Template(templateBytes)) {
byte[] logo = Files.readAllBytes(Path.of("logo.png"));
byte[] pdf = template.export(
Map.of(),
Map.of("logo", new BlobInput(logo, Map.of("image_format", "png"))),
ExportFormat.pdf(),
CompilationMode.PRODUCTION);
}
```
* TS (Node.js)
```typescript
import { readFile } from 'node:fs/promises';
import { Template, Pdf, type BlobInput } from '@oicana/node';
const templateBytes = await readFile('template.zip');
const template = new Template(templateBytes);
const logo = await readFile('logo.png');
const blobInputs = new Map();
blobInputs.set('logo', {
data: logo,
metadata: { image_format: 'png' },
});
const pdf = template.export(new Map(), blobInputs, Pdf);
```
* PHP
```php
use Oicana\CompilationMode;
use Oicana\Inputs\BlobInput;
use Oicana\Template;
$templateBytes = file_get_contents('template.zip');
$template = new Template($templateBytes);
try {
$logo = file_get_contents('logo.png');
$pdf = $template->export(
blobInputs: [
'logo' => new BlobInput($logo, ['image_format' => 'png']),
],
mode: CompilationMode::Production,
);
} finally {
$template->close();
}
```
* Python
```python
from pathlib import Path
from oicana import BlobInput, CompilationMode, Template
template_bytes = Path("template.zip").read_bytes()
with Template(template_bytes) as template:
logo = Path("logo.png").read_bytes()
pdf = template.export_pdf(
blob_inputs={
"logo": BlobInput(data=logo, metadata={"image_format": "png"}),
},
mode=CompilationMode.PRODUCTION,
)
```
* Ruby
```ruby
require "oicana"
Oicana::Template.open(File.binread("template.zip")) do |template|
logo = File.binread("logo.png")
pdf = template.export_pdf(
blob_inputs: { "logo" => Oicana::BlobInput.new(data: logo, metadata: { image_format: "png" }) },
)
end
```
* Rust
```rust
use std::fs::File;
use oicana::Template;
use oicana::input::{BlobInput, CompilationConfig, ImageFormat, TemplateInputs};
let template_file = File::open("template.zip")?;
let mut template = Template::init(template_file)?;
let logo = std::fs::read("logo.png")?;
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::production());
inputs.with_input(BlobInput::image("logo", logo, ImageFormat::Png));
let result = template.compile(inputs)?;
```
Metadata is optional in every integration. PNG and JPEG are recognized from their byte signature, so `oicana-image` picks the right decoder even without an `image_format` entry. Set it when you want to be explicit.
## Default and Development values
[Section titled “Default and Development values”](#default-and-development-values)
Inputs can define two different fallback values, `default` and `development`.
When compiling a template in development mode, input values have the priority
1. Explicit input value
2. `development` value
3. `default` value
If you compile in production mode, the `development` value is ignored:
1. Explicit input value
2. `default` value
While developing an Oicana template in a Typst editor, it will be compiled in development mode. It makes sense to define `development` values for all required inputs of your template to have a functioning preview.
Considering a template with the files `development-data.json`, `default-data.json`, `development-logo.png`, and `default-logo.png`. It could define the following inputs:
Part of typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "data"
development = "development-data.json"
default = "default-data.json"
[[tool.oicana.inputs]]
type = "blob"
key = "logo"
development = { file = "development-logo.png", meta = { image_format = "png", foo = 5, bar = ["development", "two"] } }
default = { file = "default-logo.png", meta = { image_format = "png", foo = 5, bar = ["default", "two"] } }
```
*The `default.meta` objects for blob fallback values are optional.*
In the preview of an editor, the content of `development-data.json` and `development-logo.png` would be used. If compiled in production mode through an Oicana integration, the default fallbacks would be used if the input values are not set programmatically.
## Required inputs
[Section titled “Required inputs”](#required-inputs)
By default, all inputs are required. If a required input has no value after resolving fallbacks (considering the compilation mode), Oicana will produce a compile error.
You can mark an input as optional by setting `required = false`:
Part of typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "extra-data"
required = false
```
Optional inputs without a value will have a `none` value in the `input` dictionary returned by the `setup` function.
Every integration except Rust compiles a template once in development mode when you register it, to warm up the cache. A required input with neither a `development` nor a `default` value makes that warm-up fail, so creating the template throws. Either give the input a fallback value, or pass inputs for the warm-up to the template constructor (or `Template.create` in Node.js). All constructors take JSON inputs, blob inputs, and the warm-up mode as optional arguments after the template bytes.
This is useful for inputs that templates can handle gracefully when absent, while still getting a clear error for inputs that must always be provided.
## Undeclared inputs
[Section titled “Undeclared inputs”](#undeclared-inputs)
Every input value you pass has to match an input definition in the manifest. Oicana rejects a compilation if
* the template declares no input with a given key,
* the value is of the wrong kind, for example a JSON value for an input declared as `blob`,
* the same key is passed both as a JSON and as a blob input.
## JSON schemas
[Section titled “JSON schemas”](#json-schemas)
A JSON input can point at a [JSON Schema](https://json-schema.org/) with the `schema` property. The schema is what `oicana validate` checks fallback values against, what `oicana test` fuzzes from, and what rejects bad input values before a compilation starts.
Part of typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "data"
schema = "schemas/data.schema.json"
```
* The path is relative to the template root, the same as every other file reference in the manifest, and it may point into a subdirectory.
* The schema file is read out of the packed template, so it has to be packed with it. Do not exclude it.
* The dialect is taken from the schema’s `$schema` keyword. Drafts 4, 6, 7, 2019-09, and 2020-12 are supported; without `$schema`, the newest one is assumed.
* References within the same file (`#/$defs/...`, `#/definitions/...`) work. References to other files do not.
Schemas are compiled when the template is registered. A value that violates the schema fails before Typst runs, with the offending paths listed:
```plaintext
error: Input 'data' failed schema validation:
- At /person/name: "A" is shorter than 2 characters
```
## Validation configuration
[Section titled “Validation configuration”](#validation-configuration)
By default, all explicit JSON input values with a schema are validated before compilation. You can control this on two levels.
Fallback values are not validated at compile time
Schema validation runs outside of Typst, before compilation, and only sees explicit input values supplied by the caller. The `default` and `development` fallback values are read from inside Typst by the `oicana` package’s `setup` function and therefore bypass the schema check. A `oicana compile` (or integration call) that ends up using a fallback will not reject a fallback that violates the schema.
Use `oicana validate` to check fallbacks against their schemas, and run it for every template in CI so that broken fallbacks are caught before templates are packed.
### Per-template default
[Section titled “Per-template default”](#per-template-default)
The `validate_json_inputs_by_default` property in `[tool.oicana]` controls whether validation for JSON inputs with schemas is enabled. It defaults to `true`. Setting it to `false` means the template starts with validation disabled, though integrations can still toggle it at runtime per template instance.
Part of typst.toml
```toml
[tool.oicana]
manifest_version = 1
validate_json_inputs_by_default = false
```
### Per-input
[Section titled “Per-input”](#per-input)
Each JSON input has an optional `validate` property that defaults to `true`. Setting it to `false` prevents Oicana from compiling a schema validator for that input, even if a schema is defined. This is useful when a schema is only needed for test fuzzing and not for runtime validation.
Part of typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "data"
schema = "data.schema.json"
validate = false
```
Note that `validate = false` on an input is different from `validate_json_inputs_by_default = false` on the template. The per-input flag prevents validation entirely, while the template-level flag still allows integrations to change the validation behavior at runtime.
## Using inputs in Typst
[Section titled “Using inputs in Typst”](#using-inputs-in-typst)
To access input values in your template, use the `setup` function from the `oicana` Typst package:
```typst
#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)
```
* `input` is a dictionary of resolved input values, keyed by the input’s `key`
* `oicana-image` is a helper function that takes a blob input key and returns a Typst image element
* `oicana-config` contains compilation metadata like `production: true/false`
For blob inputs that are images, you can use the `oicana-image` helper instead of accessing the raw bytes:
```typst
#oicana-image("logo", alt: "Company logo")
```
If you don’t want to use the helper method, you can use the blob input as a dictionary with the raw `bytes` and its metadata in `meta`. Since metadata is optional, read entries with a default:
```typst
#let logo = input.logo
#image(logo.bytes, format: logo.meta.at("image_format", default: auto), alt: "Company logo")
```
For JSON inputs, access the parsed data directly:
```typst
#let name = input.invoice.buyer.name
```
# Template Testing
> Snapshot testing, PDF standard conformance and JSON input fuzzing for Oicana templates.
Oicana comes with test infrastructure for templates. To get started, create a directory called `tests` in a template directory. Here is an example test collection `tests.toml` defining a single snapshot test:
tests.toml
```toml
tests_version = 1
[[test]]
name = "with_logo"
[[test.inputs]]
type = "blob"
key = "logo"
file = "../logo.jpg"
[[test.inputs]]
type = "json"
key = "data"
file = "data.json"
```
All paths in a test collection are relative to its toml file. The collection above defines a test with a `blob` input and a `json` input given as `logo.jpg` in the parent directory and `data.json` next to the test collection. Executing `oicana test` for this template, will compile it with those inputs and attempt to compare the output with a `with_logo.png` living next to the test collection.
Note
On the first run, you’ll need to use `oicana test --update` to create the initial snapshot files. Subsequent test runs will compare against these snapshots.
Use `--update` or `-u` whenever changes in the snapshot files are expected and should be accepted.
The tests directory will be recursively searched for any test collection files in the form of `tests.toml`.
The prefix is also the collection’s name unless the collection sets one, and the name prefixes the default snapshot file of every test in it. A plain `tests.toml` has no prefix and therefore no name, so its snapshots are just `.png`:
| Collection file | `name` | Default snapshot for a test called `with_logo` |
| ------------------- | ----------------- | ---------------------------------------------- |
| `tests.toml` | (none) | `with_logo.png` |
| `layout.tests.toml` | (none) | `layout.with_logo.png` |
| `layout.tests.toml` | `"my_collection"` | `my_collection.with_logo.png` |
Example
The example templates [`table`](https://github.com/oicana/oicana-example-templates/tree/main/templates/table/tests) and [`invoice`](https://github.com/oicana/oicana-example-templates/tree/main/templates/invoice/tests) both define some simple snapshot tests.
## PDF standards
[Section titled “PDF standards”](#pdf-standards)
Every test also exports the compiled document to PDF using the [PDF standards configured in the template manifest](/docs/templates/export/). A template that declares `standards = ["ua-1"]` fails its tests when the document does not meet PDF/UA-1, for example because the document title is missing or an image has no alt text.
The failure names the standards and includes the diagnostics from the export:
```plaintext
🔥 Test failures
-> invoice
↳ with_logo
↳ Failed to export PDF for the standards 'ua-1' configured in the template manifest:
error: PDF/UA-1 error: missing document title
= hint: set the title with `set document(title: [...])`
```
Since the requirements of a standard depend on the content, every test case is checked with its own inputs, including each sample of a [fuzzed input](#json-input-fuzzing).
Set `pdf = false` to skip the PDF export for a single test or for a whole test collection:
tests.toml
```toml
tests_version = 1
pdf = false # no test in this collection exports a PDF
[[test]]
name = "layout_only"
[[test]]
name = "conformance"
pdf = true # ... except this one
```
## Collection settings
[Section titled “Collection settings”](#collection-settings)
`snapshot` and `pdf` can be set for a whole test collection. Tests that configure the same setting override their collection:
tests.toml
```toml
tests_version = 1
name = "fuzzing" # optional, defaults to the collection file's prefix (empty for a plain tests.toml)
snapshot = false # none of the tests below compare against a snapshot file
[[test]]
name = "fuzz_json_input"
[[test]]
name = "fuzz_other_input"
```
## Watch mode
[Section titled “Watch mode”](#watch-mode)
Pass `--watch` (or `-w`) to keep the test runner alive: it executes the suite once, then re-runs the affected tests whenever a source file changes. Useful while iterating on a template. With `oicana test --watch` you can cover both the “did I break a snapshot?” and “does the manifest still parse?” checks on every save.
## JSON input fuzzing
[Section titled “JSON input fuzzing”](#json-input-fuzzing)
If a JSON input in `typst.toml` has a schema configured, you can let Oicana fuzz that input as part of a snapshot test.
tests.toml
```toml
tests_version = 1
[[test]]
name = "fuzz_json_input"
snapshot = false
[[test.inputs]]
type = "json"
key = "data"
samples = 50
```
Setting `snapshot = false` means no image files are created and compared. This is often the right choice for fuzzing tests, because the image output is likely expected to be different for different JSON input values. The configuration of `50` samples will cause Oicana to compile the template with `50` random values for the JSON input that all satisfy the schema. If the schema is very large, it might make sense to increase the number of samples to cover more possible values.
## Full example configuration
[Section titled “Full example configuration”](#full-example-configuration)
A maximal and documented example test collection:
tests.toml
```toml
tests_version = 1
name = "my_collection" # Optional, default is the collection file's prefix (empty for a plain tests.toml)
pdf = true # Optional, default `true` - whether the tests in this collection export a PDF
# snapshot = false # Optional - default snapshot configuration for all tests in this collection
[[test]]
name = "with_logo" # Required
mode = "development" # Optional, default "production" - decides if `development` values of inputs get used or not
snapshot = "my_snapshot.png" # Optional, default "..png" (just ".png" for an unnamed collection) or collection level `snapshot` if set - relative path to a png file that will be compared to the test output or `false` to disable snapshot comparison
pdf = true # Optional, default is collection level `pdf` - export the document with the PDF standards of the template manifest
[[test.inputs]]
type = "blob" # Required - `blob` or `json`
key = "logo" # Required - key of input as configured in the template manifest under test
file = "../logo.jpg" # Required - relative path to a file that will be the value of this input
meta = { image_format = "jpg" } # Optional, default `none` - meta dictionary for the blob input (see input documentation)
[[test.inputs]]
type = "json"
key = "data"
file = "data.json"
[[test]]
name = "test_without_snapshot_comparison"
snapshot = false # this disables comparing the test output with a snapshot file
[[test]]
name = "fuzz_json_input"
snapshot = false
[[test.inputs]]
type = "json"
key = "data"
samples = 50 # this requires that the "data" input has a json schema configured in `typst.toml`
# Any number of additional tests in this collection
[[test]]
name = "a_second_test"
```