Typst PDF generation library for Java 25+.
Renders Typst templates into PDF documents via an embedded native Typst compiler called through Java FFM API (Project Panama). Zero external runtime dependencies – one JAR, everything bundled.
- Embedded Typst compiler — no need to install Typst or any other tool
- Java FFM API (Project Panama) — no JNI, no subprocess, direct in-process calls
- Fluent builder API — configure engine, bind data, render PDF in a few lines
- Template engine — pure Typst templates with JSON data injection via virtual filesystem
- Auto-serialization — Java Records, POJOs, Maps, Lists automatically serialized to JSON
- Custom fonts — load from directories, byte arrays, InputStreams (classpath resources, DB, S3)
- Template caching — compiled templates reused across renders, mtime-based invalidation
- Thread-safe — one engine instance, concurrent rendering from multiple threads
- Structured errors —
TypstCompilationExceptionwith file, line, column, message, hints - Cross-platform — Linux, macOS, Windows; x86_64 and aarch64
<dependency>
<groupId>name.velikodniy.vitaliy</groupId>
<artifactId>typst-java</artifactId>
<version>${add here the last version}</version>
</dependency>The library uses Java FFM API for native calls. Without this flag everything works, but the JVM prints a warning. It will become required in future Java versions:
java --enable-native-access=ALL-UNNAMED -jar myapp.jar
try (var engine = TypstEngine.builder().build()) {
byte[] pdf = engine.template("hello", "= Hello, World!")
.renderPdf();
Files.write(Path.of("hello.pdf"), pdf);
}Create a Typst template invoice.typ:
#let data = json("data.json")
= Invoice #data.number
*Customer:* #data.customer.name \
*Date:* #data.date
#table(
columns: (1fr, auto, auto),
table.header([*Item*], [*Qty*], [*Price*]),
..data.items.map(item => (
[#item.name],
[#str(item.qty)],
[#str(item.price)],
)).flatten()
)
*Total:* #data.totalRender it from Java:
record Customer(String name, String email) {}
record LineItem(String name, int qty, BigDecimal price) {}
try (var engine = TypstEngine.builder()
.addFont(getClass().getResourceAsStream("/fonts/corporate.ttf"))
.build()) {
byte[] pdf = engine.template(Path.of("invoice.typ"))
.data("number", "INV-2026-001")
.data("customer", new Customer("Acme Corp", "billing@acme.com"))
.data("date", LocalDate.now())
.data("items", List.of(
new LineItem("Consulting", 40, new BigDecimal("150.00")),
new LineItem("Development", 120, new BigDecimal("200.00"))
))
.data("total", "$30,000.00")
.renderPdf();
}record InvoiceData(
String number,
Customer customer,
LocalDate date,
List<LineItem> items
) {}
byte[] pdf = engine.template(Path.of("invoice.typ"))
.data(new InvoiceData("INV-001", customer, LocalDate.now(), items))
.renderPdf();Record fields become top-level keys in data.json. You can combine .data(record) with .data(key, value) — last write wins on key conflicts.
TypstEngine engine = TypstEngine.builder()
.addFontDir(Path.of("/usr/share/fonts")) // directory with .ttf/.otf files
.addFont(fontBytes) // byte[]
.addFont(inputStream) // InputStream (classpath, DB, S3)
.enableTemplateCache(true) // default: true
.build();- Thread-safe, create once, share across threads
- Implements
AutoCloseable— use try-with-resources - Default fonts from Typst are always available (bundled in native library)
// From file
engine.template(Path.of("template.typ"))
// From string (name used as cache key)
engine.template("my-template", typstSource).data("key", value) // key-value pair
.data(record) // expand record fields as top-level keys
.dataJson("{\"raw\":true}") // raw JSON string
.renderPdf() // returns byte[]engine.invalidateTemplate(Path.of("template.typ"));
engine.invalidateTemplate("cache-key");
engine.invalidateAllTemplates();File templates auto-invalidate when mtime changes.
try {
engine.template(path).data(data).renderPdf();
} catch (TypstCompilationException e) {
for (TypstDiagnostic d : e.getDiagnostics()) {
System.err.printf("%s:%d:%d: %s %s%n",
d.file(), d.line(), d.column(),
d.severity(), d.message());
// e.g.: "invoice.typ:12:5: ERROR unknown variable: compny"
}
}Exception hierarchy:
| Exception | When |
|---|---|
TypstCompilationException |
Template compilation errors (with structured diagnostics) |
TypstEngineException |
Configuration errors (bad font, missing directory, closed engine) |
TypstNativeException |
Native library loading or FFI call failures |
| Java Type | JSON Representation |
|---|---|
String |
"string" |
int, long, double |
number literal |
boolean |
true / false |
BigDecimal |
"string" (preserves precision) |
LocalDate |
"2026-03-29" |
LocalDateTime |
"2026-03-29T10:30:00" |
Enum |
"NAME" |
Record |
object (component names as keys) |
Map<String, ?> |
object |
List<?> / array |
array |
| POJO | object (via getters) |
null |
null |
Templates are standard Typst files. Data is injected via a virtual data.json file:
#let data = json("data.json")
// Access fields
#data.name
#data.customer.email
// Iterate arrays
#for item in data.items [
- #item.name: #str(item.qty) x #str(item.price)
]
// Tables
#table(
columns: (auto, auto),
..data.rows.map(r => ([#r.key], [#r.value])).flatten()
)
// Conditionals
#if data.showHeader [
= #data.title
]Templates work in typst.app with a manually provided data.json — no vendor lock-in.
Typst packages from packages.typst.org are supported and downloaded on demand.
Typst packages are downloaded on demand from https://packages.typst.org and cached locally. No configuration required — it works out of the box.
For advanced use cases you can optionally customize how packages are fetched:
Custom registry URL — redirect downloads to a mirror or private registry:
var engine = TypstEngine.builder()
.registry("https://my-registry.example.com")
.build();Custom resolver — implement TypstPackageResolver to fetch packages from any source:
// From local filesystem
var engine = TypstEngine.builder()
.packageResolver((namespace, name, version) ->
Files.readAllBytes(
Path.of("/packages", namespace, name + "-" + version + ".tar.gz")))
.build();
// From S3
var engine = TypstEngine.builder()
.packageResolver((namespace, name, version) ->
s3Client.getObjectAsBytes(req -> req
.bucket("typst-packages")
.key(namespace + "/" + name + "-" + version + ".tar.gz"))
.asByteArray())
.build();The resolver is a @FunctionalInterface that receives package coordinates (namespace, name, version) and returns the archive as tar.gz bytes. The engine handles unpacking and disk caching automatically. Throw TypstPackageNotFoundException if the package does not exist.
Java Application
|
v
TypstEngine (fluent API, AutoCloseable)
|
v
TypstTemplate (data binding, serialization)
|
v
TypstNative (Java FFM API bindings)
| C ABI calls via MemorySegment + MethodHandle
v
libtypst_java.so/dylib/dll (Rust shared library)
|
|--- Typst compiler (typst 0.13)
|--- PDF exporter (typst-pdf 0.13)
|--- Font book (typst-assets, embedded)
|--- Template cache (RwLock<HashMap>)
|--- Virtual filesystem (data.json injection)
|--- Package resolver (upcall to Java)
|
v
TypstPackageResolver (Java, pluggable)
|--- HttpPackageResolver (default, packages.typst.org)
|--- Custom: S3, filesystem, database, etc.
- Java 25+
- Rust toolchain (stable)
- Maven 3.9+
mvn clean verifyThis will:
- Compile the Rust native library (
cargo build --release) - Copy it to the classpath
- Run Rust tests (
cargo test) - Compile Java sources
- Run Java tests (63 tests)
- Java 25+ (uses stable FFM API from JEP 454)
- JVM flag
--enable-native-access=ALL-UNNAMED(optional now, suppresses warning) - No Rust needed at runtime — native library is bundled in the JAR
- No Typst installation needed — compiler is embedded
| OS | Architecture | Status |
|---|---|---|
| Linux | x86_64 | Supported |
| Linux | aarch64 | Supported |
| macOS | x86_64 | Supported |
| macOS | aarch64 (Apple Silicon) | Supported |
| Windows | x86_64 | Supported |