();
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...
;
}
```
# 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. 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. 2. Add the [`Oicana` NuGet package](https://www.nuget.org/packages/Oicana#readme-body-tab) as a dependency with `dotnet add package Oicana`. 3. Read the template file and prepare it for compilation at the beginning of `Program.cs`: Part of Program.cs
```cs
using Scalar.AspNetCore;
using System.Text.Json.Nodes;
using Oicana.Config;
using Oicana.Inputs;
using Oicana;
var templateFile =
await File.ReadAllBytesAsync("templates/example-0.1.0.zip");
var template = new Template(templateFile);
```
4. Replace the generated `/weatherforecast` endpoint with the following: Part of Program.cs
```cs
app.MapPost("compile", () =>
{
var stream = template.Export(
new Dictionary(),
new Dictionary(),
ExportFormat.Pdf(),
new CompilationOptions(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 with two empty input dictionaries 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. ## Passing inputs from C\# [Section titled “Passing inputs from C#”](#passing-inputs-from-c) Our `compile` endpoint is currently calling the template’s `Export` method with empty dictionaries. This compiles the template without any explicit inputs. The first dictionary could contain JSON inputs (key to JsonNode) and the second blob inputs (key to BlobInput). 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.Export(
jsonInputs,
new Dictionary(),
ExportFormat.Pdf(),
new CompilationOptions(CompilationMode.Production));
var now = DateTimeOffset.Now;
return Results.File(
fileStream: stream,
contentType: "application/pdf",
fileDownloadName: $"example_{now:yyyy_MM_dd_HH_mm_ss_ffff}.pdf"
);
});
```
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 `CompilationMode.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. 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, 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. `Export` 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.Export(
jsonInputs,
new Dictionary(),
ExportFormat.Pdf(),
new CompilationOptions(CompilationMode.Production));
}
catch (Oicana.Interop.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"
);
});
```
# 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. 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 "3.4.3"
id("io.spring.dependency-management") version "1.1.7"
}
group = "com.example"
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.oicana:oicana:0.6.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.6.0")
runtimeOnly("com.oicana:oicana-linux-aarch64:0.6.0")
runtimeOnly("com.oicana:oicana-macos-x86_64:0.6.0")
runtimeOnly("com.oicana:oicana-macos-aarch64:0.6.0")
runtimeOnly("com.oicana:oicana-windows-x86_64:0.6.0")
}
```
And set the project name in `settings.gradle.kts`: settings.gradle.kts
```kotlin
rootProject.name = "my-pdf-service"
```
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);
}
}
```
You can test it by running `./gradlew bootRun` 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 project called `templates` and copy `example-0.1.0.zip` into that directory. 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.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@Service
public class TemplateService {
private Template template;
@PostConstruct
public void init() throws IOException {
byte[] templateBytes = Files.readAllBytes(
Path.of("templates/example-0.1.0.zip")
);
template = new Template(templateBytes);
}
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. 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. ## 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 switch to production mode: Part of TemplateService.java
```java
public byte[] compile() {
return template.export(
Map.of("info", "{\"name\": \"Baby Yoda\"}"),
Map.of()
);
}
```
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. `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);
}
```
# 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. 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. Create a new directory in the Node.js project called `templates` and copy `example-0.1.0.zip` into that directory. 2. Add the [`@oicana/node` npm package](https://www.npmjs.com/package/@oicana/node) as a dependency with `npm install @oicana/node`. 3. Generate a new controller and service for templates:
```bash
npx nest generate module templates
npx nest generate service templates
npx nest generate controller templates
```
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 } 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(
process.cwd(),
'templates',
'example-0.1.0.zip'
);
const buffer = await fs.readFile(templatePath);
// Template registration defaults to Development mode
// so it will use the development value of our template 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
);
}
}
```
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';
@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);
}
```
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. `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));
}
}
```
# 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. Let’s start with a fresh Axum project. First, create a new binary project with `cargo init --bin` in a new directory. Then add the necessary dependencies to your `Cargo.toml`: Part of Cargo.toml
```toml
[dependencies]
oicana = "0.6.0"
axum = { version = "0.8", features = ["macros"] }
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 std::fs::File;
use std::sync::{Arc, Mutex};
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}
};
#[tokio::main]
async fn main() {
let template_file = File::open("templates/example-0.1.0.zip")
.expect("Failed to open template file");
let template = Template::init(template_file)
.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. 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::input::json::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.
# 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. 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("templates/example-0.1.0.zip")
template_bytes = template_path.read_bytes()
# Template registration uses development mode by default
template = Template(template_bytes)
yield
app = FastAPI(lifespan=lifespan)
@app.post("/compile")
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 deploy multiple worker processes using Gunicorn or similar ASGI servers. 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
import json
# previous code...
@app.post("/compile")
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. 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 Template
# previous code...
@app.post("/compile")
def compile_template():
try:
pdf = template.export_pdf(
json_inputs={"info": json.dumps({"name": "Baby Yoda"})}
)
except Exception 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 two changes to the imports: `logging`, and `HTTPException` alongside `FastAPI`.
# 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. 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.6.0
```
3. Update `index.php` to load the template at startup and add 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. Start the PHP development server in the same shell where you ran the `PHP_INI_SCAN_DIR` line printed by the installer (or run `vendor/bin/oicana-env` first):
```bash
php -S localhost:8000 index.php
```
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)` before and after the `compile` 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"');
});
```
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. `export()` throws a plain `\Exception` that we can catch: 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 (\Exception $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.
# 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. ## 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, 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 18+ [C# / ASP.NET](/docs/getting-started/4-2-csharp/)Prerequisites: .NET; 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 18+ [Rust / Axum](/docs/getting-started/4-5-rust/)Prerequisites: Rust toolchain (cargo) [Python / FastAPI](/docs/getting-started/4-6-python/)Prerequisites: Python 3.9+ and uv [PHP / Slim](/docs/getting-started/4-7-php/)Prerequisites: PHP 8.3+ and Composer ## 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 application. No data leaves your servers. * **Multi-platform** - The same templates work with all Oicana integrations. * **Powerful Layouting** - Templates can use all of Typst’s functionality, including its extensive package ecosystem. * **Performant** - Create a PDF in single digit milliseconds. * **AI and Version Control Ready** - Templates are text files. They can live next to your code and AI can assist in writing them. * **Escape Vendor Lock-in** - Reuse templates with other Typst based solutions. The Typst compiler is open source!
# 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.6.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.6.0/oicana_cli-installer.sh | sh
```
Powershell script: Powershell script to install Oicana CLI
```powershell
-ExecutionPolicy Bypass -c "irm https://github.com/oicana/oicana/releases/download/oicana_cli-v0.6.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 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 sym links and include copies of the linked files in the template. 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/`) and the `output/` directory 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 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`) | ## 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 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. [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-compression`](https://www.npmjs.com/package/vite-plugin-compression) (or its successor) and configure both `gzip` and `brotliCompress`. * **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. ## 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 will resolve the import to bundled bytes instead of a URL string, which makes `initialize()` fail at runtime in a way that’s easy to misread. 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);
```
This is recognized by webpack 5+, Next.js (webpack and Turbopack), CRA 5+, esbuild, Rollup, and Parcel 2. Each emits the WASM as a separate asset and rewrites the URL to point at it. The same form also works in Vite, so it’s a safe choice if you want a single snippet that travels across stacks. ## 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 (the URL-based import above triggers this) so that the URL changes every time the file changes. The package itself ships the file under a fixed name, so if you serve it directly from `node_modules` without a bundler, 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.
# 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)` | `Template.EvictCache(maxAge)` | | Java | `Template.configureAutomaticCacheEviction(maxAge)` | `Template.evictCache(maxAge)` | | PHP | `Template::configureAutomaticCacheEviction($maxAge)` | `Template::evictCache($maxAge)` | | Python | `oicana.configure_automatic_cache_eviction(max_age)` | `oicana.evict_cache(max_age)` | | Rust | `oicana::configure_automatic_cache_eviction(max_age)` | `oicana::evict_cache(max_age)` | 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. 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.
# 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);
```
## 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. ## 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. 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. ## 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 BlobWithMetadata } 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', { bytes: 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.Export(
jsonInputs,
blobInputs,
ExportFormat.Pdf(),
new CompilationOptions(CompilationMode.Production));
```
* 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 BlobWithMetadata } 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', { bytes: 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->cleanup();
}
```
* 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::{CompilationConfig, TemplateInputs};
use oicana::input::input::blob::BlobInput;
use oicana::input::input::json::JsonInput;
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. For all integrations, you can find open source example applications on GitHub. ## 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.6.0")
runtimeOnly("com.oicana:oicana-linux-x86_64:0.6.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. 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. 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. ### 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. 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). The default in Oicana is PDF/A-3b (PDF 1.7). You can configure the default 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 Accessibility-oriented standards like PDF/UA-1 (and several PDF/A profiles) require additional metadata in your template. At minimum a document title and language, plus alt text on every image and equation are required.
```typst
#set document(title: "Invoice 2026-001", description: "Invoice for ...")
#set text(lang: "en")
```
Typst surfaces an error per missing detail (e.g. `PDF/UA-1 error: missing document title`); see [Typst’s PDF reference](https://typst.app/docs/reference/pdf/) for the full list of prerequisites for each standard. ### 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
```
### 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. ### 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 default 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: a human-readable PDF/A-3b document with the structured invoice XML embedded as an associated file and the matching metadata declared in the document’s XMP. ## 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. To keep memory usage in check, a PNG export is limited to 256 million pixels by default, which is about 1 GB of memory. That corresponds to roughly 14 A4 pages at 300 DPI, or a single A4 page at 800 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. ## Exporting specific pages [Section titled “Exporting specific pages”](#exporting-specific-pages) Every export 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 `Export` in C#. ## 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.
# Custom Fonts
> Using custom fonts in Oicana templates.
To use any font in an Oicana template, add a `.ttf`, `.ttc`, `.otf`, or `.otc` file to the project. The location of the file in the template is not relevant, it can even be part of an imported package. 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. The fonts “Libertinus Serif”, “New Computer Modern”, “DejaVu Sans Mono”, and “New Computer Modern Math” are included in Typst by default and always available in Oicana templates.
# 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"
```
The following code snippet shows how to set the value for this input from your integration: * TS (Browser)
```typescript
import { Template } from '@oicana/browser';
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.Export(
jsonInputs,
new Dictionary(),
ExportFormat.Pdf(),
new CompilationOptions(CompilationMode.Production));
```
* 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->cleanup();
}
```
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,
)
```
* Rust
```rust
use std::fs::File;
use oicana::Template;
use oicana::input::{CompilationConfig, TemplateInputs};
use oicana::input::input::json::JsonInput;
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"
```
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 { Template, type BlobWithMetadata } from '@oicana/browser';
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', {
bytes: logo,
meta: { 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.Export(
new Dictionary(),
blobInputs,
ExportFormat.Pdf(),
new CompilationOptions(CompilationMode.Production));
```
* 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 BlobWithMetadata } 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', {
bytes: logo,
meta: { 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->cleanup();
}
```
* 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,
)
```
* Rust
```rust
use std::fs::File;
use oicana::Template;
use oicana::typst::{Bytes, Dict, Value};
use oicana::input::{CompilationConfig, TemplateInputs};
use oicana::input::input::blob::{Blob, BlobInput};
let template_file = File::open("template.zip")?;
let mut template = Template::init(template_file)?;
let logo = std::fs::read("logo.png")?;
let mut metadata = Dict::new();
metadata.insert("image_format".into(), Value::Str("png".into()));
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::production());
inputs.with_input(BlobInput::new(
"logo",
Blob {
bytes: Bytes::new(logo),
metadata,
},
));
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. 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. ## 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")
```
For JSON inputs, access the parsed data directly:
```typst
#let name = input.invoice.buyer.name
```
# Template Testing
> Snapshot testing 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. The tests directory will be recursively searched for any test collection files in the form of `tests.toml`. ## 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
[[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" - relative path to a png file that will be compared to the test output
[[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"
```