Skip to content

tweedegolf/typst-webservice

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Typst Webservice

Typst Webservice compiles Typst templates into PDFs given a JSON input. Templates, assets, and fonts are preloaded from an on-disk directory. It ships as both a library (PdfContext::render, PdfContext::render_batch) and an optional Axum-based HTTP server.

Features

  • GET /render-pdf/{template}/{file_name} renders a single template into PDF.
  • POST /render-pdf/batch renders multiple templates and returns a streaming ZIP archive.
  • Streaming ZIP writer keeps memory usage predictable for large batches.
  • Detailed error responses include unique reference IDs for troubleshooting.
  • Structured logging powered by tracing.

Cargo features

  • server (default): enables the Axum-based HTTP server, the typst-webservice binary, and the handlers / ZipResponse types. Disable with default-features = false to use the library without any HTTP dependencies:

    [dependencies]
    typst-webservice = { version = "0.5", default-features = false }

    The library-only build still exposes PdfContext::render (single PDF) and PdfContext::render_batch (ZIP of PDFs) alongside the lower-level render_batch_to_writer for streaming into a user-provided AsyncWrite.

Getting Started

cargo run

By default the server loads templates from the assets/ directory in the project root and binds to 127.0.0.1:8080.

Choosing a asset directory

You can point the service at a different assets directory using either a command-line argument or an environment variable:

# Command-line override
cargo run -- ./my-templates

# Environment variable override
TWS_DIR=./my-templates cargo run

The command-line argument takes precedence; both fall back to assets/ when unset.

Using as a library

With default-features = false the crate has no HTTP dependencies and exposes just the rendering pipeline.

Loading a PdfContext

A PdfContext holds all Typst sources, fonts, and binary assets in memory. Build one from a directory or from in-memory tuples:

use std::sync::Arc;
use typst_webservice::PdfContext;

// From a directory on disk.
let context = PdfContext::from_directory("./assets")?;

// Or from in-memory files (e.g. embedded via `include_bytes!`).
let context = PdfContext::from_assets(&[
    ("example.typ", include_bytes!("../assets/example.typ")),
    ("Bagnard.otf", include_bytes!("../assets/Bagnard.otf")),
])?;

// Share the context between render calls.
let context = Arc::new(context);

Rendering a single PDF

PdfContext::render takes the template file name, a serde_json::Value payload (exposed inside the template as input.json), and returns the PDF bytes:

use std::sync::Arc;
use typst_webservice::PdfContext;

let context = Arc::new(PdfContext::from_directory("./assets")?);

let pdf_bytes = PdfContext::render(
    context,
    "example.typ".to_string(),
    serde_json::json!({
        "name": "World",
        "list": ["Memory Safety", "Open Source", "World Peace"],
    }),
)?;

std::fs::write("out.pdf", pdf_bytes)?;

render runs a synchronous Typst compile; call it from a blocking context (or wrap it in tokio::task::spawn_blocking when running inside an async runtime).

Rendering a batch as a ZIP archive

PdfContext::render_batch renders many templates in parallel and returns the resulting ZIP as Vec<u8>:

use std::sync::Arc;
use typst_webservice::{BatchRenderRequest, PdfContext};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Arc::new(PdfContext::from_directory("./assets")?);

    let requests = vec![
        BatchRenderRequest {
            template: "example.typ".to_string(),
            file_name: "first.pdf".to_string(),
            input: serde_json::json!({ "name": "One", "list": ["Item"] }),
        },
        BatchRenderRequest {
            template: "example.typ".to_string(),
            file_name: "second.pdf".to_string(),
            input: serde_json::json!({ "name": "Two", "list": ["Item"] }),
        },
    ];

    let zip_bytes = PdfContext::render_batch(context, requests).await?;
    std::fs::write("out.zip", zip_bytes)?;
    Ok(())
}

If any request references a template that is not loaded in the context, render_batch returns AppError::MainSourceNotFound before rendering starts. The call requires a Tokio runtime because rendering happens on a spawn_blocking pool and PDFs are fed through an async ZIP writer.

Streaming into your own writer

For large batches where you don't want to buffer the whole archive in memory, use render_batch_to_writer with any tokio::io::AsyncWrite:

use std::sync::Arc;
use tokio::fs::File;
use typst_webservice::{BatchRenderRequest, PdfContext};
use typst_webservice::zip::ZipResponseWriter;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Arc::new(PdfContext::from_directory("./assets")?);
    let requests: Vec<BatchRenderRequest> = /* ... */ vec![];

    let file = File::create("out.zip").await?;
    let writer = ZipResponseWriter::new(file);
    PdfContext::render_batch_to_writer(context, requests, writer).await?;
    Ok(())
}

render_batch_to_writer finishes (and shuts down) the writer before returning it, so the archive is complete as soon as the call resolves.

Running Tests

cargo test

Integration tests exercise both single and batch rendering flows using fixtures from the assets/ directory.

About

Predefined Typst template rendering wrapped in a small HTTP API

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors