This is the full developer documentation for Oicana
# Generate PDFs in your own infrastructure
>
Print-ready PDFs created inside your app in milliseconds: Node, Python, Java, C#, Rust, PHP, or the browser. One template format. No headless Chrome, no per-document fees, no data leaving your servers.
Invoices, reports, contracts, certificates, and statements designed in Typst. Free for non-commercial use, commercial evaluation free for 30 days.
Templatemain.typ
```typst
#import "@preview/oicana:0.2.0": setup
#import "certificate.typ": *
#let project-file(path) = read(path, encoding: none)
#let (input, _, _) = setup(project-file)
#let name = input.certificate.name
#show: certificate.with(name: name)
```
Compile from
BrowserNode.jsPythonJavaC#RustPHP
```ts
import { Template } from "@oicana/browser";
const template = new Template(templateBytes);
const inputs = new Map();
inputs.set("certificate", JSON.stringify({ name: "Alex Kim" }));
const pdf = template.compile(inputs, new Map());
```
```ts
import { Template } from "@oicana/node";
const template = new Template(templateBytes);
const inputs = new Map();
inputs.set("certificate", JSON.stringify({ name: "Marcus Lee" }));
const pdf = template.compile(inputs, new Map());
```
```python
from oicana import Template
import json
template = Template(template_bytes)
pdf = template.compile(
json_inputs={"certificate": json.dumps({"name": "Priya Patel"})}
)
```
```java
import com.oicana.Template;
Template template = new Template(templateBytes);
byte[] pdf = template.compile(
Map.of("certificate", "{\"name\": \"Jakob Müller\"}"),
Map.of()
);
```
```cs
using Oicana;
using System.Text.Json.Nodes;
var template = new Template(templateBytes);
var inputs = new Dictionary
{
["certificate"] = JsonNode.Parse("{ \"name\": \"Sofía García\" }")!
};
var pdf = template.Compile(inputs, new(), ExportFormat.Pdf(),
new CompilationOptions(CompilationMode.Production));
```
```rust
use oicana::Template;
use oicana_input::{TemplateInputs, input::json::JsonInput};
let mut template = Template::init(Cursor::new(template_bytes))?;
let mut inputs = TemplateInputs::new();
inputs.with_input(JsonInput::new(
"certificate".into(),
json!({ "name": "Yui Tanaka" }).to_string(),
));
let pdf = template.compile(inputs)?;
```
```php
use Oicana\Template;
$template = new Template($templateBytes);
$pdf = $template->compile(
jsonInputs: ['certificate' => ['name' => 'Anya Volkov']]
);
```
Outputcertificate.pdf

Your Data Stays With You
PDFs are generated inside your own application. No data is sent to a third-party service, which keeps compliance with regulations like GDPR or HIPAA in your hands. That makes Oicana a fit for fintech, health, and other regulated software. With the browser integration, documents can even be created directly on your users’ devices.
Flat Price, Unlimited Documents
No metering, no per-document fees. One [flat license per application](#pricing) covers unlimited documents, seats, and environments. A hundred invoices a month cost the same as a hundred thousand. Non-commercial use is free.
Milliseconds, Not Seconds
No headless browser eating hundreds of megabytes of memory, no network round-trip. Warmed up templates produce non-trivial PDF files in single digit milliseconds, in process.
Powerful Layouting
Builds on the [modern open source typesetter Typst](https://typst.app/). Oicana templates can use all of Typst’s functionality including its [extensive package ecosystem](https://typst.app/universe/).
Multi-platform
The same templates can be compiled from the browser or server-side with C#, Java, Node.js, Rust, Python, and PHP.
Templates Are Code
Templates are plain text files. Diff them in Git, review them in pull requests, and let AI assist in writing them.
Escape Vendor Lock-in
Reuse templates with other Typst based solutions; The Typst compiler is open source!
## Documentation
[Read the docs](/docs/)Oicana's documentation contains a getting started guide.
[Your tech stack](/pdf-generation/)See an example for your stack: Node.js, Python, Java, C#, Rust, PHP, or the browser.
[Examples](https://github.com/oicana/?q=example)Every Oicana integration has an open source example project in our GitHub organization. There you can also find a repository with example templates.
[Guides](/docs/guides/)The documentation includes guides for common scenarios when using Oicana.
[Comparisons](/compare/)How Oicana compares to Gotenberg, DocRaptor, PDFMonkey, Carbone, and other approaches.
## Pricing
#### Non-commercial
Free
Oicana is source-available under the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0). Any non-commercial use is free — personal projects, hobby use, research, and education. The license also explicitly permits use by charitable organizations, educational institutions, public research organizations, and government institutions, regardless of their funding source.
[View license →](https://github.com/oicana/oicana/blob/main/LICENSE.md)
#### Commercial
Free 30-day trial
Flat fee, no metering. Licenses are** per application**, priced by company size. Pay once for unlimited seats, environments, and documents. Every subscription includes all Oicana components like the CLI and the integrations.
MonthlyAnnual Save 15%
### Startup
For small companies and individuals
Up to €2M yearly revenue
19.00€/ month
Buy subscription
### Scaleup
For growing companies
Up to €25M yearly revenue
49.00€/ month
Buy subscription
### Enterprise
For large organizations
Above €25M yearly revenue
199.00€/ month
Buy subscription
Free 30-day commercial evaluation. Every purchase is covered by a 30-day money-back guarantee. Oicana is in public beta and has been used in production since the alpha.
Already subscribed?[Manage your subscriptions.](https://creem.io/my-orders/login)
Our order processes are conducted by our merchant of record Creem.
#### FAQ
What counts as one application?
An application is one distinct software product or internal tool, regardless of how many services, environments, or users it has. See [Section 4.3 of our Terms of Service](/terms-of-service/#43-per-application-licensing) for the binding definition.
* A SaaS with multiple microservices generating documents1 application
* Web, iOS, and Android apps sharing one backend1 application
* Production, staging, and dev environments1 application
* A customer-facing SaaS plus a separate internal admin tool2 applications
* One codebase white-labeled and sold as 3 separate products (reach out to us for a custom solution)3 applications
Can I try Oicana commercially before buying?
Yes, commercial evaluation is free for 30 days from your first commercial use. You can follow the getting-started guide, build a proof-of-concept integration, and test and use Oicana in production without a subscription. See [Section 4.2 of our Terms of Service](/terms-of-service/#42-evaluation-use) for the full terms.
Is Oicana open source?
Oicana is source available, not open source. All code is public on [GitHub](https://github.com/oicana/oicana) under the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0): non-commercial use is free, commercial use requires a subscription. The Typst package is MIT licensed, and the example projects and templates are open source under their respective licenses.
Your templates are plain Typst projects and the Typst compiler itself is open source. Even if you stop using Oicana, most of your templates keep working with other Typst based tooling.
Do you offer a money-back guarantee?
Yes, we offer a 30-day money-back guarantee. If Oicana isn't the right fit, just email within 30 days of your purchase and we'll refund you in full. See our [Refund Policy](/refund-policy/) for the full terms.
×
### Thank you for your purchase!
Ready to dive in? Head to the [getting started guide](/docs/getting-started/1-setup/) to set up Oicana in your project.
A confirmation email from Creem with your receipt should arrive in your inbox shortly.
Not happy? You're covered by our **30-day money-back guarantee**, just contact us at . You can manage your subscription anytime through the [Creem customer portal](https://creem.io/my-orders/login).
# Installation and Setup
> Install the Oicana CLI and set up an editor for template development with Typst.
No license needed to follow this guide
For commercial use, the first 30 days are free for evaluation. Follow this guide, build a proof of concept, and test Oicana without a subscription. See [Section 4.2 of our Terms of Service](/terms-of-service/#42-evaluation-use) for the full terms.
Non-commercial use of Oicana like personal projects, hobby use, research, education, and use by charitable, educational, public research, and government institutions is free under the [PolyForm Noncommercial License](https://polyformproject.org/licenses/noncommercial/1.0.0).
To get started, you need the Oicana CLI and an editor for Typst. Both are only necessary on machines used for Oicana template development. An end-user machine, running software that uses an Oicana integration, does not require any additional installations.
## Oicana CLI
[Section titled “Oicana CLI”](#oicana-cli)
Install a [prebuild binary from GitHub](https://github.com/oicana/oicana/releases/tag/oicana_cli-v0.6.0) or let a script do that for you.
Bash script:
Shell script to install Oicana CLI
```bash
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/oicana/oicana/releases/download/oicana_cli-v0.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"
```
After the installation, run `oicana --version` to make sure the installation succeeded.
The CLI can manage Oicana templates. The most relevant commands for this guide are `oicana pack` to package a template and `oicana compile` if you want to test template compilation. See the [CLI chapter](/docs/cli/) for more information.
## Editor for Typst
[Section titled “Editor for Typst”](#editor-for-typst)
You can edit Typst files in any text editor, but syntax highlighting and live previews make development significantly easier. Here are some suggestions:
* Several IDEs have Typst plugins with live previews and syntax highlighting.
* Visual Studio Code: [Tinymist](https://marketplace.visualstudio.com/items?itemName=myriad-dreamin.tinymist) offers a complete experience with live preview, syntax/error highlighting and more.
* JetBrains IDEs: [Kvasir](https://plugins.jetbrains.com/plugin/25061-kvasir)
* [Official Typst editor in the browser](https://typst.app/).
* This editor has a [significant free tier and a pro tier for $8 per month](https://typst.app/pricing/). It is maintained by the Typst company.
* Web based; No installation required.
* You can work on the same document with multiple people and sync your templates to GitHub or GitLab.
## For this Guide
[Section titled “For this Guide”](#for-this-guide)
In the following chapters, you will create a basic Oicana template that can be used with any integration. After creating the template, you’ll be able to choose which integration to explore based on your preferred tech stack.
# Create a Template
> Create your first Oicana template with static content and package it.
The simplest Oicana template consists of a `typst.toml` file and a Typst source file with some static content. The `oicana new` command scaffolds both for you in one step.
## Template with static content
[Section titled “Template with static content”](#template-with-static-content)
We will use the `new` command of the Oicana CLI to create a starter template. If you are using the online Typst editor, manually create the files and paste their content.
```bash
oicana new example
```
This creates an `example/` directory with two files. Open it in your IDE with Typst support.
main.typ
```typst
#set document(date: datetime.today())
= Hello
Edit `main.typ` to design your template.
```
Note
This Typst document uses [markup mode](https://typst.app/docs/reference/syntax/#markup), where `=` starts a heading, and plain text is just text. `#` switches to code mode, where you can call functions like `#set document(title: "Invoice")` or `#text(fill: blue)[...]` to style content.
Refer to [Typst’s online documentation](https://typst.app/docs/) for the full reference.
The preview of `main.typ` should show the heading and text.
Note
In an IDE, you might need to open the preview. For example, if you use VS Code with `Tinymist`, press `Ctrl` + `K` followed by `V` while the `main.typ` file is open.
The companion `example/typst.toml` is the template manifest:
typst.toml
```toml
[package]
name = "example"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
```
It gives our template the name `example`, the semantic version `0.1.0`, and defines the entrypoint as `main.typ`. The `tool.oicana` section only configures the Oicana manifest version for now.
To check that the manifest is well-formed, run `oicana validate` in the template directory. It will report any issues with `typst.toml` before you try to package or compile the template.
## Packaging
[Section titled “Packaging”](#packaging)
To compile the template through Oicana integrations, like shown in later chapters of this guide, it has to be packaged. Navigate into the template directory in a terminal and execute `oicana pack`. This will create a file called `example-0.1.0.zip`.
Complete code at the end of this chapter
main.typ
```typst
#set document(date: datetime.today())
= Hello
Edit `main.typ` to design your template.
```
typst.toml
```toml
[package]
name = "example"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
```
# Defining Inputs
> Learn how to define JSON and blob inputs for your Oicana template.
There are two types of inputs. A `json` input contains structured data while a `blob` input passes bytes and optionally metadata to the template. For example, in an invoice the items and customer data could be a `json` input and the company logo could be a `blob` input.
## The Oicana Typst package
[Section titled “The Oicana Typst package”](#the-oicana-typst-package)
Template inputs are configured in the manifest file `typst.toml`. The Oicana Typst package determines the current values of inputs.
Add the following to the top of your `main.typ` file to initialize the package:
Beginning of main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
```
The `setup` method reads the manifest file and resolves all inputs. It returns three values:
1. *input* - dictionary of resolved input values, keyed by input key
2. *oicana-image* - a utility method, that takes the key of a blob input and returns the input as an image
3. *oicana-config* - dictionary with compilation metadata
We only need the inputs for now. You can take a look at [the example templates](https://github.com/oicana/oicana-example-templates) to see the `oicana-image` helper in action.
Why the \`read-project-file\` wrapper?
The `setup` call needs access to the files in the template project. Due to how Typst handles paths, this currently requires passing a `read` closure from the template itself, as done with `read-project-file`.
## Defining inputs
[Section titled “Defining inputs”](#defining-inputs)
We will use a `json` input to pass a name into the template. Add the following to the end of the `typst.toml` file:
typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "info"
```
The value of this input is now available in the template as `input.info`, where `info` is the key of the input as defined in `typst.toml`.
While we develop the template, the value of the input will be `none`, because there is no Oicana integration setting a value for it. We can change that by defining a `default` or `development` value for the input.
### Default and Development values
[Section titled “Default and Development values”](#default-and-development-values)
Inputs can define two different fallback values, `default` and `development`. These fallback values differ in priority based on which mode the template is compiled in.
When compiling a template in development mode, input values have the priority
1. Explicit input value (for example through an integration)
2. `development` value
3. `default` value
If you compile in production mode, the `development` value is ignored:
1. Explicit input value (for example through an integration)
2. `default` value
When to use each mode
**Development mode** is used in two scenarios:
* When developing templates in a Typst editor, live previews will use the development mode. The same goes for other tooling without Oicana integration (like the official Typst CLI).
* By default, template registration (typically at application startup) uses development mode when warming up the Typst cache for the template to speed up later compilations.
**Production mode** is the default for document compilation in integrations. It ensures the compilation fails explicitly rather than accidentally using test data for the document. If an input value is missing in production mode and the input does not have a `default` value, the compilation will fail unless you marked the input as not required and your template handles `none` values for it.
While developing an Oicana template in a Typst editor, it will be compiled in development mode. It makes sense to define `development` values for all required inputs of your template to have a functioning preview.
Let’s extend our input with a `development` value. First create an `info.json` file in the template directory:
info.json
```json
{
"name": "Chuck Norris"
}
```
Then extend the input definition and set the `development` value to be `info.json`:
typst.toml
```toml
[[tool.oicana.inputs]]
type = "json"
key = "info"
development = "info.json"
```
In our template we can now use `input.info.name` and the preview will show “Chuck Norris”. Update your template to the following:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Hello from Typst, #input.info.name
Now we can pass names into the template from any Oicana integration.
```
## Compiling with the CLI
[Section titled “Compiling with the CLI”](#compiling-with-the-cli)
After changing the manifest, run `oicana validate` to confirm the new input definition is valid. If the JSON input has a schema, `validate` also checks that the `development` and `default` values match it.
Running `oicana compile --development` should create a PDF file in an `output` directory. To set the input value from the CLI, copy `info.json` to `test-input.json` and change the name in the new file. Then run `oicana compile -j info=test-input.json` and the new PDF file in `output` will contain whatever name you configured.
With the input defined in your template, you’re ready to choose an integration and learn how to pass dynamic values from your application code. In preparation for that, you should pack the template again using `oicana pack` to have the latest state of the template at hand.
Complete code at the end of this chapter
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Hello from Typst, #input.info.name
Now we can pass names into the template from any Oicana integration.
```
typst.toml
```toml
[package]
name = "example"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
[[tool.oicana.inputs]]
type = "json"
key = "info"
development = "info.json"
```
info.json
```json
{
"name": "Chuck Norris"
}
```
# Browser / React
> Integrate Oicana into a React application using WebAssembly.
In this chapter, you’ll integrate Oicana into a React application using WebAssembly. Unlike the server-side integrations, Oicana runs entirely in the browser here - no server required for PDF generation.
Note
This chapter assumes that you have a working Node.js 18+ setup with npm. If that is not the case, please follow [the official Node.js installation guide](https://nodejs.org/en/download/) to install Node.js on your machine.
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh React project using Vite. Run the following command:
```bash
npm create vite@latest my-pdf-app -- --template react-ts
cd my-pdf-app
npm install
```
You can test it by running `npm run dev` and navigating to in your browser.
## Adding Oicana
[Section titled “Adding Oicana”](#adding-oicana)
Install the Oicana browser packages:
```bash
npm install @oicana/browser @oicana/browser-wasm
```
Note
The WASM file included in `@oicana/browser-wasm` is about 40 MB uncompressed (\~17 MB gzipped, \~12 MB brotli). It’s fetched once and browser-cached. For more about handling the WASM file read the [Deploying the Browser WASM](/docs/guides/browser-deployment/) guide. It covers pre-compression, CDN caveats, Web Worker offload, and a few other things that are easy to get wrong.
1. Create a `public` directory (if it doesn’t exist) and copy `example-0.1.0.zip` into it. This makes the template accessible to the browser.
2. Replace the contents of `src/App.tsx` with the following:
src/App.tsx
```tsx
import { useEffect, useState } from 'react';
import { initialize, Template, CompilationMode, Pdf } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
function App() {
const [template, setTemplate] = useState(null);
useEffect(() => {
async function setup() {
await initialize(wasmUrl);
const response = await fetch('/example-0.1.0.zip');
const templateBytes = new Uint8Array(await response.arrayBuffer());
setTemplate(new Template(templateBytes));
}
void setup();
}, []);
function generatePdf() {
const pdf = template!.export(
new Map(),
new Map(),
Pdf,
CompilationMode.Development,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
}
if (!template) {
return
Template is being prepared...
;
}
return (
);
}
export default App;
```
Start the dev server with `npm run dev` and click the “Generate PDF” button. The generated `example.pdf` file should contain your template with the development value.
## About performance
[Section titled “About performance”](#about-performance)
PDF compilation is CPU-bound and runs on whichever thread calls it, so any non-trivial UI should run it from a Web Worker rather than the main thread. The [Deploying the Browser WASM](/docs/guides/browser-deployment/) guide covers the production setup, and the open source [React example project](https://github.com/oicana/oicana-example-typescript-react/) shows a complete shared worker setup.
## Passing inputs from TypeScript
[Section titled “Passing inputs from TypeScript”](#passing-inputs-from-typescript)
Our code currently compiles with empty inputs and development mode. This means Oicana will use the development value for the input. Now we’ll provide an explicit input value and switch to production mode. Update the `generatePdf` function:
Part of src/App.tsx
```tsx
function generatePdf() {
const jsonInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
const pdf = template!.export(
jsonInputs,
new Map(),
Pdf,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
}
```
*With this change, you need to remove the now unused `CompilationMode` import, or the build will fail.*
Your explicit input value takes precedence over the `development` value in either mode, so it is what changes the output here. Notice that we removed the explicit `CompilationMode.Development` parameter. The `export()` method defaults to `CompilationMode.Production`. Production mode is the recommended default - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values for inputs. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles `none` values for that input.
Clicking the button now will download a PDF with “Baby Yoda” instead of “Chuck Norris”. For a more complete example including Web Workers, take a look at the [open source React example project on GitHub](https://github.com/oicana/oicana-example-typescript-react/).
For inputs other than JSON, see [Template inputs](/docs/templates/inputs/), which documents blob inputs with examples for every integration.
## Handling errors
[Section titled “Handling errors”](#handling-errors)
The setup promise can reject. If it does, nothing catches it, so the UI shows “Template is being prepared…” forever and the only trace is a console error. Catching the rejection and keeping the reason in state can make the failure visible:
Part of src/App.tsx
```tsx
const [template, setTemplate] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
async function setup() {
await initialize(wasmUrl);
const response = await fetch('/example-0.1.0.zip');
if (!response.ok) {
throw new Error(`Failed to fetch the template: ${response.status}`);
}
const templateBytes = new Uint8Array(await response.arrayBuffer());
setTemplate(new Template(templateBytes));
}
setup().catch((cause) => setError(String(cause)));
}, []);
```
Compiling can fail too, most often because an input is missing or fails schema validation. Wrap the `export()` call in a `try`/`catch` that writes to the same error state, and add a branch that renders it. Without that branch the error is stored but never shown:
Part of src/App.tsx
```tsx
function generatePdf() {
const jsonInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
try {
const pdf = template!.export(
jsonInputs,
new Map(),
Pdf,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
} catch (cause) {
setError(String(cause));
}
}
if (error) {
return
Something went wrong: {error}
;
}
if (!template) {
return
Template is being prepared...
;
}
```
Note
Compilation errors carry the full Typst diagnostic, including the template source path and excerpt. That is useful while building the app, and fine when the user owns the template. If you ship a template that your users should not see the internals of, show a generic message.
Complete code at the end of this chapter
src/App.tsx
```tsx
import { useEffect, useState } from 'react';
import { initialize, Template, Pdf } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
function App() {
const [template, setTemplate] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
async function setup() {
await initialize(wasmUrl);
const response = await fetch('/example-0.1.0.zip');
if (!response.ok) {
throw new Error(`Failed to fetch the template: ${response.status}`);
}
const templateBytes = new Uint8Array(await response.arrayBuffer());
setTemplate(new Template(templateBytes));
}
setup().catch((cause) => setError(String(cause)));
}, []);
function generatePdf() {
const jsonInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
try {
const pdf = template!.export(
jsonInputs,
new Map(),
Pdf,
);
const blob = new Blob([pdf], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'example.pdf';
a.click();
URL.revokeObjectURL(url);
} catch (cause) {
setError(String(cause));
}
}
if (error) {
return
Something went wrong: {error}
;
}
if (!template) {
return
Template is being prepared...
;
}
return (
);
}
export default App;
```
# C# / ASP.NET
> Integrate Oicana into a C# web service using ASP.NET Core.
In this chapter, you’ll integrate Oicana into a C# web service using [ASP.NET Core](https://dotnet.microsoft.com/en-us/apps/aspnet). ASP.NET Core is Microsoft’s modern, cross-platform framework for building web applications and APIs. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working .NET 10 setup. If that is not the case, please follow [the official Microsoft guide](https://learn.microsoft.com/en-us/dotnet/core/install/) to install .NET on your machine.
Oicana works with older .NET versions. If you want to use an older version, the ASP.NET related code in this guide will differ a bit.
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh 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);
```
Note
Template registration (the `new Template()` call) compiles the template once in development mode to warm up the Typst cache. This happens at startup and uses the `development` value of the input you defined previously.
4. Replace the generated `/weatherforecast` endpoint with the following:
Part of Program.cs
```cs
app.MapPost("compile", () =>
{
var stream = template.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.
Note
`Template` is safe to share across requests and you can register it as a singleton. Compilations of the same `Template` instance serialize internally; for more concurrency, create multiple `Template` instances from the same template file.
Repeated compilations are fast because Typst memoizes its work in a global cache. The [cache management guide](/docs/guides/cache-management/) explains how that cache is evicted and when it pays off to tune it.
## Passing inputs from C\#
[Section titled “Passing inputs from C#”](#passing-inputs-from-c)
Our `compile` endpoint is currently calling the template’s `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"
);
});
```
Complete code at the end of this chapter
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);
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.MapPost("compile", (ILogger logger) =>
{
var jsonInputs = new Dictionary
{
["info"] = JsonNode.Parse("{ \"name\": \"Baby Yoda\" }")!
};
Stream stream;
try
{
stream = template.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"
);
});
app.Run();
```
# Java / Spring Boot
> Integrate Oicana into a Java web service using Spring Boot.
In this chapter, you’ll integrate Oicana into a Java web service using [Spring Boot](https://spring.io/projects/spring-boot). Spring Boot is a popular Java framework for building production-ready web services. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Java 17+ setup with Gradle. If that is not the case, please follow [the Adoptium installation guide](https://adoptium.net/) to install a JDK and [install Gradle.](https://gradle.org/install/) Make sure your Gradle version [supports your Java version.](https://docs.gradle.org/current/userguide/compatibility.html)
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh Spring Boot project. Create a new directory and initialize it with Gradle:
```bash
mkdir my-pdf-service
cd my-pdf-service
gradle init --type basic --dsl kotlin
```
Replace the generated `build.gradle.kts` with:
build.gradle.kts
```kotlin
plugins {
java
id("org.springframework.boot") version "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"
```
Note
Replace the `runtimeOnly` dependency with the native library for your platform. Available options: `oicana-linux-x86_64`, `oicana-linux-aarch64`, `oicana-macos-x86_64`, `oicana-macos-aarch64`, `oicana-windows-x86_64`. You can add multiple if your team uses different platforms - only the matching one will be loaded at runtime.
Create the main application class:
src/main/java/com/example/Application.java
```java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
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);
}
```
Complete code at the end of this chapter
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")
// Since this is an example project, we add all native implementations.
// In your project, only add what you need.
runtimeOnly("com.oicana:oicana-linux-x86_64:0.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")
}
```
settings.gradle.kts
```kotlin
rootProject.name = "my-pdf-service"
```
src/main/java/com/example/Application.java
```java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
src/main/java/com/example/TemplateService.java
```java
package com.example;
import com.oicana.Template;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.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(
Map.of("info", "{\"name\": \"Baby Yoda\"}"),
Map.of()
);
}
@PreDestroy
public void cleanup() {
template.close();
}
}
```
src/main/java/com/example/CompileController.java
```java
package com.example;
import com.oicana.OicanaException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CompileController {
private static final Logger log =
LoggerFactory.getLogger(CompileController.class);
private final TemplateService templateService;
public CompileController(TemplateService templateService) {
this.templateService = templateService;
}
@PostMapping("/compile")
public ResponseEntity compile() {
byte[] pdf;
try {
pdf = templateService.compile();
} catch (OicanaException exception) {
log.error("Failed to compile template", exception);
return ResponseEntity.internalServerError()
.contentType(MediaType.TEXT_PLAIN)
.body("Failed to generate the document".getBytes());
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"example.pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdf);
}
}
```
# Node.js / NestJS
> Integrate Oicana into a Node.js web service using NestJS.
In this chapter, you’ll integrate Oicana into a Node.js web service using [NestJS](https://nestjs.com/). NestJS is a progressive Node.js framework for building efficient, scalable server-side applications. It uses TypeScript by default and provides a modular architecture. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Node.js 18+ setup with npm. If that is not the case, please follow [the official Node.js installation guide](https://nodejs.org/en/download/) to install Node.js on your machine.
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh 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));
}
}
```
Complete code at the end of this chapter
src/templates/templates.service.ts
```typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Template } 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);
this.template = new Template(buffer);
}
compile(): Uint8Array {
const jsonInputs = new Map();
const blobInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
return this.template.export(jsonInputs, blobInputs);
}
}
```
src/templates/templates.controller.ts
```typescript
import {
Controller,
InternalServerErrorException,
Logger,
Post,
Res,
} from '@nestjs/common';
import type { Response } from 'express';
import { TemplatesService } from './templates.service';
@Controller('templates')
export class TemplatesController {
private readonly logger = new Logger(TemplatesController.name);
constructor(
private readonly templatesService: TemplatesService
) {}
@Post('compile')
compile(@Res() res: Response) {
let pdf: Uint8Array;
try {
pdf = this.templatesService.compile();
} catch (error) {
this.logger.error('Failed to compile template', error);
throw new InternalServerErrorException(
'Failed to generate the document',
);
}
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="example.pdf"',
'Content-Length': pdf.length,
});
res.status(200).end(Buffer.from(pdf));
}
}
```
# Rust / Axum
> Integrate Oicana into a Rust web service using axum.
In this chapter, you’ll integrate Oicana into a Rust web service using [axum](https://github.com/tokio-rs/axum). axum is a web application framework built on top of [Tokio](https://tokio.rs/) and [Tower](https://github.com/tower-rs/tower), designed for building fast, reliable HTTP services. We’ll create a simple async web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Rust setup with cargo. If that is not the case, please follow [the official Rust installation guide](https://www.rust-lang.org/tools/install) to install Rust on your machine.
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh Axum project. First, create a new binary project with `cargo 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.
Complete code at the end of this chapter
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"
```
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();
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();
}
};
let pdf = match export_pdf(
&result.document,
&*template,
template.manifest().pdf_standards(),
template.manifest().pdf_tagged(),
None,
) {
Ok(pdf) => pdf,
Err(error) => {
eprintln!("Failed to export PDF: {error}");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
};
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/pdf")
.header(
header::CONTENT_DISPOSITION,
"attachment; filename=\"example.pdf\"",
)
.body(Body::from(pdf))
.unwrap()
}
```
# Python / FastAPI
> Integrate Oicana into a Python web service using FastAPI.
In this chapter, you’ll integrate Oicana into a Python web service using [FastAPI](https://fastapi.tiangolo.com/). FastAPI is a modern, high-performance web framework for building APIs with Python based on standard Python type hints. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Python 3.9+ setup with uv. If that is not the case, please follow [the official Python installation guide](https://www.python.org/downloads/) and [get uv.](https://docs.astral.sh/uv/#installation)
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh FastAPI project. First, create a new directory for your project, then initialize it and install FastAPI with `uv init && uv add "fastapi[standard]"`. Replace the content of `main.py` with the following basic FastAPI application:
main.py
```python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
```
You can test it by running `uv run fastapi dev main.py` and navigating to in your browser.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Create a new directory in the Python project called `templates` and copy `example-0.1.0.zip` into that directory.
2. Add the [`oicana` PyPI package](https://pypi.org/project/oicana/) as a dependency with `uv add oicana`.
3. Update `main.py` to load the template at startup and add a compile endpoint:
main.py
```python
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import Response
from oicana import Template, CompilationMode
template: Template
@asynccontextmanager
async def lifespan(app: FastAPI):
global template
template_path = Path("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`.
Complete code at the end of this chapter
main.py
```python
import json
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from oicana import Template
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 = Template(template_bytes)
yield
app = FastAPI(lifespan=lifespan)
@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"
},
)
```
# PHP / Slim
> Integrate Oicana into a PHP web service using Slim.
In this chapter, you’ll integrate Oicana into a PHP web service using [Slim](https://www.slimframework.com/). Slim is a lightweight PHP micro-framework well-suited for building APIs and web services. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working PHP 8.3+ setup with Composer. If that is not the case, please follow [the official PHP installation guide](https://www.php.net/manual/en/install.php) and [the Composer installation guide](https://getcomposer.org/doc/00-intro.md) to set up your environment.
It also assumes you have the packed template `example-0.1.0.zip` from the previous chapters. If you don’t, run `oicana pack` in your template directory first.
Let’s start with a fresh Slim project. Create a new directory for your PHP project (separate from your template directory) and initialize it with Composer:
```bash
mkdir my-pdf-service
cd my-pdf-service
composer init
```
Install Slim with its PSR-7 implementation:
```bash
composer require slim/slim slim/psr7
```
Create an `index.php` file with the following basic Slim application:
index.php
```php
get('/', function (Request $request, Response $response) {
$response->getBody()->write('Hello World');
return $response;
});
$app->run();
```
You can test it by running `php -S localhost:8000 index.php` and navigating to in your browser. You should see “Hello World”.
## New service endpoint
[Section titled “New service endpoint”](#new-service-endpoint)
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
1. Create a new directory in the PHP project called `templates` and copy `example-0.1.0.zip` into that directory.
2. The following commands add the `oicana/oicana` Composer package as a dependency. The package is hosted on a custom Composer repository, so we need to register it, allow the Oicana installer plugin, and finally install the package:
```bash
composer config repositories.oicana composer https://composer.oicana.com
composer config allow-plugins.oicana/installer true
composer require oicana/oicana:^0.6.0
```
Note
The installer downloads the native extension binary into `vendor/` and writes a PHP ini file at `vendor/oicana/installer/php/oicana.ini`. At the end of the install output it prints the exact command to activate the extension, already filled in with your project’s absolute path. Copy that line and run it before starting PHP.
Lost the install output? Run `vendor/bin/oicana-env` to print the same command again. Verify the extension is loaded with `php -m | grep oicana`.
3. Update `index.php` to load the template at startup and 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
```
Note
PHP’s built-in development server (`php -S`) starts a new process for each request, so state like loaded templates is not preserved between requests. This is fine for quick testing, but for production deployments, use a long-running PHP application server like [RoadRunner](https://roadrunner.dev/). RoadRunner keeps your PHP worker process alive, so templates are loaded once at startup and stay in memory across all requests. See the [example project on GitHub](https://github.com/oicana/oicana-example-php-slim/) for a RoadRunner setup.
Test the endpoint with curl:
```bash
curl -X POST http://localhost:8000/compile --output example.pdf
```
The generated `example.pdf` file should contain your template with the development value.
## About performance
[Section titled “About performance”](#about-performance)
The PDF generation should not take longer than a couple of milliseconds. You can measure the compilation time with PHP’s built-in `microtime(true)` 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.
Complete code at the end of this chapter
index.php
```php
addErrorMiddleware(false, true, true);
$app->post('/compile', function (Request $request, Response $response) use ($template) {
try {
$pdf = $template->export(
jsonInputs: ['info' => ['name' => 'Baby Yoda']]
);
} catch (\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"');
});
$app->run();
```
# Choose Your Integration
> Pick the integration that matches your tech stack.
You’ve created a working Oicana template with a dynamic input! Now it’s time to integrate it into an application.
Have your template ready
All integration guides expect you to have the packed Oicana template `example-0.1.0.zip` ready, that we built in the previous sections.
## One Template, Multiple Platforms
[Section titled “One Template, Multiple Platforms”](#one-template-multiple-platforms)
A key strength of Oicana is that the exact same template works across all integrations. The `example-0.1.0.zip` file you created can be used in C#, Java, Node.js, Rust, Python, PHP, 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!
# Compare Oicana to other PDF generation tools
> Every tool below is good at something. These pages lay out the trade-offs honestly, so you can pick what fits your documents, your stack, and your volume.
## Specific tools
[Section titled “Specific tools”](#specific-tools)
[Gotenberg vs Oicana](/compare/gotenberg/)Docker plus Chromium document conversion vs in-process Typst compilation.
[DocRaptor vs Oicana](/compare/docraptor/)Metered HTML-to-PDF API vs flat-fee, self-hosted generation.
[PDFMonkey vs Oicana](/compare/pdfmonkey/)Hosted PDF API with document quotas vs unlimited documents in your app.
[Carbone vs Oicana](/compare/carbone/)Word and Excel templates vs Typst templates as code.
## Approaches
[Section titled “Approaches”](#approaches)
[Oicana vs HTML-to-PDF](/docs/comparisons/vs-html-to-pdf/)Puppeteer, wkhtmltopdf, WeasyPrint, and other browser-based tools.
[Oicana vs PDF libraries](/docs/comparisons/vs-pdf-libraries/)PDFKit, iText, and other libraries where layout is code.
[Oicana vs LaTeX](/docs/comparisons/vs-latex/)Two typesetting systems, decades apart.
[Oicana vs commercial services](/docs/comparisons/vs-commercial-services/)Metered PDF APIs with per-document pricing.
# Carbone alternative: templates as code, flat price
> Carbone fills Word and Excel templates with JSON. Oicana takes the developer route, Typst templates in Git, compiled inside your own application, with a flat license and unlimited documents.
## What Carbone does well
[Section titled “What Carbone does well”](#what-carbone-does-well)
[Carbone](https://carbone.io/) generates documents from office templates: you design in Word, Excel, or LibreOffice, and Carbone injects JSON data. It outputs many formats beyond PDF, offers SDKs for several languages, and can run as a cloud API or on-premise. There is also a source-available community edition. If non-developers design your documents in Word, Carbone is built exactly for that.
## Where Oicana takes a different route
[Section titled “Where Oicana takes a different route”](#where-oicana-takes-a-different-route)
* **Templates as code instead of binary office files.** Oicana templates are plain [Typst](https://typst.app/) text files. They diff cleanly in Git, get reviewed in pull requests, are covered by snapshot tests, and AI can help write them. A `.docx` template is a zipped binary: no meaningful diffs, no code review, no merge.
* **Flat fee instead of counted documents.** Carbone’s cloud plans meter documents: Essential is $29/month ($26 billed annually), Advanced $159/month, and additional documents beyond plan volume are invoiced separately. On-premise licenses start around $1,500/year. Oicana is a flat €19 to €199 per month per application, unlimited documents included.
* **In process instead of a document service.** Oicana is a library for Node.js, Python, Java, C#, Rust, and PHP, plus WebAssembly in the browser. No HTTP hop to a render service, and a warmed up template compiles in single digit milliseconds.
* **A typesetter instead of an office suite.** Typst is a modern typesetting system with precise control over layout, fonts, and page structure, including PDF/A, PDF/UA, and ZUGFeRD / Factur-X e-invoices. You are not limited to what a Word document can express.
## Side by side
[Section titled “Side by side”](#side-by-side)
| | Carbone | Oicana |
| ----------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| What it is | Document generator filling office templates with JSON | PDF templating library, in process |
| Templates | Word, Excel, LibreOffice files | Typst projects with declared JSON and image inputs |
| Template workflow | Designed in an office suite | Text files in Git: diffs, reviews, snapshot tests |
| Runs | Cloud API or on-premise service | Inside your app: Node.js, Python, Java, C#, Rust, PHP, browser |
| Output formats | PDF and many office formats | PDF (including PDF/A, PDF/UA, hybrid e-invoices), PNG, SVG |
| Pricing | Metered cloud plans from $29/month, extra documents invoiced; on-premise from about $1,500/year | Flat €19 to €199 per month per application, unlimited documents |
| Free option | Free cloud tier, source-available community edition | Free for non-commercial use, free 30-day commercial evaluation |
Claims and dates
Carbone pricing and features were checked on carbone.io and GitHub on July 3, 2026. If something is outdated, please tell us at .
## Which one should you pick?
[Section titled “Which one should you pick?”](#which-one-should-you-pick)
Pick Carbone if your templates are designed by non-developers in Word or Excel and that workflow works for your team. Pick Oicana if your developers own the documents: templates in version control, declared and validated inputs, generation inside your own services, and a price that does not grow with your document volume.
## Next steps
[Section titled “Next steps”](#next-steps)
[Getting started](/docs/getting-started/1-setup/)From template to compiled PDF in your stack, step by step.
[Oicana vs commercial services](/docs/comparisons/vs-commercial-services/)The broader comparison with metered PDF APIs.
# DocRaptor alternative: no per-document fees
> DocRaptor is a polished HTML-to-PDF API. If your document volume is growing or your data must stay on your servers, Oicana compiles Typst templates inside your own application, with a flat license and unlimited documents.
## What DocRaptor does well
[Section titled “What DocRaptor does well”](#what-docraptor-does-well)
[DocRaptor](https://docraptor.com/) is a hosted HTML-to-PDF API built on the PrinceXML engine, which is known for print-quality output from HTML and CSS. It offers SOC2 and HIPAA compliance, a 99.99% uptime guarantee, and a free plan with 5 production documents per month plus unlimited watermarked test documents. If you already have HTML and want a managed service with strong typography, DocRaptor is a solid choice.
## Where Oicana takes a different route
[Section titled “Where Oicana takes a different route”](#where-oicana-takes-a-different-route)
* **Flat fee instead of metered documents.** DocRaptor bills by volume: $15/month for 125 documents, $29 for 325, $75 for 1,250, $149 for 5,000, up to $1,000/month for 40,000. Oicana is a flat €19 to €199 per month per application, with unlimited documents. The more you generate, the bigger the gap.
* **Your infrastructure instead of theirs.** Every DocRaptor document is rendered on their servers, so your invoice and report data leaves your infrastructure. Oicana compiles PDFs inside your own application, which keeps GDPR and HIPAA compliance fully in your hands. With the browser integration, documents can even be created on your users’ devices.
* **In process instead of API round trips.** Oicana is a library for Node.js, Python, Java, C#, Rust, and PHP. A warmed up template compiles in single digit milliseconds, with no network hop, no rate limits, and no dependency on an external service being up.
* **Typesetting instead of CSS print rules.** Templates are plain [Typst](https://typst.app/) projects. Page breaks, headers, footers, and margins are first-class concepts, and templates live in Git next to your code.
## Side by side
[Section titled “Side by side”](#side-by-side)
| | DocRaptor | Oicana |
| ------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- |
| What it is | Hosted HTML-to-PDF API | PDF templating library, in process |
| Engine | PrinceXML | Typst compiler, native or WebAssembly |
| Templates | Your HTML and CSS | Typst projects with declared JSON and image inputs |
| Runs | On DocRaptor’s servers | Inside your app: Node.js, Python, Java, C#, Rust, PHP, browser |
| Data locality | Data sent to DocRaptor | Never leaves your infrastructure |
| Pricing | $15 to $1,000+ per month, metered by document count | Flat €19 to €199 per month per application, unlimited documents |
| Free option | 5 production documents/month, unlimited watermarked test documents | Free for non-commercial use, free 30-day commercial evaluation |
Claims and dates
DocRaptor pricing and features were checked on docraptor.com on July 3, 2026. If something is outdated, please tell us at .
## Which one should you pick?
[Section titled “Which one should you pick?”](#which-one-should-you-pick)
Pick DocRaptor if you already have polished HTML, want a fully managed service, and your volume stays comfortably inside a small plan. Pick Oicana when documents start as application data, volume is growing, or your data is not allowed to leave your servers.
## Next steps
[Section titled “Next steps”](#next-steps)
[Getting started](/docs/getting-started/1-setup/)From template to compiled PDF in your stack, step by step.
[Oicana vs commercial services](/docs/comparisons/vs-commercial-services/)The broader comparison with metered PDF APIs.
# Gotenberg alternative: PDF generation without Chromium
> Gotenberg is the standard for self-hosted document conversion. If you generate documents from data instead of converting existing files, Oicana compiles Typst templates in process, with no browser and no extra container.
## What Gotenberg does well
[Section titled “What Gotenberg does well”](#what-gotenberg-does-well)
[Gotenberg](https://gotenberg.dev/) is a free, MIT-licensed Docker API that converts documents to PDF. You send URLs, HTML, Markdown, or Office files via HTTP and get a PDF back. It uses Chromium for web content and LibreOffice for office documents, and it can also merge, split, and manipulate existing PDFs. With over 12,500 GitHub stars, it is the go-to choice for self-hosted document conversion.
If your job is converting files that already exist, Gotenberg is a great fit and Oicana is not a replacement. Oicana does not convert HTML, Office files, or web pages.
## Where Oicana takes a different route
[Section titled “Where Oicana takes a different route”](#where-oicana-takes-a-different-route)
Oicana is built for a different job: generating documents from application data. Invoices, reports, certificates, and statements start as data in your app, not as files. For that job, running a browser engine in a sidecar container adds cost you do not need to pay.
* **In process instead of over HTTP.** Oicana is a library for Node.js, Python, Java, C#, Rust, and PHP, plus WebAssembly for the browser. There is no extra service to deploy, scale, monitor, and secure.
* **A typesetter instead of a browser.** Templates are plain [Typst](https://typst.app/) projects. Page breaks, headers, footers, and margins are first-class concepts instead of CSS print quirks, and the compiler is a few megabytes instead of a Chromium install.
* **Milliseconds instead of page renders.** A warmed up template compiles a print-ready PDF in single digit milliseconds. Gotenberg’s Chromium engine supports at most 6 concurrent conversions per instance (its `--chromium-max-concurrency` flag), so sustained volume means scaling containers.
* **Declared inputs instead of string templating.** Templates declare their inputs (JSON, images) in a manifest, optionally with a schema to validate against. Your app passes structured data, not interpolated HTML.
## Side by side
[Section titled “Side by side”](#side-by-side)
| | Gotenberg | Oicana |
| ------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------ |
| What it is | Docker HTTP API for document conversion | PDF templating library, in process |
| Input | URLs, HTML, Markdown, Office files | Typst templates plus declared JSON and image inputs |
| Engine | Chromium and LibreOffice | Typst compiler, native or WebAssembly |
| Runs as | Separate container next to your app | Part of your app: Node.js, Python, Java, C#, Rust, PHP, browser |
| Concurrency | Up to 6 concurrent Chromium conversions per instance | Bound by your CPU |
| Browser (client-side) use | No | Yes, via WebAssembly |
| Data locality | Self-hosted | Self-hosted, or fully on the user’s device |
| License and price | MIT, free | Source-available; free for non-commercial use, flat fee per application commercially |
Claims and dates
Gotenberg facts were checked against the Gotenberg documentation and GitHub repository on July 3, 2026. If something is outdated, please tell us at .
## Which one should you pick?
[Section titled “Which one should you pick?”](#which-one-should-you-pick)
Pick Gotenberg when you convert existing files: web pages to PDF, Word documents to PDF, merging uploads. Pick Oicana when your documents start as data and you want them typeset from version-controlled templates, inside your own application, at volume, without a per-document fee.
## Next steps
[Section titled “Next steps”](#next-steps)
[Getting started](/docs/getting-started/1-setup/)From template to compiled PDF in your stack, step by step.
[Oicana vs HTML-to-PDF](/docs/comparisons/vs-html-to-pdf/)The broader comparison with browser-based PDF generation.
# PDFMonkey alternative: no document quotas
> PDFMonkey makes it easy to start generating PDFs from a dashboard. If your volume is growing or your data must stay on your servers, Oicana compiles Typst templates inside your own application, with a flat license and unlimited documents.
## What PDFMonkey does well
[Section titled “What PDFMonkey does well”](#what-pdfmonkey-does-well)
[PDFMonkey](https://www.pdfmonkey.io/) is a hosted PDF generation API with a template editor in its dashboard. It is easy to adopt and priced kindly at the low end: a free plan with 20 documents per month, a Starter plan at €5/month for 300 documents, and a 30-day trial. For a side project or an app that sends a handful of documents a week, that is hard to beat.
## Where Oicana takes a different route
[Section titled “Where Oicana takes a different route”](#where-oicana-takes-a-different-route)
* **Flat fee instead of quotas.** PDFMonkey meters every document: €15/month for 3,000, €60 for 5,000, €300 for 60,000, with per-document overages beyond that. Oicana is a flat €19 to €199 per month per application, with unlimited documents. Your invoice run should not show up in your tooling bill.
* **Your infrastructure instead of theirs.** PDFMonkey renders documents on its servers, so your customer data leaves your infrastructure on every render. Oicana compiles PDFs inside your own application, which keeps GDPR and HIPAA compliance fully in your hands.
* **Templates as code instead of dashboard templates.** Oicana templates are plain [Typst](https://typst.app/) text files. They live in Git next to your code, get reviewed in pull requests, are tested with snapshot tests, and AI can help write them. No copy-pasting between a web dashboard and your repository.
* **In process instead of API round trips.** Oicana is a library for Node.js, Python, Java, C#, Rust, and PHP. A warmed up template compiles in single digit milliseconds, with no network hop and no dependency on an external service being up.
## Side by side
[Section titled “Side by side”](#side-by-side)
| | PDFMonkey | Oicana |
| ------------- | --------------------------------------------------------------- | --------------------------------------------------------------- |
| What it is | Hosted PDF generation API with dashboard templates | PDF templating library, in process |
| Templates | Managed in the PDFMonkey dashboard | Typst projects in your Git repository |
| Runs | On PDFMonkey’s servers | Inside your app: Node.js, Python, Java, C#, Rust, PHP, browser |
| Data locality | Data sent to PDFMonkey | Never leaves your infrastructure |
| Pricing | Free 20 docs/month; €5 to €300 per month, metered with overages | Flat €19 to €199 per month per application, unlimited documents |
| Free option | 20 documents/month | Free for non-commercial use, free 30-day commercial evaluation |
Claims and dates
PDFMonkey pricing and features were checked on pdfmonkey.io on July 3, 2026. If something is outdated, please tell us at .
## Which one should you pick?
[Section titled “Which one should you pick?”](#which-one-should-you-pick)
Pick PDFMonkey if you want the fastest possible start, like editing templates in a dashboard, and your volume fits the small plans. Pick Oicana when documents are part of your product: templates in version control, generation inside your own services, and volume that should not be metered.
## Next steps
[Section titled “Next steps”](#next-steps)
[Getting started](/docs/getting-started/1-setup/)From template to compiled PDF in your stack, step by step.
[Oicana vs commercial services](/docs/comparisons/vs-commercial-services/)The broader comparison with metered PDF APIs.
# 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.
Note
The [open source React example](https://github.com/oicana/oicana-example-typescript-react/) ships a complete worker setup, including the compile-request message protocol and the bundler config to ship the worker as a separate chunk. It’s the fastest way to get a production-shaped baseline.
# Cache Management
> Understanding and configuring Oicana's template compilation cache.
Typst uses [comemo](https://github.com/typst/comemo), a memoized function cache. This significantly speeds up repeated compilations.
## How Cache Eviction Works
[Section titled “How Cache Eviction Works”](#how-cache-eviction-works)
The comemo cache is global and shared across all template instances. To prevent unbounded memory growth, Oicana provides configurable cache eviction based on an aging mechanism:
* Each cache entry has an age counter
* Age increases by 1 during each eviction call
* Age resets to 0 when the entry is accessed (during a template compilation)
* Entries with age ≥ max\_age are removed when running cache eviction
## Default Behavior
[Section titled “Default Behavior”](#default-behavior)
By default, Oicana integrations automatically evict the cache after each compilation with a maximum age of 10.
## Configuring Cache Eviction
[Section titled “Configuring Cache Eviction”](#configuring-cache-eviction)
All integrations provide two APIs for cache management: one to configure or disable automatic eviction after each compilation, and one to manually trigger cache eviction with a specific age threshold.
| Integration | Configure automatic eviction | Manual eviction |
| ----------------- | ------------------------------------------------------- | ------------------------------- |
| Browser / Node.js | `configureAutomaticCacheEviction(maxAge)` | `evictCache(maxAge)` |
| C# | `Configuration.ConfigureAutomaticCacheEviction(maxAge)` | `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.
Bun
Bun does not run on libuv; it implements Node-API on its own thread pool. The async methods still run off the main thread and keep the event loop free, but `UV_THREADPOOL_SIZE` has no effect there. Bun sizes its pool itself.
## Concurrency
[Section titled “Concurrency”](#concurrency)
Async calls are safe to run concurrently. Independent compilations and exports proceed in parallel across the pool, up to the thread count, and share the compilation cache. Fire several off with `Promise.all` when a request needs more than one document:
```typescript
const [invoice, receipt] = await Promise.all([
invoiceTemplate.exportPdfAsync(invoiceInputs, new Map()),
receiptTemplate.exportPdfAsync(receiptInputs, new Map()),
]);
```
## When to still reach for worker threads
[Section titled “When to still reach for worker threads”](#when-to-still-reach-for-worker-threads)
With the async API you rarely need worker threads. If you want to isolate compilation in a separate thread pool, or keep it off the pool your other libuv work depends on, a library like [piscina](https://github.com/piscinajs/piscina) can help
# Styled Inputs
> How to support styled and rich-text inputs in Oicana templates.
Document templates in applications often need to be customizable. For example, a user might want to customize the footer of a given document. A common requirement is that these customizations need to be styled. In the footer, the users might want multiple blocks of text in a grid and some bold or underlined sections. We can support this by passing Typst code into the template instead of plain text and using [`#eval`](https://typst.app/docs/reference/foundations/eval/) in the template to render the given Typst code as the footer.
If our users know Typst, we are done at this point. But users often don’t know Typst and might not be technical at all. In these cases, the input used to customize the template should likely be a WYSIWYG editor.
Currently, there is no production-ready WYSIWYG editor for Typst (though keep an eye on ). A well supported format for WYSIWYG editors is HTML. You can use a tool like pandoc to convert from the format your editor exports to Typst and then pass the generated Typst code into the template.
# ZUGFeRD and Factur-X e-invoices
> How to create a template for e-invoices and use it from Oicana integrations.
A ZUGFeRD / Factur-X e-invoice is a regular PDF with the structured invoice data embedded as XML, plus XMP metadata declaring which profile that XML follows. Oicana produces all three parts from a Typst template. You supply the invoice XML, and Oicana renders the PDF, embeds the XML, and writes the metadata.
This guide builds the smallest setup that still produces a valid e-invoice, so you have a working baseline to iterate on. The visible PDF is almost empty on purpose. The focus is the plumbing: exporting the right PDF standards, embedding the XML, and passing data from an integration.
Background reading
For the *why* behind ZUGFeRD / Factur-X, read the post [E-invoices with ZUGFeRD and Factur-X in Oicana](/news/2026-06-30-zugferd-factur-x-e-invoices/). The [`invoice_zugferd` example template](https://github.com/oicana/oicana-example-templates/tree/main/templates/invoice_zugferd) is a more complete version of the Oicana template we build here.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
This guide uses the [`oicana` CLI](/docs/cli/) to compile and pack the template, so [install it](/docs/cli/) first.
The file embedding and custom metadata is wrapped in the open source [`invoice-harness`](https://github.com/oicana/invoice-harness) package, which gives you a single `factur-x(...)` call. `invoice-harness` is not on the Typst package registry yet, so install it as a [local package](/docs/templates/dependencies/#local-packages) following [the instructions in its repository](https://github.com/oicana/invoice-harness). After that, `@local/invoice-harness:0.1.1` is importable from your template.
## The invoice XML
[Section titled “The invoice XML”](#the-invoice-xml)
A valid e-invoice needs a Cross Industry Invoice (CII) XML document that conforms to the profile you declare. Hand-writing one is error prone, so start from an official reference sample.
The `invoice-harness` repository ships one validated sample per profile under [`tests/invoices//factur-x.xml`](https://github.com/oicana/invoice-harness/tree/main/tests/invoices), taken from the [ZUGFeRD corpus](https://github.com/ZUGFeRD/corpus). We use the **EN 16931** profile, the European baseline most national mandates build on. Copy that sample into your template directory:
```plaintext
tests/invoices/en16931/factur-x.xml -> factur-x.xml
```
Profile and XML must match
The profile you pass in the template (`profiles.en16931`) has to match the profile the XML actually conforms to. Embedding an EN 16931 XML but declaring `MINIMUM` produces a file that validators reject.
## The manifest
[Section titled “The manifest”](#the-manifest)
The manifest wires up two things: the PDF standards and the inputs.
typst.toml
```toml
[package]
name = "minimal_e_invoice"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
[tool.oicana.export.pdf]
standards = ["ua-1", "a-3b"]
# Structured invoice fields drive the visible PDF.
[[tool.oicana.inputs]]
type = "json"
key = "invoice"
development = "invoice.json"
# The CII document that gets embedded into the PDF.
[[tool.oicana.inputs]]
type = "blob"
key = "zugferd"
required = false
development = { file = "factur-x.xml" }
```
The choices behind this manifest:
* **`standards = ["ua-1", "a-3b"]`**: `a-3b` is the archivable PDF/A profile that allows embedded files, which makes the file a valid e-invoice. Adding `ua-1` makes the same document [accessible (PDF/UA-1)](/news/2026-06-17-accessible-pdfs-pdf-ua/). Both build on PDF 1.7, so they combine. See [Export Formats](/docs/templates/export/#combining-standards).
* **`zugferd` is an optional `blob` input** with a `development` value. The sample XML lets the template compile on its own during development, while in production an integration passes a fresh XML per invoice. `required = false` means a missing XML is not an error: the template just skips the embedding and renders a plain PDF.
* **`invoice` is a `json` input** with a `development` value, so the editor preview has data to show. It feeds the human-readable side of the PDF.
Create the `invoice.json` development value next to the manifest:
invoice.json
```json
{
"id": "2026-0001",
"customer": "ACME Corp",
"total": "1190.00 EUR"
}
```
## The template
[Section titled “The template”](#the-template)
The template stays deliberately bare. It embeds the XML with one call and renders just enough to be a recognizable document.
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#import "@local/invoice-harness:0.1.1": *
#let read-project-file(path) = read(path, encoding: none)
#let (input, _, _) = setup(read-project-file)
#set document(title: "Invoice " + input.invoice.id, date: datetime.today())
#if input.zugferd != none {
factur-x(input.zugferd.bytes, profiles.en16931)
}
= Invoice #input.invoice.id
Billed to #input.invoice.customer.
*Total: #input.invoice.total*
```
What each part does:
* `factur-x(input.zugferd.bytes, profiles.en16931)` takes the bytes of the `zugferd` blob input, embeds them as the associated `factur-x.xml`, and declares the EN 16931 profile in the XMP. That call is the whole e-invoice machinery. It is guarded by `if input.zugferd != none` so the template still produces a plain PDF when no XML is passed.
* `set document(title: ...)` is required for PDF/UA-1. If you add images later, give each one `alt` text for the same reason.
* Everything below is ordinary Typst. Grow it into a real invoice layout at your own pace.
The visible PDF and the XML are independent here
In this minimal setup the visible text comes from the `invoice` JSON input while the embedded data is a fixed sample XML, so they do not describe the same invoice. That is fine for getting the pipeline working, but a real e-invoice is only correct when the human-readable PDF and the embedded XML match. The next step is to **generate the XML from the same data** you render, so changing one changes both.
## Preview and compile locally
[Section titled “Preview and compile locally”](#preview-and-compile-locally)
With the `oicana` CLI in the template directory:
```bash
oicana validate # check the manifest and that fallbacks fit their schemas
oicana compile --development # render a PDF into ./output using the development values
oicana pack # produce minimal_e_invoice-0.1.0.zip
```
## Validate the result
[Section titled “Validate the result”](#validate-the-result)
Let’s make sure the PDF created by `oicana compile --development` is a valid e-invoice.
Three things have to line up: PDF/A-3 conformance, the embedded XML, and the XMP metadata. A mistake in any of them makes a file that systems could reject, so validate every change with a tool. Here some options:
* **[portinvoice.com](https://www.portinvoice.com/)**: a free, vendor-neutral online validator. Upload the PDF and it checks the embedded XML against EN 16931 and reports the detected profile. Convenient while iterating.
* **[KoSIT validator](https://github.com/itplr-kosit/validator)**: the official German government reference validator. Run it locally when you need the authoritative verdict.
* **[Mustangproject](https://www.mustangproject.org/)**: open source, runs offline, and validates both the PDF/A side (via veraPDF) and the CII schema plus EN 16931 Schematron. This can be a good choice to run in CI.
* **[veraPDF](https://verapdf.org/)**: the industry-standard PDF/A validator if you only want to check the archival conformance of the PDF.
## Pass data from an integration
[Section titled “Pass data from an integration”](#pass-data-from-an-integration)
In an application you register the packed template once, then per request pass the invoice fields as the `invoice` JSON input and the XML bytes as the `zugferd` blob input.
* TS (Browser)
```typescript
import { initialize, Template, type 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.
Help us prioritize
If you are missing support for a specific tech stack, please reach out at !
For all integrations, you can find open source example applications on GitHub.
## 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.
Example
Take a look at the [example Oicana template `dependency`](https://github.com/oicana/oicana-example-templates/tree/main/templates/dependency). It uses the awesome [`cetz` package to draw a diagram](https://typst.app/universe/package/cetz/) based on an input.
### Local packages
[Section titled “Local packages”](#local-packages)
A locally installed package can have any namespace. A common one is `@local`, but feel free to use your company name or any other identifier. To install a local Typst package, you can use a community developed tool or manually copy files to the right place.
#### Typship
[Section titled “Typship”](#typship)
[Typship](https://github.com/jassielof/typship) is a tool for Typst package development and publishing. Its CLI can install local Typst packages for you. To install a package into the `@local` namespace, run `typship install local` in the package directory.
#### Manual
[Section titled “Manual”](#manual)
Installing a Typst package means that Typst can find it at `{data-dir}/typst/packages/{namespace}/{name}/{version}`. Here, `{data-dir}` is:
* `XDG_DATA_HOME` or `~/.local/share` on Linux
* `~/Library/Application Support` on macOS
* `%APPDATA%` on Windows
For example, on Linux:
1. Store a package in `~/.local/share/typst/packages/local/my-package/1.0.0`
2. Import all items from the package with `#import "@local/my-package:1.0.0": *` in a Typst document
For more information on packages, please refer to [Typst’s packages repository](https://github.com/typst/packages).
## Bundling at pack time
[Section titled “Bundling at pack time”](#bundling-at-pack-time)
When you run [`oicana pack`](/docs/cli/#package-a-template), the CLI scans every `.typ` file in your template (and recursively inside each resolved package) for `import` statements with package specs, and copies the matching package sources into the archive. The result is a self-contained zip. At runtime, integrations resolve imports from the bundle, not from your local Typst cache or registry.
Packages land at `.dependencies////` inside the archive. For example, a template that imports `@preview/cetz:0.4.2` and `@local/my-helpers:1.0.0` will contain `.dependencies/preview/cetz/0.4.2/` and `.dependencies/local/my-helpers/1.0.0/` next to the template’s own files.
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
```
Note
Tagging is always disabled when exporting only a subset of a document’s pages, because Typst cannot tag partial exports.
### 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.
Note
When a PNG export covers more than one page, the pages are merged into a single, vertically stacked image. Export pages individually (see below) if you need separate files.
Note
Typst’s PNG export is optimized for speed, not file size. Before you save the images or send them over the network, consider optimizing them.
To keep memory usage in check, a PNG export is limited to 256 million pixels by default, which is about 1 GB of memory. 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.
Example
Take a look at the [example Oicana template `fonts`](https://github.com/oicana/oicana-example-templates/tree/main/templates/fonts). It demonstrates how to include and use custom fonts.
# 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.
Note
On the first run, you’ll need to use `oicana test --update` to create the initial snapshot files. Subsequent test runs will compare against these snapshots.
Use `--update` or `-u` whenever changes in the snapshot files are expected and should be accepted.
The tests directory will be recursively searched for any test collection files in the form of `tests.toml`.
Example
The example templates [`table`](https://github.com/oicana/oicana-example-templates/tree/main/templates/table/tests) and [`invoice`](https://github.com/oicana/oicana-example-templates/tree/main/templates/invoice/tests) both define some simple snapshot tests.
## 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"
```
# Java and PHP support - upcoming beta
> Oicana adds Java and PHP integrations, joining browser, C#, Node.js, Rust, and Python. Getting started guides extended for new integrations.
It’s been a busy few months for Oicana. Here’s what’s new.
## New Integrations: Java and PHP
[Section titled “New Integrations: Java and PHP”](#new-integrations-java-and-php)
We’ve added two new integrations to the lineup: **Java** and **PHP**. Together with the existing browser, C#, Node.js, Rust, and Python integrations, Oicana now covers most of the major server-side ecosystems. Check out the [getting started documentation](/docs/getting-started/1-setup/) to try it out.
Just like the other integrations, the two new ones come with open source example applications. A small [Spring Boot Java application](https://github.com/oicana/oicana-example-java-spring-boot) and a [php service using Slim](https://github.com/oicana/oicana-example-php-slim) demonstrate backend services that can generate PDFs from templates and data with Oicana.
## Migrated Documentation
[Section titled “Migrated Documentation”](#migrated-documentation)
The docs have been migrated from Typst to MDX and included in this website. While we love Typst and still plan to eventually use it for the website and the online documentation, there were too many small issues in making the documentation look good in a responsive website. The Typst version of the documentation will at some point power the online documentation again.
## What’s Next: Beta and Pricing
[Section titled “What’s Next: Beta and Pricing”](#whats-next-beta-and-pricing)
Oicana is approaching **beta**. For the moment, no new integrations are planned, to focus on improving what we already have. The non-commercial license will stay and pricing for commercial licenses is work in progress. More details on that soon — stay tuned.
# Oicana is in public beta
> Oicana is now in public beta — a Typst-based PDF templating engine with integrations for browser, Node.js, C#, Java, Rust, Python, and PHP.
[Source available on GitHub](https://github.com/oicana/oicana)
Generating PDFs from application code is usually a pick-your-poison choice. HTML-to-PDF tools have fragile pagination and weak typography, PDF libraries put verbose layout code into your application, and SaaS PDF builders often come with proprietary template formats and vendor lock-in.
Oicana takes a different route. Templates are plain [Typst](https://typst.app/) projects. Your application passes typed inputs (JSON, images), and Oicana compiles a PDF.
* **Multi-platform**: the same template works across all integrations: browser, Node.js, C#, Java, Rust, Python, and PHP.
* **Powerful layouting**: templates use the full power of Typst, including its package ecosystem.
* **Performant**: PDFs can generate in single digit milliseconds with warmed-up templates in native integrations.
* **AI and version control ready**: templates are text files. They live next to your code, and AI can help write them.
* **Minimal vendor lock-in**: templates are plain Typst projects. The Typst compiler is open source.
## A quick taste
[Section titled “A quick taste”](#a-quick-taste)
Here’s a minimal Oicana template, a single Typst file with a manifest defining one JSON input:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Hello from Oicana, #input.info.name
```
typst.toml
```toml
[package]
name = "example"
version = "0.1.0"
entrypoint = "main.typ"
[tool.oicana]
manifest_version = 1
[[tool.oicana.inputs]]
type = "json"
key = "info"
```
[Typst](https://typst.app/) is a modern markup-based typesetting system. In `main.typ`, the `=` marks a heading and `#` switches to code mode. The first line imports the Typst Oicana package and the next two lines set it up. In the last line is the actual visual content of our document. `#input.info.name` interpolates the `name` field of the `info` input into the document.
Note
For more information on Typst, please [take a look at their documentation](https://typst.app/docs).
In `typst.toml`, we define metadata for the template like a name, entrypoint, and inputs. The example defines a single input of type `json` (see [the Oicana documentation on inputs](/docs/templates/inputs/)). If the input value is `{ "name": "Alice" }`, the content of our document will be a header reading “Hello from Oicana, Alice”.
### Template development
[Section titled “Template development”](#template-development)
There are good options for working on Typst documents like [the official webapp](https://typst.app/play/) and several IDE plugins. this is what it can look like to work on an Oicana template in VS Code using the [Tinymist Typst extension](https://marketplace.visualstudio.com/items?itemName=myriad-dreamin.tinymist) with syntax highlighting and live preview:
[](/_astro/oicana-template-development-in-vs-code.DFNVuU94.png)
You can find an open source collection of [example Oicana templates on GitHub](https://github.com/oicana/oicana-example-templates), including the certificate template shown above.
### Compile from your application
[Section titled “Compile from your application”](#compile-from-your-application)
If you install the Oicana CLI and run `oicana pack` in the template directory, it produces the file `example-0.1.0.zip` that any Oicana integration can compile. Passing `{ "name": "Alice" }` for the `info` input from your application looks like this in the different integrations (omitting some general boilerplate):
* TS (Browser)
```typescript
import { Template } from '@oicana/browser';
const templateFile = await fetch('/example-0.1.0.zip');
const template = new Template(new Uint8Array(await templateFile.arrayBuffer()));
const jsonInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Alice' }));
const pdf = template.compile(jsonInputs, new Map());
```
* C#
```csharp
using System.Text.Json.Nodes;
using Oicana;
using Oicana.Config;
using Oicana.Inputs;
var template = new Template(File.ReadAllBytes("example-0.1.0.zip"));
var jsonInputs = new Dictionary
{
["info"] = JsonNode.Parse("""{ "name": "Alice" }""")!,
};
var pdf = template.Compile(
jsonInputs,
new Dictionary(),
ExportFormat.Pdf(),
new CompilationOptions(CompilationMode.Production));
```
* Java
```java
import com.oicana.Template;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
byte[] templateBytes = Files.readAllBytes(Path.of("example-0.1.0.zip"));
try (var template = new Template(templateBytes)) {
byte[] pdf = template.compile(Map.of("info", "{\"name\":\"Alice\"}"), Map.of());
}
```
* TS (Node.js)
```typescript
import { readFile } from 'node:fs/promises';
import { Template } from '@oicana/node';
const template = new Template(await readFile('example-0.1.0.zip'));
const jsonInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Alice' }));
const pdf = template.compile(jsonInputs, new Map());
```
* Rust
```rust
use std::fs::File;
use oicana::Template;
use oicana_input::TemplateInputs;
use oicana_input::input::json::JsonInput;
let mut template = Template::init(File::open("example-0.1.0.zip")?)?;
let mut inputs = TemplateInputs::new();
inputs.with_input(JsonInput::new(
"info",
serde_json::json!({ "name": "Alice" }).to_string(),
));
let pdf = template.compile(inputs)?;
```
* Python
```python
import json
from pathlib import Path
from oicana import Template
template_bytes = Path("example-0.1.0.zip").read_bytes()
with Template(template_bytes) as template:
pdf = template.compile(
json_inputs={"info": json.dumps({"name": "Alice"})},
)
```
* PHP
```php
use Oicana\Template;
$template = new Template(file_get_contents('example-0.1.0.zip'));
try {
$pdf = $template->compile(
jsonInputs: ['info' => ['name' => 'Alice']],
);
} finally {
$template->cleanup();
}
```
One template, seven tech stacks including the browser. The [getting started guide](/docs/getting-started/1-setup/) walks through this end-to-end for your stack of choice.
## Pricing
[Section titled “Pricing”](#pricing)
**Non-commercial use is free**: Personal projects, hobby use, research, education, and use by charitable organizations, educational institutions, public research organizations, and government institutions are covered by the [PolyForm Noncommercial License](https://polyformproject.org/licenses/noncommercial/1.0.0).
Commercial licenses are available [on the homepage](/#pricing). Licenses are **per application**, priced by company size, and every subscription includes the CLI and all integrations. All prices include VAT.
* **Startup**: €19/month, for companies up to €2M yearly revenue
* **Scaleup**: €49/month, for companies up to €25M yearly revenue
* **Enterprise**: €99/month, for companies above €25M yearly revenue
Annual billing saves 15%. Every subscription comes with a **30-day money-back guarantee**. If Oicana doesn’t fit, email for a refund.
## What’s next
[Section titled “What’s next”](#whats-next)
Oicana was already used in production during the alpha and improved based on the learnings. The same idea goes for the beta. If something feels off, please let us know. That’s the feedback that shapes the road from beta to 1.0.
Give Oicana a try and get started with the [setup guide](/docs/getting-started/1-setup/).
# Accessibility law and your PDFs: creating PDF/UA with Oicana
> The European Accessibility Act and ADA-era rules affect the PDFs businesses generate. Here is what they require, where PDF/UA fits, and how Oicana exports PDF/UA-1 documents from Typst templates.
Accessibility reaches a lot further than websites. It includes the PDFs you generate for your customers. Invoices, statements, contracts, tickets, and reports are part of the service you offer, and services have to be accessible.
This post looks at what the **European Accessibility Act** and the US accessibility framework (**ADA** and **Section 508**) mean for businesses that produce PDFs, whether **PDF/UA** is actually required, and how Oicana exports PDF/UA-1 documents from [Typst](https://typst.app/) templates.
Note
This is an engineering overview, not legal advice. Whether and how these rules apply to your organization depends on your jurisdiction, size, and what you sell. Talk to qualified counsel before making compliance decisions.
## What the laws require
[Section titled “What the laws require”](#what-the-laws-require)
### European Accessibility Act (EAA)
[Section titled “European Accessibility Act (EAA)”](#european-accessibility-act-eaa)
The [European Accessibility Act](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32019L0882) (Directive (EU) 2019/882) [became enforceable on **28 June 2025**](https://eur-lex.europa.eu/EN/legal-content/summary/accessibility-of-products-and-services.html). It requires a broad set of products and services sold in the EU to be accessible to people with disabilities. It includes services like e-commerce, consumer banking, e-books, transport, or telecoms and applies regardless of where your company is based. If you sell into the EU market, you are in scope. Any customer facing PDFs that you generate are part of your service and need to be accessible.
Two important nuances:
* **[Microenterprise exemption (services)](https://eur-lex.europa.eu/EN/legal-content/summary/accessibility-of-products-and-services.html):** organizations with fewer than 10 employees *and* annual turnover below €2 million are generally exempt for the *services* parts of the EAA.
* **A transition window:** new products and services have to comply [from 28 June 2025](https://eur-lex.europa.eu/EN/legal-content/summary/accessibility-of-products-and-services.html). Many existing ones have until **28 June 2030**.
Penalties are set by each member state and can be severe. There can be fines, and in some jurisdictions removal of a product or service from the market.
### How the EAA defines “accessible”
[Section titled “How the EAA defines “accessible””](#how-the-eaa-defines-accessible)
The EAA itself contains no checklist. It points at the harmonized European standard **[EN 301 549](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/03.02.01_60/en_301549v030201p.pdf)**, which in turn incorporates the **[Web Content Accessibility Guidelines (WCAG) 2.1 Level AA](https://www.w3.org/TR/WCAG21/)** as the technical benchmark for digital content. So in practice, “accessible” means “meets WCAG 2.1 AA”. That applies to document content and to web pages.
### United States: ADA and Section 508
[Section titled “United States: ADA and Section 508”](#united-states-ada-and-section-508)
* **[Section 508](https://www.section508.gov/develop/applicability-conformance/)** of the Rehabilitation Act applies to US federal agencies and the vendors that sell to them. Its refresh [adopted **WCAG 2.0 Level AA**](https://www.section508.gov/develop/applicability-conformance/) as the technical baseline for electronic content, including PDF documents, and the standard is widely paired with **[PDF/UA](https://pdfa.org/resource/iso-14289-pdfua/)** to make PDFs conform.
* The **Americans with Disabilities Act (ADA)** applies to private “places of public accommodation”. The Department of Justice has stated that [the ADA’s requirements extend to the goods and services businesses offer on the web](https://www.ada.gov/resources/web-guidance/), pointing to WCAG as helpful guidance. In its [2024 Title II rule](https://www.ada.gov/resources/2024-03-08-web-rule/) DOJ went further and formally adopted **WCAG 2.1 AA** (including for documents) for state and local governments.
Across both continents: **WCAG is the legal benchmark and a PDF has to carry the structure a screen reader needs in order to satisfy it.**
## Is PDF/UA a requirement?
[Section titled “Is PDF/UA a requirement?”](#is-pdfua-a-requirement)
Short answer: **not literally, but it is the recognized way to get there for PDFs.**
[PDF/UA](https://pdfa.org/resource/iso-14289-pdfua/) (“Universal Accessibility”, ISO 14289) is the technical standard that defines *how* an accessible PDF must be built: a complete tag tree, a logical reading order, alternative text for images and formulas, a declared document language and title, and so on. The law mandates the *outcome* (WCAG conformance), and PDF/UA is the established, testable specification for achieving that outcome in the PDF format.
In other words:
**The EAA / [EN 301 549](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/03.02.01_60/en_301549v030201p.pdf) do not name PDF/UA as mandatory.** They require [WCAG 2.1 AA](https://www.w3.org/TR/WCAG21/). **[PDF/UA](https://pdfa.org/resource/iso-14289-pdfua/) is how the PDF industry implements those requirements.** A PDF/UA-conformant file carries exactly the tagging, metadata, and structure that WCAG asks for. Pairing PDF/UA conformance with WCAG is widely seen as the safest, most future-proof approach for any organization producing PDFs at scale.
So if you generate PDFs and want a clear, verifiable target, **“produce PDF/UA” is the practical goal**.
## Why generated PDFs are the hard part
[Section titled “Why generated PDFs are the hard part”](#why-generated-pdfs-are-the-hard-part)
A handful of brochures can be remediated by hand in Acrobat. But the documents that put a business in scope are usually the *generated* ones: thousands of invoices a day, per-customer statements, contracts assembled from data. You cannot manually tag those after the fact. Accessibility has to be produced **at generation time, by the template and the engine**, consistently, on every document.
This is where a lot of PDF tooling struggles. Headless-browser HTML-to-PDF pipelines produce weak or no tag trees; many imperative PDF libraries leave tagging entirely to you. Getting reliable, valid tags out of them is a project in itself.
## How Oicana and Typst help
[Section titled “How Oicana and Typst help”](#how-oicana-and-typst-help)
Oicana compiles [Typst](https://typst.app/) templates to PDF, and [Typst’s PDF export](https://typst.app/docs/reference/pdf/) was built with accessibility in mind. Two things matter here:
**1. Tagged PDFs by default.** Oicana [produces *tagged* (accessible) PDFs out of the box](/docs/templates/export/). The tag tree is the foundation everything else builds on. The semantic markup you already write in Typst (`= Heading`, lists, `figure`, tables) maps to the corresponding PDF structure elements automatically, so a well-structured template yields a well-structured document.
**2. One-line PDF/UA-1 export.** You opt into PDF/UA-1 directly in the template manifest:
typst.toml
```toml
[tool.oicana.export.pdf]
standards = ["ua-1"]
```
That is all the integration code needs to know. The same Typst template exports a PDF/UA-1 file whether you compile it from Node.js, Python, Java, Rust, C#, PHP, or the browser. ([Oicana’s default standard](/docs/templates/export/) is PDF/A-3b; PDF/A profiles such as `a-2a`/`a-3a` are also tagged and accessibility-oriented if you need PDF/A instead.)
### What you still have to provide
[Section titled “What you still have to provide”](#what-you-still-have-to-provide)
PDF/UA is a contract, and Typst holds you to it. Producing a valid PDF/UA-1 file [requires real accessibility metadata in the template](https://typst.app/docs/reference/pdf/), and if something is missing, the compile *fails* with a clear error rather than silently shipping a broken file. At minimum you need:
* a **document title** and a **document language**,
* **alternative text** on every image and formula,
* genuine **semantic structure** (headings, lists, tables) instead of visual fakes,
* **decorative** elements marked as artifacts so screen readers skip them.
In an Oicana template that looks like this:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, _) = setup(read-project-file)
// Required for PDF/UA-1: a title and a language
#set document(title: "Invoice " + input.invoice.number)
#set text(lang: "en")
= Invoice #input.invoice.number
// Images need alternative text
#figure(
oicana-image("logo", alt: "Acme Corporation logo"),
)
Billed to #input.invoice.customer
```
A valid PDF/UA file is not automatic legal compliance
Exporting PDF/UA-1 gives you a document that is *structurally* accessible and machine-checkable, which is a large, hard part of the job. It does not, on its own, guarantee WCAG 2.1 AA or legal compliance: the *content* still has to be right (meaningful alt text, sufficient color contrast, sensible reading order, accessible tables). Validate your output with a tool like the free [PAC (PDF Accessibility Checker)](https://pdfa.org/product/pac/) and review it with real assistive technology.
### Why the template-based approach fits compliance work
[Section titled “Why the template-based approach fits compliance work”](#why-the-template-based-approach-fits-compliance-work)
* **Accessibility lives in the template, not scattered across application code.** Flip one manifest setting and every document your service emits is PDF/UA-1, across all integrations and tech stacks.
* **Templates are plain text in version control.** Accessibility requirements, alt-text conventions, and the standard you target are reviewable and diffable like any other code.
* **Failures are loud.** A missing title or alt text breaks the build, so a regression cannot quietly ship a non-conformant invoice to a customer.
* **Built on open source.** Typst’s compiler and its PDF export are open source, so your accessible-document pipeline isn’t locked to a single vendor.
## Getting started
[Section titled “Getting started”](#getting-started)
If accessibility is on your roadmap, generating PDF/UA documents from data is one of the trickier pieces to get right. Oicana makes it a manifest setting on top of a templating system you can read, review, version, and use from many different tech stacks.
[Get started with Oicana](/docs/getting-started/1-setup/)[Read the export documentation](/docs/templates/export/)
# E-invoices with ZUGFeRD and Factur-X in Oicana
> ZUGFeRD and Factur-X put structured invoice data inside a PDF, so people and software can both understand the invoice. Here is why that matters and how Oicana builds one from a Typst template.
An invoice can have two readers. A person opens the PDF, checks the amount, and pays it. Or software extracts its data, so it can handle the invoice automatically. A regular PDF mostly serves the first reader and can make it difficult to trust automatic handling of invoices.
**ZUGFeRD** and **Factur-X** are standards used in Germany and France that solve this by putting both readers in one file: a normal PDF users can look at, with the structured invoice data embedded as XML for automation. This post explains why you would want or need that and how Oicana produces such a file from a [Typst](https://typst.app/) template.
## Why structured invoices
[Section titled “Why structured invoices”](#why-structured-invoices)
More and more countries require invoices to carry machine-readable data, not just a printed layout. The relevant ones for the standards we are looking at here are:
* **Germany** mandates that businesses can *receive* structured e-invoices for domestic B2B since **2025**, with required sending of e-invoices following in phases over the next years.
* **France** is rolling out a Factur-X based mandate on a similar timeline.
Even where it is not yet required, structured invoices save work. The recipient’s system reads the totals, tax rates, and line items straight from the file. No manual entry, no OCR guesswork, fewer mistakes.
## What ZUGFeRD / Factur-X is
[Section titled “What ZUGFeRD / Factur-X is”](#what-zugferd--factur-x-is)
Three things have to line up for a valid e-invoice:
1. A **PDF/A-3** document (the archivable, self-contained PDF profile).
2. The invoice **XML embedded** as an associated file.
3. **XMP metadata** in the PDF that declares the embedded XML and which profile it follows.
Get one of those wrong and validators can reject the file. This makes e-invoices fiddly to produce by hand.
## How Oicana builds one
[Section titled “How Oicana builds one”](#how-oicana-builds-one)
Oicana compiles a Typst template to PDF, and its PDF export can embed files and write custom XMP metadata. That is exactly what the three points above need.
The [`invoice_zugferd` example template](https://github.com/oicana/oicana-example-templates/tree/main/templates/invoice_zugferd) shows the full setup.
**1. Export to PDF/A-3 in the manifest.** You declare the standards once, next to the template:
typst.toml
```toml
[tool.oicana.export.pdf]
standards = ["ua-1", "a-3b"]
[[tool.oicana.inputs]]
type = "blob"
key = "zugferd"
default = { file = "factur-x.xml" }
```
Here the invoice XML comes in as a `blob` input, so you can pass a different one per invoice. The `a-3b` standard allows embedding files to be a valid e-invoice. Adding `ua-1` makes the same document [more accessible through PDF/UA-1](/news/2026-06-17-accessible-pdfs-pdf-ua/).
**2. Embed the XML in the template.** A single line takes the XML bytes, embeds them, and writes the matching XMP metadata:
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, oicana-image, _) = setup(read-project-file)
// Embed the invoice XML and declare it in the PDF metadata
#factur-x(input.zugferd.bytes, profiles.en16931)
// ... the rest of the template renders the visible invoice
```
The `factur-x` call handles the embedding and the XMP. The rest of the template is an ordinary invoice layout, free to look however you want.
Note
The `factur-x` helper lives in the [open source `invoice-harness`](https://github.com/oicana/invoice-harness) package. Embedding files and writing custom metadata currently only work through Oicana or a patched Typst, so `invoice-harness` is not published on the typst package registry yet. You can install it as a [local package](/docs/templates/dependencies/#local-packages). See [the e-invoice guide](/docs/guides/zugferd-factur-x/) to learn more.
## Getting started
[Section titled “Getting started”](#getting-started)
The Oicana documentation [includes a guide on creating e-invoices with one of our seven integrations](/docs/guides/zugferd-factur-x/). You can follow it see how to generate e-invoices from Node.js, the browser, Python, Rust, Java, PHP, or C#.
With Oicana it’s easy to create e-invoices in your code.
[Get started with Oicana](/docs/getting-started/1-setup/)
# Self-hosted PDF generation for your tech stack
> One template format, seven integrations. Templates are plain Typst projects and compile to print-ready PDFs inside your own application. No headless Chrome, no per-document fees, no data leaving your servers.
## Pick your integration
[Section titled “Pick your integration”](#pick-your-integration)
The same packed template works across all integrations. Each page below shows a complete example for one stack, from JSON input to finished PDF.
[Node.js](/pdf-generation/nodejs/)Generate PDF invoices in Node.js without Puppeteer.
[Python](/pdf-generation/python/)Generate PDF invoices in Python without a headless browser.
[Java](/pdf-generation/java/)Generate PDF invoices on the JVM without layout code.
[C#](/pdf-generation/csharp/)Generate PDF invoices in .NET without a headless browser.
[Rust](/pdf-generation/rust/)Generate PDF invoices in Rust with Typst templates.
[PHP](/pdf-generation/php/)Generate PDF invoices in PHP with a native extension.
[Browser](/pdf-generation/browser/)Generate PDFs on your users' devices with WebAssembly.
[Something else?](mailto:support@oicana.com)Tell us which integration you are missing. It helps us prioritize.
## How it compares
[Section titled “How it compares”](#how-it-compares)
[Gotenberg vs Oicana](/compare/gotenberg/)Docker plus Chromium document conversion vs in-process Typst compilation.
[All comparisons](/compare/)HTML-to-PDF, PDF libraries, LaTeX, and metered PDF APIs.
# Generate PDFs in the browser with WebAssembly
> Run the Typst compiler as WebAssembly and create print-ready PDFs directly on your users' devices. No server round-trip, no data egress, no PDF backend to operate.
## PDF generation without a server
[Section titled “PDF generation without a server”](#pdf-generation-without-a-server)
Client-side PDF generation usually means drawing every line yourself with a low-level library, or sending user data to a backend or third-party API. The first buries your document design in code, the second means the data leaves the device.
Oicana runs the Typst compiler as WebAssembly in the browser. Install with `npm install @oicana/browser @oicana/browser-wasm`, fetch a packed template, and compile documents from JSON:
```typescript
import { Template, initialize } from '@oicana/browser';
import wasmUrl from '@oicana/browser-wasm/oicana_browser_wasm_bg.wasm?url';
await initialize(wasmUrl);
const templateFile = await fetch('/invoice-0.1.0.zip');
const template = new Template(new Uint8Array(await templateFile.arrayBuffer()));
const jsonInputs = new Map();
jsonInputs.set('invoice', JSON.stringify({
number: '2026-001',
customer: 'Acme GmbH',
total: '€1,190.00',
}));
const pdf = template.export(jsonInputs, new Map());
```
## The template
[Section titled “The template”](#the-template)
The snippet loads `invoice-0.1.0.zip`, a packed Oicana template. Templates are plain [Typst](https://typst.app/) projects: a `typst.toml` manifest declares a JSON input named `invoice`, and `main.typ` uses it:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Invoice #input.invoice.number
Billed to: #input.invoice.customer
*Total: #input.invoice.total*
```
Running `oicana pack` in the template directory produces the zip. [Create a Template](/docs/getting-started/2-first-template/) walks through the manifest and input definitions, and the [open source example templates](https://github.com/oicana/oicana-example-templates) include a complete invoice.
## Running it in production
[Section titled “Running it in production”](#running-it-in-production)
Compilation is CPU-bound and runs on whichever thread calls it, so any non-trivial UI should compile in a Web Worker instead of the main thread. The WASM file is hosted as part of your frontend; bundlers can resolve its URL with a `?url` import. The [deployment guide](/docs/guides/browser-deployment/) covers the production setup.
The [live demo](https://example.oicana.com) runs this exact setup, and the [React example project](https://github.com/oicana/oicana-example-typescript-react/) shows the complete shared worker implementation.
## Why Oicana in the browser
[Section titled “Why Oicana in the browser”](#why-oicana-in-the-browser)
Zero data egress
Documents are created directly on your users’ devices. Sensitive data never leaves the browser, which is the strongest privacy setup possible.
No backend needed
No PDF service to build, deploy, and scale. Static hosting for the WASM file and templates is enough.
Flat price, unlimited documents
One flat license per application, no metering. A hundred invoices a month cost the same as a hundred thousand. Non-commercial use is free.
## Next steps
[Section titled “Next steps”](#next-steps)
[Browser / React getting started](/docs/getting-started/4-1-browser/)Build a React app that compiles your template to PDF, step by step.
[Deploying the browser WASM](/docs/guides/browser-deployment/)How to host the WebAssembly file and templates in production.
# Generate PDF invoices in C# without a headless browser
> One NuGet package compiles Typst templates to print-ready PDFs inside your ASP.NET service. No browser process, no layout code in C#, no per-document fees.
## PDF generation in .NET, in process
[Section titled “PDF generation in .NET, in process”](#pdf-generation-in-net-in-process)
In .NET, PDF generation usually means wrapping the abandoned wkhtmltopdf, running a headless browser next to your service, or writing every layout as code in a low-level library. All three make a simple invoice harder than it should be.
Oicana compiles PDFs in process through native bindings instead. Install with `dotnet add package Oicana`, load a packed template once, and compile documents from JSON:
```csharp
using System.Text.Json.Nodes;
using Oicana;
using Oicana.Config;
using Oicana.Inputs;
var template = new Template(File.ReadAllBytes("invoice-0.1.0.zip"));
var jsonInputs = new Dictionary
{
["invoice"] = JsonNode.Parse(
"""{ "number": "2026-001", "customer": "Acme GmbH", "total": "€1,190.00" }""")!,
};
var pdf = template.Export(
jsonInputs,
new Dictionary(),
ExportFormat.Pdf(),
new CompilationOptions(CompilationMode.Production));
```
A warmed up template produces a print-ready PDF in single digit milliseconds, directly in your service.
## The template
[Section titled “The template”](#the-template)
The snippet loads `invoice-0.1.0.zip`, a packed Oicana template. Templates are plain [Typst](https://typst.app/) projects: a `typst.toml` manifest declares a JSON input named `invoice`, and `main.typ` uses it:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Invoice #input.invoice.number
Billed to: #input.invoice.customer
*Total: #input.invoice.total*
```
Running `oicana pack` in the template directory produces the zip. [Create a Template](/docs/getting-started/2-first-template/) walks through the manifest and input definitions, and the [open source example templates](https://github.com/oicana/oicana-example-templates) include a complete invoice.
## Running it in production
[Section titled “Running it in production”](#running-it-in-production)
`Template` is safe to share across requests, so you can register it as a singleton in your dependency injection container. Compilations of one instance serialize internally; for more concurrency, create several `Template` instances from the same template file.
The [ASP.NET example project](https://github.com/oicana/oicana-example-csharp-asp-net/) shows a complete service.
## Why Oicana for C\#
[Section titled “Why Oicana for C#”](#why-oicana-for-c)
No headless browser
Native bindings compile PDFs inside your .NET service. No browser process to deploy, no wkhtmltopdf wrapper to maintain.
Your data stays with you
Documents are generated inside your own service. No customer data is sent to a third party, which keeps GDPR and HIPAA compliance in your hands.
Flat price, unlimited documents
One flat license per application, no metering. A hundred invoices a month cost the same as a hundred thousand. Non-commercial use is free.
## Next steps
[Section titled “Next steps”](#next-steps)
[C# / ASP.NET getting started](/docs/getting-started/4-2-csharp/)Build an ASP.NET service that compiles your template to PDF, step by step.
[Oicana vs HTML-to-PDF](/docs/comparisons/vs-html-to-pdf/)How Oicana compares to Puppeteer, wkhtmltopdf, and other HTML-to-PDF tools.
# Generate PDF invoices in Java without a headless browser
> Native JNI bindings compile Typst templates to print-ready PDFs inside your Spring Boot or plain JVM service. No layout code in Java, no browser process next to your deployment.
## PDF generation on the JVM, in process
[Section titled “PDF generation on the JVM, in process”](#pdf-generation-on-the-jvm-in-process)
On the JVM, PDF generation usually means low-level libraries where every table and page break is layout code, or rendering HTML in a headless browser next to your service. One buries the document design in Java, the other adds a heavy runtime dependency.
Oicana compiles PDFs in process through a native JNI interface instead. Add the `com.oicana:oicana` Maven package plus the native artifact for your platform, load a packed template once, and compile documents from JSON:
```java
import com.oicana.Template;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
byte[] templateBytes = Files.readAllBytes(Path.of("invoice-0.1.0.zip"));
try (var template = new Template(templateBytes)) {
byte[] pdf = template.export(
Map.of("invoice", """
{ "number": "2026-001", "customer": "Acme GmbH", "total": "€1,190.00" }
"""),
Map.of());
}
```
A warmed up template produces a print-ready PDF in single digit milliseconds, directly in your service.
## The template
[Section titled “The template”](#the-template)
The snippet loads `invoice-0.1.0.zip`, a packed Oicana template. Templates are plain [Typst](https://typst.app/) projects: a `typst.toml` manifest declares a JSON input named `invoice`, and `main.typ` uses it:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Invoice #input.invoice.number
Billed to: #input.invoice.customer
*Total: #input.invoice.total*
```
Running `oicana pack` in the template directory produces the zip. [Create a Template](/docs/getting-started/2-first-template/) walks through the manifest and input definitions, and the [open source example templates](https://github.com/oicana/oicana-example-templates) include a complete invoice.
## Running it in production
[Section titled “Running it in production”](#running-it-in-production)
The `Template` instance is thread-safe and can be shared across requests, which fits Spring Boot’s singleton service scope naturally. Next to the main `com.oicana:oicana` package you add a native runtime artifact per target platform; you can declare several, and the matching one is loaded at runtime.
The [Spring Boot example project](https://github.com/oicana/oicana-example-java-spring-boot/) shows a complete service.
## Why Oicana for Java
[Section titled “Why Oicana for Java”](#why-oicana-for-java)
No headless browser
JNI bindings compile PDFs on the JVM. No browser process next to your service, no document rendering over HTTP.
Your data stays with you
Documents are generated inside your own service. No customer data is sent to a third party, which keeps GDPR and HIPAA compliance in your hands.
Flat price, unlimited documents
One flat license per application, no metering. A hundred invoices a month cost the same as a hundred thousand. Non-commercial use is free.
## Next steps
[Section titled “Next steps”](#next-steps)
[Java / Spring Boot getting started](/docs/getting-started/4-3-java/)Build a Spring Boot service that compiles your template to PDF, step by step.
[Oicana vs PDF libraries](/docs/comparisons/vs-pdf-libraries/)How Oicana compares to low-level PDF libraries with layout code.
# Generate PDF invoices in Node.js without Puppeteer
> Compile print-ready PDFs inside your Node.js service with native bindings. Typst templates instead of HTML, declared JSON inputs instead of string interpolation, milliseconds instead of browser renders.
## PDF generation in Node.js, in process
[Section titled “PDF generation in Node.js, in process”](#pdf-generation-in-nodejs-in-process)
The default answer in Node.js is Puppeteer: render HTML in headless Chrome and print it to PDF. That means shipping a full browser with your service, hundreds of megabytes of memory, slow cold starts, and CSS that was never designed for paged documents.
Oicana compiles PDFs in process through native bindings instead. Install with `npm install @oicana/node`, load a packed template once, and compile documents from JSON:
```typescript
import { readFile } from 'node:fs/promises';
import { Template } from '@oicana/node';
const template = new Template(await readFile('invoice-0.1.0.zip'));
const jsonInputs = new Map();
jsonInputs.set('invoice', JSON.stringify({
number: '2026-001',
customer: 'Acme GmbH',
total: '€1,190.00',
}));
const pdf = template.export(jsonInputs, new Map());
```
A warmed up template produces a print-ready PDF in single digit milliseconds, directly in your service.
## The template
[Section titled “The template”](#the-template)
The snippet loads `invoice-0.1.0.zip`, a packed Oicana template. Templates are plain [Typst](https://typst.app/) projects: a `typst.toml` manifest declares a JSON input named `invoice`, and `main.typ` uses it:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Invoice #input.invoice.number
Billed to: #input.invoice.customer
*Total: #input.invoice.total*
```
Running `oicana pack` in the template directory produces the zip. [Create a Template](/docs/getting-started/2-first-template/) walks through the manifest and input definitions, and the [open source example templates](https://github.com/oicana/oicana-example-templates) include a complete invoice.
## Running it in production
[Section titled “Running it in production”](#running-it-in-production)
Load the template once at service startup and compile per request; after that there is no file I/O on the hot path. Compilation is CPU-bound, so every method has an async variant (`exportPdfAsync`, `compileAsync`, and friends) that runs on Node.js’ libuv thread pool and keeps the event loop free under load. The [async Node.js compilation guide](/docs/guides/nodejs-async/) shows the async methods and how to size the thread pool.
The [NestJS example project](https://github.com/oicana/oicana-example-typescript-nestjs/) shows a complete service, including blob inputs, error handling, and Swagger documentation.
## Why Oicana for Node.js
[Section titled “Why Oicana for Node.js”](#why-oicana-for-nodejs)
No headless browser
Native bindings compile PDFs inside your service. No Chromium download in your Docker image, no browser pool to babysit.
Your data stays with you
Documents are generated inside your own service. No customer data is sent to a third party, which keeps GDPR and HIPAA compliance in your hands.
Flat price, unlimited documents
One flat license per application, no metering. A hundred invoices a month cost the same as a hundred thousand. Non-commercial use is free.
## Next steps
[Section titled “Next steps”](#next-steps)
[Node.js / NestJS getting started](/docs/getting-started/4-4-nodejs/)Build a NestJS service that compiles your template to PDF, step by step.
[Oicana vs HTML-to-PDF](/docs/comparisons/vs-html-to-pdf/)How Oicana compares to Puppeteer, wkhtmltopdf, and other HTML-to-PDF tools.
# Generate PDF invoices in PHP without a headless browser
> A native extension compiles Typst templates to print-ready PDFs inside your PHP application. No shelling out to binaries, no browser process, no per-document fees.
## PDF generation in PHP, in process
[Section titled “PDF generation in PHP, in process”](#pdf-generation-in-php-in-process)
PHP teams generating PDFs usually pick between HTML-to-PDF libraries that struggle with page breaks and fonts, the abandoned wkhtmltopdf, or running a headless browser next to the application.
Oicana compiles PDFs in process through a native PHP extension instead. Install the `oicana/oicana` Composer package, load a packed template once, and compile documents from JSON:
```php
use Oicana\Template;
$template = new Template(file_get_contents('invoice-0.1.0.zip'));
try {
$pdf = $template->export(
jsonInputs: ['invoice' => [
'number' => '2026-001',
'customer' => 'Acme GmbH',
'total' => '€1,190.00',
]],
);
} finally {
$template->cleanup();
}
```
A warmed up template produces a print-ready PDF in single digit milliseconds, directly in your application.
## The template
[Section titled “The template”](#the-template)
The snippet loads `invoice-0.1.0.zip`, a packed Oicana template. Templates are plain [Typst](https://typst.app/) projects: a `typst.toml` manifest declares a JSON input named `invoice`, and `main.typ` uses it:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Invoice #input.invoice.number
Billed to: #input.invoice.customer
*Total: #input.invoice.total*
```
Running `oicana pack` in the template directory produces the zip. [Create a Template](/docs/getting-started/2-first-template/) walks through the manifest and input definitions, and the [open source example templates](https://github.com/oicana/oicana-example-templates) include a complete invoice.
## Running it in production
[Section titled “Running it in production”](#running-it-in-production)
The Composer package installs a native extension through an installer plugin: register the `composer.oicana.com` repository, allow the plugin, and require `oicana/oicana`. The installer prints the exact command to activate the extension, and `vendor/bin/oicana-env` reprints it anytime.
For production, use a long-running application server like RoadRunner. It keeps your PHP worker alive, so templates are loaded once and stay in memory across requests. The [Slim example project](https://github.com/oicana/oicana-example-php-slim/) includes a RoadRunner setup, blob inputs, error handling, and request validation.
## Why Oicana for PHP
[Section titled “Why Oicana for PHP”](#why-oicana-for-php)
No headless browser
A native extension compiles PDFs inside PHP. No shelling out to wkhtmltopdf, no browser process to deploy.
Your data stays with you
Documents are generated inside your own application. No customer data is sent to a third party, which keeps GDPR and HIPAA compliance in your hands.
Flat price, unlimited documents
One flat license per application, no metering. A hundred invoices a month cost the same as a hundred thousand. Non-commercial use is free.
## Next steps
[Section titled “Next steps”](#next-steps)
[PHP / Slim getting started](/docs/getting-started/4-7-php/)Build a Slim service that compiles your template to PDF, step by step.
[Oicana vs HTML-to-PDF](/docs/comparisons/vs-html-to-pdf/)How Oicana compares to Puppeteer, wkhtmltopdf, and other HTML-to-PDF tools.
# Generate PDF invoices in Python without a headless browser
> Native PDF compilation for FastAPI, Django, or any Python service. Design documents as Typst templates, pass JSON, and get print-ready PDFs back in milliseconds.
## PDF generation in Python, in process
[Section titled “PDF generation in Python, in process”](#pdf-generation-in-python-in-process)
Python teams generating PDFs usually pick between a headless browser, HTML-to-PDF engines that fight page breaks and fonts, or low-level libraries that turn every layout into code. The abandoned wkhtmltopdf still shows up in production stacks.
Oicana compiles PDFs in process through native bindings instead. Install with `pip install oicana`, load a packed template once, and compile documents from JSON:
```python
import json
from pathlib import Path
from oicana import Template
template_bytes = Path("invoice-0.1.0.zip").read_bytes()
with Template(template_bytes) as template:
pdf = template.export(
json_inputs={"invoice": json.dumps({
"number": "2026-001",
"customer": "Acme GmbH",
"total": "€1,190.00",
})},
)
```
A warmed up template produces a print-ready PDF in single digit milliseconds, directly in your service.
## The template
[Section titled “The template”](#the-template)
The snippet loads `invoice-0.1.0.zip`, a packed Oicana template. Templates are plain [Typst](https://typst.app/) projects: a `typst.toml` manifest declares a JSON input named `invoice`, and `main.typ` uses it:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Invoice #input.invoice.number
Billed to: #input.invoice.customer
*Total: #input.invoice.total*
```
Running `oicana pack` in the template directory produces the zip. [Create a Template](/docs/getting-started/2-first-template/) walks through the manifest and input definitions, and the [open source example templates](https://github.com/oicana/oicana-example-templates) include a complete invoice.
## Running it in production
[Section titled “Running it in production”](#running-it-in-production)
Load the template once at startup and compile per request. The compilation runs in native code, so a couple of milliseconds per document is the norm. Oicana releases the GIL while it compiles and exports, so multiple threads compile in parallel. For heavy load, you can combine that with multiple worker processes behind Gunicorn or another server.
The [FastAPI example project](https://github.com/oicana/oicana-example-python-fastapi/) shows a complete service, including blob inputs and error handling.
## Why Oicana for Python
[Section titled “Why Oicana for Python”](#why-oicana-for-python)
No headless browser
Native extensions compile PDFs inside your service. No headless Chrome to deploy, no fragile wkhtmltopdf binary to maintain.
Your data stays with you
Documents are generated inside your own service. No customer data is sent to a third party, which keeps GDPR and HIPAA compliance in your hands.
Flat price, unlimited documents
One flat license per application, no metering. A hundred invoices a month cost the same as a hundred thousand. Non-commercial use is free.
## Next steps
[Section titled “Next steps”](#next-steps)
[Python / FastAPI getting started](/docs/getting-started/4-6-python/)Build a FastAPI service that compiles your template to PDF, step by step.
[Oicana vs HTML-to-PDF](/docs/comparisons/vs-html-to-pdf/)How Oicana compares to Puppeteer, wkhtmltopdf, and other HTML-to-PDF tools.
# Generate PDF invoices in Rust with Typst templates
> Oicana is written in Rust, and the oicana crate is its most direct integration. Compile Typst templates to print-ready PDFs inside your service, with no subprocess and no browser.
## PDF generation in Rust, no detours
[Section titled “PDF generation in Rust, no detours”](#pdf-generation-in-rust-no-detours)
Rust has solid low-level PDF crates, but hand-coding every table and page break gets old fast. And bundling a headless browser defeats the point of a lean Rust service.
Oicana is written in Rust, and the `oicana` crate is the most direct way to use it. Add it with `cargo add oicana`, load a packed template once, and compile documents from JSON:
```rust
use std::fs::File;
use oicana::Template;
use oicana::export::pdf::export_pdf;
use oicana::input::input::json::JsonInput;
use oicana::input::{CompilationConfig, TemplateInputs};
let mut template = Template::init(File::open("invoice-0.1.0.zip")?)?;
let mut inputs = TemplateInputs::new();
inputs.with_config(CompilationConfig::production());
inputs.with_input(JsonInput::new(
"invoice",
serde_json::json!({
"number": "2026-001",
"customer": "Acme GmbH",
"total": "€1,190.00"
})
.to_string(),
));
let document = template.compile(inputs)?;
let pdf = export_pdf(
&document.document,
&template,
template.manifest().pdf_standards(),
template.manifest().pdf_tagged(),
None,
)?;
```
A warmed up template produces a print-ready PDF in single digit milliseconds, directly in your service.
## The template
[Section titled “The template”](#the-template)
The snippet loads `invoice-0.1.0.zip`, a packed Oicana template. Templates are plain [Typst](https://typst.app/) projects: a `typst.toml` manifest declares a JSON input named `invoice`, and `main.typ` uses it:
main.typ
```typst
#import "@preview/oicana:0.2.0": setup
#let read-project-file(path) = read(path, encoding: none)
#let (input, oicana-image, oicana-config) = setup(read-project-file)
#set document(date: datetime.today())
= Invoice #input.invoice.number
Billed to: #input.invoice.customer
*Total: #input.invoice.total*
```
Running `oicana pack` in the template directory produces the zip. [Create a Template](/docs/getting-started/2-first-template/) walks through the manifest and input definitions, and the [open source example templates](https://github.com/oicana/oicana-example-templates) include a complete invoice.
## Running it in production
[Section titled “Running it in production”](#running-it-in-production)
Load the template once at startup and share it across handlers, for example via `Arc` in your Axum state; after that there is no file I/O on the hot path. For managing multiple templates, the example project demonstrates thread-safe template caching with a `DashMap`.
The [Axum example project](https://github.com/oicana/oicana-example-rust-axum/) shows a complete service, including blob inputs, error handling, and OpenAPI documentation.
## Why Oicana for Rust
[Section titled “Why Oicana for Rust”](#why-oicana-for-rust)
No headless browser
Rust all the way down. The compiler runs in your own process: no subprocess, no browser, no document service to operate.
Your data stays with you
Documents are generated inside your own service. No customer data is sent to a third party, which keeps GDPR and HIPAA compliance in your hands.
Flat price, unlimited documents
One flat license per application, no metering. A hundred invoices a month cost the same as a hundred thousand. Non-commercial use is free.
## Next steps
[Section titled “Next steps”](#next-steps)
[Rust / Axum getting started](/docs/getting-started/4-5-rust/)Build an Axum service that compiles your template to PDF, step by step.
[Oicana vs PDF libraries](/docs/comparisons/vs-pdf-libraries/)How Oicana compares to low-level PDF libraries with layout code.
# Imprint
> Legal notice and provider identification for the Oicana website.
**Last updated: 03.05.2026**
Information pursuant to Art. 3:15d of the Dutch Civil Code (Burgerlijk Wetboek).
***
## Provider
[Section titled “Provider”](#provider)
| | |
| ---------------- | --------------------------------------------- |
| **Legal name** | Niklas Martin Eicker, trading as Oicana |
| **Legal form** | Eenmanszaak (sole proprietorship) |
| **Address** | Smidspad 27, 1506 MW Zaandam, the Netherlands |
| **Email** | |
| **KVK number** | 42036325 |
| **BTW-id (VAT)** | NL005445995B24 |
***
## EU Online Dispute Resolution
[Section titled “EU Online Dispute Resolution”](#eu-online-dispute-resolution)
The European Commission provides a platform for online dispute resolution (ODR), available at .
Our email address for consumer complaints is ****.
***
## Consumer Dispute Resolution
[Section titled “Consumer Dispute Resolution”](#consumer-dispute-resolution)
We are neither obliged nor willing to participate in any consumer dispute resolution proceedings under Dutch or EU law.
***
## Liability for Content
[Section titled “Liability for Content”](#liability-for-content)
We are responsible for our own content on these pages in accordance with applicable Dutch and EU law. We are not obligated to monitor transmitted or stored third-party information or to investigate circumstances that indicate illegal activity, beyond the obligations imposed by the EU Digital Services Act (Regulation (EU) 2022/2065) and its Dutch implementation.
Obligations to remove or block the use of information under general laws remain unaffected. Liability in this regard is only possible from the point in time at which a concrete infringement of the law becomes known. Upon becoming aware of such infringements, we will remove this content immediately.
***
## Liability for Links
[Section titled “Liability for Links”](#liability-for-links)
Our website contains links to external third-party websites over whose content we have no influence. We therefore cannot assume any liability for this external content. The respective provider or operator of the linked pages is always responsible for their content. The linked pages were checked for possible legal violations at the time of linking. Illegal content was not recognizable at the time of linking.
Permanent monitoring of the content of linked pages is not reasonable without concrete evidence of a legal violation. Upon becoming aware of legal violations, we will remove such links immediately.
***
## Copyright
[Section titled “Copyright”](#copyright)
The content and works created by the site operator on these pages are subject to copyright law. Duplication, processing, distribution, and any form of commercialization of such material beyond the scope of the copyright law shall require the prior written consent of the respective author or creator.
***
## Related Documents
[Section titled “Related Documents”](#related-documents)
* [Privacy Policy](/privacy-policy/): how we process personal data.
* [Terms of Service](/terms-of-service/): governs your commercial license to the Oicana software.
* [Refund Policy](/refund-policy/): refund procedure for commercial license purchases.
# Privacy Policy
> Privacy Policy for the Oicana website and the purchase of commercial licenses.
**Last updated: 03.05.2026**
This Privacy Policy explains how Niklas Martin Eicker, trading as Oicana (“Oicana,” “we,” “us,” or “our”), collects and processes personal data in connection with our website at and the purchase of commercial licenses for the Oicana software.
***
## 1. Who We Are
[Section titled “1. Who We Are”](#1-who-we-are)
We are the data controller for the processing activities described in this policy.
| | |
| -------------- | --------------------------------------------- |
| **Legal name** | Niklas Martin Eicker, trading as Oicana |
| **Legal form** | Eenmanszaak (sole proprietorship) |
| **Address** | Smidspad 27, 1506 MW Zaandam, the Netherlands |
| **Email** | |
| **KVK number** | 42036325 |
| **BTW-id** | NL005445995B24 |
Given the nature and scale of our data processing activities, we are not required to appoint a Data Protection Officer. For any privacy-related inquiries, please contact us at .
***
## 2. What Our Software Does NOT Collect
[Section titled “2. What Our Software Does NOT Collect”](#2-what-our-software-does-not-collect)
The Oicana software libraries run entirely within your own infrastructure. **The software does not collect, transmit, store, or process any personal data.** There is no telemetry, no analytics, no crash reporting, no license verification, and no network calls made by the software to Oicana or any third party.
This Privacy Policy covers only our website and the commercial license purchase process, not the software itself.
***
## 3. What Personal Data We Process
[Section titled “3. What Personal Data We Process”](#3-what-personal-data-we-process)
We process personal data only in the limited situations described below.
### 3.1 Website Hosting (Server Logs)
[Section titled “3.1 Website Hosting (Server Logs)”](#31-website-hosting-server-logs)
When you visit our website, our hosting provider Netlify automatically processes server access logs.
| | |
| --------------- | ----------------------------------------------------------------------------------------------- |
| **Data** | IP address, browser type and version, pages visited, date and time of access, referring URL |
| **Purpose** | Website delivery, security, and abuse prevention |
| **Legal basis** | Legitimate interest (Art. 6(1)(f) GDPR) — ensuring the availability and security of our website |
| **Retention** | Up to 30 days (Netlify’s standard log retention) |
| **Recipient** | Netlify, Inc. (USA) — see Section 5 |
### 3.3 Purchases via Creem
[Section titled “3.3 Purchases via Creem”](#33-purchases-via-creem)
When you purchase a commercial license, the transaction is processed by Armitage Labs OÜ, trading as Creem (“Creem”), registered in Estonia. Creem acts as our merchant of record and as an **independent data controller** for payment data; Creem collects and processes your payment information (such as credit card details) directly, and this information is never shared with or accessible to us. Creem’s full identification, contact details, and processing description are set out in [Creem’s Privacy Policy](https://www.creem.io/privacy).
Creem shares limited data with us for the purpose of license fulfillment:
| | |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Data shared with us by Creem** | Name, email address, country, transaction details (product, price, payment status) |
| **Purpose** | License fulfillment, product support, subscription management, and service communications (e.g., notice of changes to our [Terms of Service](/terms-of-service/)) |
| **Legal basis** | Performance of a contract (Art. 6(1)(b) GDPR) — necessary to provide you with the commercial license you purchased under our [Terms of Service](/terms-of-service/) |
| **Retention** | Duration of your subscription plus 7 years thereafter (Dutch tax record-keeping obligation under Art. 52 Algemene wet inzake rijksbelastingen) |
| **Recipient** | Stored by us; see Section 5 for service providers |
We may only use the buyer data Creem shares with us for order fulfillment, product support, and service communications necessary for the performance of your subscription contract (such as notifying you of changes to our [Terms of Service](/terms-of-service/) or pricing). **We will not use this data for marketing purposes unless you give us separate, explicit consent.**
### 3.4 No Automated Decision-Making
[Section titled “3.4 No Automated Decision-Making”](#34-no-automated-decision-making)
We do not engage in automated decision-making or profiling within the meaning of Art. 22 GDPR.
### 3.5 Support Communications
[Section titled “3.5 Support Communications”](#35-support-communications)
When you contact us by email for support or other inquiries, we process the data you provide.
| | |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Data** | Name, email address, and the content of your communication |
| **Purpose** | Responding to your inquiry and providing support |
| **Legal basis** | Performance of a contract (Art. 6(1)(b) GDPR) for existing customers; legitimate interest (Art. 6(1)(f) GDPR) for other inquiries |
| **Retention** | Duration of the business relationship plus 7 years |
| **Recipient** | Zoho Corporation Pvt. Ltd. (email hosting) — see Section 5 |
***
## 4. Cookies and Similar Technologies
[Section titled “4. Cookies and Similar Technologies”](#4-cookies-and-similar-technologies)
### 4.1 Our Website
[Section titled “4.1 Our Website”](#41-our-website)
Our website does not set any first-party cookies. The Starlight documentation framework may store a theme preference (light/dark mode) in your browser’s local storage. This is strictly functional data that remains on your device and is never transmitted to us.
### 4.2 No Tracking or Advertising Cookies
[Section titled “4.2 No Tracking or Advertising Cookies”](#42-no-tracking-or-advertising-cookies)
We do not use any tracking cookies, advertising cookies, or third-party analytics scripts that run in your browser. We do not participate in any advertising networks.
***
## 5. Third-Party Service Providers
[Section titled “5. Third-Party Service Providers”](#5-third-party-service-providers)
We use the following third-party service providers who may process personal data on our behalf or as independent controllers:
| Provider | Role | Data processed | Location | Safeguards |
| ------------------------------ | ------------------------------------------- | ----------------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- |
| **Netlify, Inc.** | Website hosting | Server logs (IP address, browser data, pages visited) | USA | [Netlify DPA](https://www.netlify.com/gdpr-ccpa/); Standard Contractual Clauses |
| **Armitage Labs OÜ (Creem)** | Merchant of record (independent controller) | Payment and billing data | Estonia (EU) | Independent controller for payment data; see [Creem’s Privacy Policy](https://www.creem.io/privacy) |
| **Zoho Corporation Pvt. Ltd.** | Email hosting | Email communications | India/EU (we use their EU data centers) | [Zoho Privacy](https://www.zoho.com/privacy.html); we signed a DPA with Zoho |
| **IONOS SE** | DNS hosting | DNS query logs (IP addresses) | Germany | EU-based; subject to GDPR |
### 5.1 Package Registries
[Section titled “5.1 Package Registries”](#51-package-registries)
Our software is distributed through public package registries including npm, PyPI, crates.io, NuGet, Maven Central, and Packagist. When you download our software from these registries, your interaction is subject to that registry’s own terms and privacy policy. We do not receive personally identifiable information about individual package downloads.
***
## 6. International Data Transfers
[Section titled “6. International Data Transfers”](#6-international-data-transfers)
Some of our service providers are located outside the European Economic Area (EEA). Where personal data is transferred outside the EEA, we ensure that appropriate safeguards are in place:
* **Netlify (USA)**: Standard Contractual Clauses (SCCs) as part of Netlify’s Data Processing Agreement
* **Zoho (India)**: Standard Contractual Clauses; EU data center option available
***
## 7. Data Retention
[Section titled “7. Data Retention”](#7-data-retention)
| Data category | Retention period |
| ---------------------- | --------------------------------------------------- |
| Server logs | Up to 30 days |
| Buyer data from Creem | Duration of subscription + 7 years (tax obligation) |
| Support communications | Duration of business relationship + 7 years |
| Aggregate analytics | Indefinitely (no personal data) |
When the retention period expires, personal data is deleted or anonymized.
***
## 8. Your Rights
[Section titled “8. Your Rights”](#8-your-rights)
Under the GDPR, you have the following rights regarding your personal data:
* **Right of access** (Art. 15): You may request a copy of the personal data we hold about you.
* **Right to rectification** (Art. 16): You may request correction of inaccurate or incomplete personal data.
* **Right to erasure** (Art. 17): You may request deletion of your personal data, subject to legal retention obligations.
* **Right to restriction of processing** (Art. 18): You may request that we restrict the processing of your personal data in certain circumstances.
* **Right to data portability** (Art. 20): You may request to receive your personal data in a structured, commonly used, and machine-readable format.
* **Right to object** (Art. 21): You may object to processing based on legitimate interest. We will cease processing unless we demonstrate compelling legitimate grounds that override your interests.
* **Right to withdraw consent** (Art. 7(3)): Where processing is based on consent, you may withdraw consent at any time without affecting the lawfulness of processing before withdrawal.
To exercise any of these rights, please contact us at . We will respond to your request within one month. If we need more time (up to two additional months for complex requests), we will inform you within the initial one-month period.
If you are not satisfied with our response, you have the right to lodge a complaint with the Dutch Data Protection Authority:
> **Autoriteit Persoonsgegevens** Postbus 93374 2509 AJ Den Haag
***
## 9. Children’s Privacy
[Section titled “9. Children’s Privacy”](#9-childrens-privacy)
Our website and services are not directed at children under the age of 16. We do not knowingly collect personal data from children under 16. If you believe we have inadvertently collected such data, please contact us and we will promptly delete it.
***
## 10. Changes to This Policy
[Section titled “10. Changes to This Policy”](#10-changes-to-this-policy)
We may update this Privacy Policy from time to time. If we make material changes, we will notify you by posting the updated policy on our website with a revised “Last updated” date. If you have an active subscription, we will also notify you by email.
***
## 11. Contact
[Section titled “11. Contact”](#11-contact)
For any questions or requests regarding this Privacy Policy or the processing of your personal data, please email ****. Our full legal identity and postal address are listed in Section 1.
***
## 12. Related Documents
[Section titled “12. Related Documents”](#12-related-documents)
* [Terms of Service](/terms-of-service/): governs your commercial license to the Oicana software.
* [Refund Policy](/refund-policy/): explains how refunds for commercial license purchases work.
* [Imprint](/imprint/): provider identification under Art. 3:15d Dutch Civil Code.
# Terms of Service
> Terms of Service for the Oicana software and commercial licenses.
**Last updated: 15.06.2026**
These Terms of Service (“Terms”) govern your commercial license to use the Oicana software (“Software”) provided by Niklas Martin Eicker, trading as Oicana (“Oicana,” “we,” “us,” or “our”), a sole proprietorship (eenmanszaak) registered in the Netherlands.
By purchasing a commercial license or otherwise using the Software beyond the scope of the PolyForm Noncommercial License, you (“Customer,” “you,” or “your”) agree to be bound by these Terms. If you are entering into these Terms on behalf of a company or other legal entity, you represent that you have the authority to bind that entity.
***
## 1. Company Information
[Section titled “1. Company Information”](#1-company-information)
| | |
| -------------- | --------------------------------------------- |
| **Legal name** | Niklas Martin Eicker, trading as Oicana |
| **Legal form** | Eenmanszaak (sole proprietorship) |
| **Address** | Smidspad 27, 1506 MW Zaandam, the Netherlands |
| **Email** | |
| **KVK number** | 42036325 |
| **BTW-id** | NL005445995B24 |
***
## 2. Scope of These Terms
[Section titled “2. Scope of These Terms”](#2-scope-of-these-terms)
### 2.1 Noncommercial Use
[Section titled “2.1 Noncommercial Use”](#21-noncommercial-use)
The Oicana software source code and most packages are available under the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0). Any use permitted under that license — including personal research, experimentation, testing, private study, hobby projects, amateur pursuits, and use by charitable organizations, educational institutions, public research organizations, public safety or health organizations, environmental protection organizations, and government institutions — is governed solely by the PolyForm Noncommercial License and not by these Terms.
### 2.2 Commercial Use
[Section titled “2.2 Commercial Use”](#22-commercial-use)
Any use of the Oicana software that falls outside the scope of the PolyForm Noncommercial License constitutes “Commercial Use” and requires a paid commercial license subscription under these Terms. Commercial Use includes, but is not limited to, use within or on behalf of any for-profit organization, use in products or services offered for sale, and use in revenue-generating activity.
### 2.3 Relationship Between Licenses
[Section titled “2.3 Relationship Between Licenses”](#23-relationship-between-licenses)
These Terms do not modify, replace, or limit the PolyForm Noncommercial License. If your use qualifies as noncommercial under the PolyForm Noncommercial License, you do not need a commercial license. If you are uncertain whether your use qualifies, please contact us.
### 2.4 Components Under Other Licenses
[Section titled “2.4 Components Under Other Licenses”](#24-components-under-other-licenses)
Some components of Oicana are made available under other licenses (for example, the Typst integration is available under the MIT License). Use of such a component that is fully covered by its own license is governed solely by that license and not by these Terms or the PolyForm Noncommercial License. References in these Terms to the PolyForm Noncommercial License should be read as referring, where applicable, to the license governing the relevant component.
***
## 3. Payment and Billing
[Section titled “3. Payment and Billing”](#3-payment-and-billing)
### 3.1 Creem as Merchant of Record
[Section titled “3.1 Creem as Merchant of Record”](#31-creem-as-merchant-of-record)
Creem (operated by Armitage Labs OÜ, Estonia) is the Merchant of Record for all our orders and handles customer payment, invoicing, refunds, and tax collection (including VAT and sales tax). Full details are set out in [Creem’s Buyer Terms](https://www.creem.io/buyer-terms) (provided at checkout).
### 3.2 Payment Processing
[Section titled “3.2 Payment Processing”](#32-payment-processing)
When you purchase a subscription, you enter into a purchase agreement with Creem under their Buyer Terms, and a separate license agreement with Oicana under these Terms.
### 3.3 Prices and Taxes
[Section titled “3.3 Prices and Taxes”](#33-prices-and-taxes)
Current prices for all subscription tiers are listed on our website at . All prices displayed at checkout are inclusive of applicable taxes as determined by Creem based on your location. We reserve the right to change prices at any time. Any price change will take effect at the start of your next billing period; you will be notified in advance and may cancel your subscription before the new price applies.
### 3.4 Invoices and Receipts
[Section titled “3.4 Invoices and Receipts”](#34-invoices-and-receipts)
Creem issues all invoices and payment receipts. You can see your payments and print out invoices for them in the Creem customer portal. Follow the link provided in your purchase confirmation email.
### 3.5 Buyer Data
[Section titled “3.5 Buyer Data”](#35-buyer-data)
Creem shares limited buyer data with us for license fulfillment and subscription management. How we process that data, and the role Creem plays as an independent controller for payment data, is described in our [Privacy Policy](/privacy-policy/).
***
## 4. Commercial License
[Section titled “4. Commercial License”](#4-commercial-license)
### 4.1 License Grant
[Section titled “4.1 License Grant”](#41-license-grant)
Subject to your compliance with these Terms and payment of the applicable subscription fees, Oicana grants you a non-exclusive, non-transferable, non-sublicensable, revocable license to use the Oicana software for Commercial Use during the subscription term. The revocability of this license is subject to the business-continuity fallback set out in Section 4.7.
### 4.2 Evaluation Use
[Section titled “4.2 Evaluation Use”](#42-evaluation-use)
You may use Oicana for commercial evaluation. You can follow the getting-started guide, build proof-of-concept integrations, test and use it in production. Evaluation use is allowed for 30 days from your first commercial use, without a paid subscription. Continued commercial use after the 30-day period requires a subscription under Section 4.3.
### 4.3 Per-Application Licensing
[Section titled “4.3 Per-Application Licensing”](#43-per-application-licensing)
Each commercial license subscription entitles you to use Oicana in **one Application**. An “Application” is a single, distinct software product, website, service, or publication that uses Oicana to generate output. If you use Oicana in multiple Applications, you must purchase a separate subscription for each Application.
For clarity:
* An Application may consist of multiple services, modules, or microservices that together form one product.
* Different environments of the same Application (e.g., development, staging, production) count as **one** Application.
* A single product deployed by you for or on behalf of multiple end clients — including white-label deployments delivered under your clients’ names — counts as **one** Application.
* The same software released or sold as multiple distinct commercial products under separate brands counts as **one** Application per brand. Contact us for OEM, reseller, or similar arrangements.
### 4.4 Tier Eligibility
[Section titled “4.4 Tier Eligibility”](#44-tier-eligibility)
Commercial licenses are offered in multiple tiers based on company size. The current tiers and their eligibility criteria are published on our website at . You must select the tier that corresponds to your company’s size.
The metrics used to measure company size are taken as of the start of each billing period. If your company grows beyond the limits of your current tier during a billing period, you must upgrade to the appropriate tier at your next renewal.
### 4.5 License Restrictions
[Section titled “4.5 License Restrictions”](#45-license-restrictions)
You may not:
* sublicense, resell, rent, lease, or otherwise transfer the license or any rights under it to any third party;
* use the Software to build a competing product or service whose primary purpose is to replicate the core functionality of Oicana;
* remove, alter, or obscure any copyright, trademark, or other proprietary notices in the Software;
* use a single license for more than one Application as defined in Section 4.3.
### 4.6 All Integrations Included
[Section titled “4.6 All Integrations Included”](#46-all-integrations-included)
Your commercial license covers use of all Oicana integrations (including but not limited to browser/WASM, Node.js, Rust, Python, Java, C#, and PHP) within the licensed Application.
### 4.7 Business-Continuity Fallback License
[Section titled “4.7 Business-Continuity Fallback License”](#47-business-continuity-fallback-license)
If any of the following events occurs (each a “Trigger Event”):
(a) we publicly announce that we are discontinuing the Software; (b) the Software becomes unavailable through its normal distribution channels, and we fail to restore availability within 180 days of receiving written notice from you; or (c) we become insolvent, enter into surseance van betaling or faillissement under the Dutch Bankruptcy Act (Faillissementswet), cease operations, or become legally incapacitated, in each case without a successor or assignee continuing to provide the Software under Section 16.3,
then, effective immediately before the Trigger Event, each licensee with an active paid subscription is granted a perpetual, worldwide, non-exclusive, royalty-free license to continue using the version of the Software made available under their most recent paid billing period, within the scope of Commercial Use permitted by their then-current tier.
This fallback license does not include any right to support, updates, upgrades, new versions, or source code beyond what you already possess. All other provisions of these Terms — including Section 4.5 (License Restrictions), Section 9 (Intellectual Property), Section 10 (Limitation of Liability), and Section 17 (Export Control and Sanctions Compliance) — continue to apply to your use of the Software under this fallback license.
***
## 5. Subscription Terms
[Section titled “5. Subscription Terms”](#5-subscription-terms)
### 5.1 Billing Periods
[Section titled “5.1 Billing Periods”](#51-billing-periods)
Subscriptions are available on a monthly or yearly billing basis, as selected at checkout. Current pricing for each billing period is listed on our website.
### 5.2 Auto-Renewal
[Section titled “5.2 Auto-Renewal”](#52-auto-renewal)
Your subscription automatically renews at the end of each billing period unless cancelled. You will be charged the then-current price for your tier and billing period at each renewal.
### 5.3 Cancellation
[Section titled “5.3 Cancellation”](#53-cancellation)
You may cancel your subscription at the end of any billing period by providing notice before the start of the next billing period. You can cancel through the subscription management link in your Creem receipt email, or by contacting us at .
### 5.4 Effect of Cancellation
[Section titled “5.4 Effect of Cancellation”](#54-effect-of-cancellation)
Upon cancellation, your license continues until the end of the current paid billing period. After that, your commercial license terminates and you must cease all Commercial Use of the Software. You may continue to use the Software under the PolyForm Noncommercial License for any noncommercial purpose.
***
## 6. Refund Policy
[Section titled “6. Refund Policy”](#6-refund-policy)
### 6.1 Money-Back Guarantee
[Section titled “6.1 Money-Back Guarantee”](#61-money-back-guarantee)
We offer a 30-day money-back guarantee on initial purchases. The eligibility, request procedure, and processing details are set out in our [Refund Policy](/refund-policy/).
### 6.2 Statutory Rights
[Section titled “6.2 Statutory Rights”](#62-statutory-rights)
This refund policy does not affect your statutory rights. If you are a consumer in the European Union, you retain all rights provided by the applicable consumer protection laws of your country of residence.
***
## 7. Right of Withdrawal (EU Consumers)
[Section titled “7. Right of Withdrawal (EU Consumers)”](#7-right-of-withdrawal-eu-consumers)
This section applies only if you are a consumer habitually resident in the European Union.
### 7.1 Withdrawal Right
[Section titled “7.1 Withdrawal Right”](#71-withdrawal-right)
You have the right to withdraw from this contract within **14 days** without giving any reason. The withdrawal period will expire after 14 days from the day of the conclusion of the contract (i.e., the date of purchase).
To exercise the right of withdrawal, you must inform us of your decision to withdraw from this contract by an unequivocal statement (e.g., a letter sent by post or email). You may use the Model Withdrawal Form in Annex A, but it is not obligatory. You may also exercise your withdrawal right through Creem.
To meet the withdrawal deadline, it is sufficient for you to send your communication concerning your exercise of the right of withdrawal before the withdrawal period has expired.
### 7.2 Effects of Withdrawal
[Section titled “7.2 Effects of Withdrawal”](#72-effects-of-withdrawal)
If you withdraw from this contract, we shall reimburse to you all payments received from you without undue delay and in any event not later than 14 days from the day on which we are informed of your decision to withdraw from this contract. The reimbursement will be carried out using the same means of payment as you used for the initial transaction, unless you have expressly agreed otherwise. You will not incur any fees as a result of such reimbursement.
### 7.3 Waiver for Immediate Digital Content Access
[Section titled “7.3 Waiver for Immediate Digital Content Access”](#73-waiver-for-immediate-digital-content-access)
The commercial license is digital content not supplied on a tangible medium. At checkout, you will be asked to:
1. **Give your prior express consent** to begin performance of the contract immediately (i.e., for the commercial license to take effect immediately upon completion of your purchase); and
2. **Acknowledge** that by consenting to immediate performance, you lose your right of withdrawal once that performance has begun.
If you give this consent and acknowledgement, your right of withdrawal expires once the commercial license has taken effect.
Note: even after waiving your withdrawal right, you may still request a refund under our 30-day money-back guarantee (Section 6) and retain your statutory conformity rights (Section 8).
### 7.4 Subscriptions
[Section titled “7.4 Subscriptions”](#74-subscriptions)
For subscription contracts, the withdrawal waiver in Section 7.3 applies to the initial subscription term. Subsequent renewal periods are subject to the cancellation terms in Section 5.3.
***
## 8. Conformity and Updates
[Section titled “8. Conformity and Updates”](#8-conformity-and-updates)
### 8.1 Conformity Guarantee
[Section titled “8.1 Conformity Guarantee”](#81-conformity-guarantee)
We warrant that the Software will conform to its description as published on our website and documentation, will be fit for the purposes of PDF generation and document templating, and will be of the quality normal for digital content of the same type.
### 8.2 Update Commitment
[Section titled “8.2 Update Commitment”](#82-update-commitment)
During your active subscription, we will publish updates, including security updates, that are necessary to keep the Software in conformity with these Terms. Updates are released through the public package registries on which Oicana is distributed, and you are responsible for retrieving and adopting them in your Application. Updates are made available free of charge as part of your subscription.
### 8.3 Remedies for Non-Conformity
[Section titled “8.3 Remedies for Non-Conformity”](#83-remedies-for-non-conformity)
If the Software does not conform to the guarantees in Section 8.1:
1. We will first bring the Software into conformity within a reasonable time, free of charge.
2. If bringing the Software into conformity is impossible, disproportionate, or we fail to do so: you are entitled to a proportionate reduction in the subscription fee, or you may terminate the subscription.
3. You may terminate the subscription for non-conformity unless the lack of conformity is minor (in which case we bear the burden of proof that the non-conformity is minor).
4. Upon termination for non-conformity of a subscription, we will reimburse the proportionate part of the fees paid for the period during which the Software was not in conformity, plus any advance payments for periods after termination.
***
## 9. Intellectual Property
[Section titled “9. Intellectual Property”](#9-intellectual-property)
### 9.1 Ownership
[Section titled “9.1 Ownership”](#91-ownership)
Oicana and its licensors retain all right, title, and interest in and to the Oicana software, including all intellectual property rights. These Terms grant you a limited license only and do not transfer any ownership rights.
### 9.2 Open-Source Components
[Section titled “9.2 Open-Source Components”](#92-open-source-components)
The Oicana software may include or depend on open-source software components, each governed by its own license. Nothing in these Terms restricts or limits your rights under such open-source licenses.
### 9.3 Typst
[Section titled “9.3 Typst”](#93-typst)
Oicana is built on top of the [Typst](https://typst.app) typesetting system. Typst is a separate project with its own license. Your use of Typst through Oicana does not create any relationship between you and the Typst project.
### 9.4 Feedback
[Section titled “9.4 Feedback”](#94-feedback)
If you provide us with suggestions, ideas, or other feedback about the Software, we may use it without restriction or obligation to you.
***
## 10. Limitation of Liability
[Section titled “10. Limitation of Liability”](#10-limitation-of-liability)
### 10.1 Unlimited Liability
[Section titled “10.1 Unlimited Liability”](#101-unlimited-liability)
Nothing in these Terms limits or excludes our liability for:
* fraud or fraudulent misrepresentation;
* death or personal injury caused by our negligence;
* gross negligence (grove schuld) or willful misconduct (opzet);
* any other liability that cannot be limited or excluded under applicable law.
### 10.2 Liability Cap (B2B)
[Section titled “10.2 Liability Cap (B2B)”](#102-liability-cap-b2b)
If you are a business customer, subject to Section 10.1, our total aggregate liability arising out of or in connection with these Terms shall not exceed the total fees paid by you in the **12 months** immediately preceding the event giving rise to the claim.
### 10.3 Exclusion of Indirect Damages (B2B)
[Section titled “10.3 Exclusion of Indirect Damages (B2B)”](#103-exclusion-of-indirect-damages-b2b)
If you are a business customer, subject to Section 10.1, we shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits, revenue, data, or business opportunity, however caused.
### 10.4 Consumer Rights Preserved
[Section titled “10.4 Consumer Rights Preserved”](#104-consumer-rights-preserved)
If you are a consumer, Sections 10.2 and 10.3 do not apply to you. Our liability to consumers is governed by the mandatory provisions of applicable consumer protection law, which cannot be excluded or limited by contract.
***
## 11. Warranty
[Section titled “11. Warranty”](#11-warranty)
### 11.1 B2B Warranty Disclaimer
[Section titled “11.1 B2B Warranty Disclaimer”](#111-b2b-warranty-disclaimer)
If you are a business customer, except as expressly stated in Section 8.1, the Software is provided “as is” and “as available.” To the maximum extent permitted by law, we disclaim all other warranties, whether express, implied, or statutory, including implied warranties of merchantability, fitness for a particular purpose, and non-infringement.
### 11.2 Consumer Warranty
[Section titled “11.2 Consumer Warranty”](#112-consumer-warranty)
If you are a consumer, you benefit from the statutory conformity guarantee as set out in Section 8. This guarantee cannot be excluded or limited by contract. The warranty disclaimer in Section 11.1 does not apply to you.
***
## 12. Termination
[Section titled “12. Termination”](#12-termination)
### 12.1 Termination by You
[Section titled “12.1 Termination by You”](#121-termination-by-you)
You may terminate these Terms at any time by cancelling your subscription as described in Section 5.3.
### 12.2 Termination by Us
[Section titled “12.2 Termination by Us”](#122-termination-by-us)
We may terminate these Terms or suspend your license immediately upon written notice if:
* you breach any material provision of these Terms and fail to cure the breach within **30 days** of receiving notice; or
* you become insolvent, file for bankruptcy, or cease operations.
We will not terminate these Terms for convenience without providing at least **30 days’** notice, and any termination by us for convenience takes effect at the end of the then-current billing period.
### 12.3 Effect of Termination
[Section titled “12.3 Effect of Termination”](#123-effect-of-termination)
Upon termination:
* your commercial license ends and you must cease all Commercial Use of the Software;
* you may continue to use the Software under the PolyForm Noncommercial License for any noncommercial purpose as described by that license;
* termination does not affect any rights or obligations that accrued before the termination date;
* Sections 9 (Intellectual Property), 10 (Limitation of Liability), and 15 (Governing Law) survive termination.
***
## 13. Changes to These Terms
[Section titled “13. Changes to These Terms”](#13-changes-to-these-terms)
We may update these Terms from time to time. If we make material changes, we will provide you with at least **30 days’** advance notice by email before the changes take effect.
If you are a consumer and the changes are materially adverse to you, you may terminate your subscription free of charge before the changes take effect by providing notice within the 30-day notification period.
Your continued use of the Software after the effective date of the updated Terms constitutes acceptance of the changes.
***
## 14. Consumer Protection Savings Clause
[Section titled “14. Consumer Protection Savings Clause”](#14-consumer-protection-savings-clause)
If you are a consumer, nothing in these Terms limits or excludes any rights you have under mandatory consumer protection laws of your country of residence. In the event of any conflict between these Terms and such mandatory provisions, the mandatory provisions shall prevail.
***
## 15. Governing Law and Jurisdiction
[Section titled “15. Governing Law and Jurisdiction”](#15-governing-law-and-jurisdiction)
### 15.1 Governing Law
[Section titled “15.1 Governing Law”](#151-governing-law)
These Terms shall be governed by and construed in accordance with the laws of the Netherlands. If you are a consumer habitually resident in the European Union, you will also benefit from any mandatory provisions of the law of your country of residence.
### 15.2 Jurisdiction (B2B)
[Section titled “15.2 Jurisdiction (B2B)”](#152-jurisdiction-b2b)
If you are a business customer, any dispute arising out of or in connection with these Terms shall be submitted to the exclusive jurisdiction of the competent courts in Amsterdam, the Netherlands. The United Nations Convention on Contracts for the International Sale of Goods (CISG) is excluded.
### 15.3 Consumer Jurisdiction
[Section titled “15.3 Consumer Jurisdiction”](#153-consumer-jurisdiction)
If you are a consumer, you may bring proceedings in the courts of the Netherlands or in the courts of your country of habitual residence. We may only bring proceedings against you in the courts of your country of habitual residence.
***
## 16. Miscellaneous
[Section titled “16. Miscellaneous”](#16-miscellaneous)
### 16.1 Entire Agreement
[Section titled “16.1 Entire Agreement”](#161-entire-agreement)
These Terms constitute the entire agreement between you and Oicana regarding your commercial license to the Software and supersede all prior agreements or understandings. Your separate agreements with third parties (including the PolyForm Noncommercial License governing any noncommercial use of the software, and Creem’s Buyer Terms governing your purchase transaction) are not modified by these Terms.
### 16.2 Severability
[Section titled “16.2 Severability”](#162-severability)
If any provision of these Terms is held to be invalid or unenforceable, the remaining provisions shall continue in full force and effect. The invalid or unenforceable provision shall be replaced by a valid provision that most closely reflects the original intent.
### 16.3 Assignment
[Section titled “16.3 Assignment”](#163-assignment)
You may not assign or transfer these Terms or any rights under them without our prior written consent. We may assign these Terms to a successor in the event of a merger, acquisition, or sale of all or substantially all of our assets, provided that the assignee assumes all obligations under these Terms.
### 16.4 Waiver
[Section titled “16.4 Waiver”](#164-waiver)
Our failure to enforce any right or provision of these Terms shall not constitute a waiver of such right or provision.
### 16.5 Language
[Section titled “16.5 Language”](#165-language)
These Terms are drafted in English. In the event of any conflict between the English version and any translation, the English version shall prevail (except where prohibited by local law).
***
## 17. Export Control and Sanctions Compliance
[Section titled “17. Export Control and Sanctions Compliance”](#17-export-control-and-sanctions-compliance)
You are responsible for complying with all export-control and sanctions laws applicable to your use of the Software, including those of the European Union, the Netherlands, and any other jurisdiction applicable to you. You represent that you are not subject to comprehensive EU sanctions and are not listed on any applicable restricted-party list, and you will not use the Software, or permit it to be used, in breach of such laws.
If your representations cease to be accurate, or we reasonably believe your use of the Software is or would be in breach of applicable export-control or sanctions laws, we may suspend or terminate your license immediately upon written notice.
***
## 18. Contact Information
[Section titled “18. Contact Information”](#18-contact-information)
For questions about these Terms or the Software, contact us as described in Section 1. For payment or billing inquiries, please contact Creem through the link provided in your purchase confirmation email.
***
## 19. Related Documents
[Section titled “19. Related Documents”](#19-related-documents)
* [Privacy Policy](/privacy-policy/): how we process personal data in connection with the website and license purchases.
* [Refund Policy](/refund-policy/): eligibility and procedure for the 30-day money-back guarantee.
* [Imprint](/imprint/): provider identification under Art. 3:15d Dutch Civil Code.
***
## Annex A: Model Withdrawal Form
[Section titled “Annex A: Model Withdrawal Form”](#annex-a-model-withdrawal-form)
**(Complete and return this form only if you wish to withdraw from the contract)**
To: Niklas Martin Eicker, trading as Oicana Smidspad 27, 1506 MW Zaandam, the Netherlands
I/We (\*) hereby give notice that I/we (\*) withdraw from my/our (\*) contract for the supply of the following digital content:
Oicana commercial license subscription
* Ordered on: \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
* Name of consumer(s): \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
* Address of consumer(s): \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
* Signature of consumer(s) (only if this form is notified on paper): \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
* Date: \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
(\*) Delete as appropriate.
# Refund Policy
> Refund Policy for commercial license purchases of the Oicana software.
**Last updated: 11.05.2026**
This Refund Policy explains how refunds work for commercial license purchases of the Oicana software. It forms part of, and should be read together with, our [Terms of Service](/terms-of-service/).
***
## 1. 30-Day Money-Back Guarantee
[Section titled “1. 30-Day Money-Back Guarantee”](#1-30-day-money-back-guarantee)
If you are not satisfied with your commercial license, you may request a full refund within **30 days** of your initial purchase, subject to applicable law and our obligations under Section 17 of our [Terms of Service](/terms-of-service/#17-export-control-and-sanctions-compliance).
This guarantee applies to your first payment for a subscription. Renewal payments are subject to the cancellation terms in Section 5.3 of our [Terms of Service](/terms-of-service/#53-cancellation).
***
## 2. How to Request a Refund
[Section titled “2. How to Request a Refund”](#2-how-to-request-a-refund)
You can request a refund by contacting us at with the order reference from your Creem receipt, and we will arrange the refund with Creem.
We will respond to refund requests within **3 business days**. All refunds are processed by Creem and returned to the original payment method.
***
## 3. Creem as Merchant of Record
[Section titled “3. Creem as Merchant of Record”](#3-creem-as-merchant-of-record)
Creem (operated by Armitage Labs OÜ, Estonia) is the Merchant of Record for all our orders and is responsible for the actual payment and refund transaction. You can contact Creem directly using the links in your purchase-confirmation email. For details, see [Creem’s Buyer Terms](https://www.creem.io/buyer-terms).
***
## 4. Refund Processing
[Section titled “4. Refund Processing”](#4-refund-processing)
### 4.1 Timing
[Section titled “4.1 Timing”](#41-timing)
Once a refund is approved, Creem will initiate the refund within a few business days. Depending on your bank or card issuer, the funds may take several additional business days to appear on your statement.
### 4.2 Refund Amount
[Section titled “4.2 Refund Amount”](#42-refund-amount)
The amount credited back to you is the amount you paid. In some situations the amount shown on your statement may differ slightly from the original charge because of:
* **Currency fluctuations**: if your card was charged in a currency other than the one used for the original transaction, exchange-rate movements between the purchase and the refund can cause small differences.
* **Tax adjustments**: Creem returns any VAT or sales tax collected on the original transaction; local tax rules may affect the exact amount.
* **Intermediary fees**: in rare cases your bank or card issuer may apply a fee to the refund transaction.
These variations are determined by payment networks and tax authorities, not by Oicana or Creem.
***
## 5. Right of Withdrawal (EU Consumers)
[Section titled “5. Right of Withdrawal (EU Consumers)”](#5-right-of-withdrawal-eu-consumers)
If you are a consumer habitually resident in the European Union, you have a statutory **14-day right of withdrawal** in addition to the 30-day money-back guarantee described above. The full terms, including the waiver for immediate digital-content access and the Model Withdrawal Form, are set out in Section 7 of our [Terms of Service](/terms-of-service/#7-right-of-withdrawal-eu-consumers).
***
## 6. Statutory Rights Preserved
[Section titled “6. Statutory Rights Preserved”](#6-statutory-rights-preserved)
Nothing in this Refund Policy limits or excludes any rights you have under mandatory consumer-protection laws of your country of residence, including the statutory conformity guarantee described in Section 8 of our [Terms of Service](/terms-of-service/#8-conformity-and-updates).
***
## 7. Contact
[Section titled “7. Contact”](#7-contact)
For refund requests or any questions about this policy, please contact us at .
***
## 8. Related Documents
[Section titled “8. Related Documents”](#8-related-documents)
* [Terms of Service](/terms-of-service/): full terms governing your commercial license, including the EU withdrawal right.
* [Privacy Policy](/privacy-policy/): how we process personal data in connection with license purchases.
* [Imprint](/imprint/): provider identification under Art. 3:15d Dutch Civil Code.