Typst-bake — Embed Typst templates, fonts, and packages into your Rust binary

(Meet our mascot — a freshly baked T-shaped bread!)

I wanted to use Typst as a fully self-contained, embedded engine inside a Rust application — no network access, no external files at runtime. typst-as-lib is a great library, but for this specific use case, embedding packages took some extra work: manually downloading every package and its transitive dependencies, bundling them with include_dir!, copying them to a temp folder at runtime, and wiring them through the filesystem resolver.

I tried to contribute this automation upstream, but realized the scope was better suited as a standalone crate — and typst-bake was born.

What it does

At compile time, typst-bake automatically scans your .typ files for package imports, downloads and resolves all dependencies, compresses everything with zstd, and embeds it all into your binary. At runtime, generating a PDF is just:

let doc = typst_bake::document!("main.typ");
let pdf = doc.to_pdf()?;

Key features

  • Simple API - Set template-dir and fonts-dir in Cargo.toml, then generate documents with just document!("main.typ").to_pdf()
  • Multi-Format Output - Generate PDF, SVG, or PNG from the same template
  • Self-Contained Binary - Templates, fonts, and packages are all embedded into the binary at compile time. No external files or internet connection needed at runtime
  • Automatic Package Resolution - Just use #import "@preview/..." as in Typst. Packages are resolved automatically using Typst’s own cache and data directories
  • Runtime Inputs - Pass dynamic data from Rust structs to Typst via IntoValue / IntoDict derive macros
  • Optimized Binary Size - Embedded resources are deduplicated and compressed automatically

Architecture

Architecture diagram

Click the diagram for a detailed PDF version.

Examples

Each example is a runnable project with a PDF that explains its own usage in detail:

More examples (fonts, file embedding, compression benchmarks, output formats) are in the GitHub repo.

Links

Built on top of typst-as-lib. Feedback and suggestions are very welcome!

14 Likes

That looks really cool! I won’t necessarily need this soon, but if I were to use it, this feature would be great (and if you don’t add it yourself I’d PR it once I need it, so no pressure):

The flow chart shows that you skip downloading if you find packages in cache. In principle, that means that you could support non-Universe packages as long as they’re downloaded in advance. You’d just need to look in the local packages folder in addition to the cache folder, and would need to not limit package search to the @preview namespace.

2 Likes

I am using this in GitHub - sermuns/skrytsam: Generate stats for your GitHub profile README. Used for flexing on friends/enemies..

Thank you for making this, and quickly addressing my issue!

1 Like

Thanks, really appreciate the suggestion!

You’re right — currently typst-bake uses its own separate cache path ({cache-dir}/typst-bake/packages/), which means it doesn’t benefit from packages Typst has already downloaded, and there’s no way to use local packages either.

I’m going to align it with Typst’s official package resolution — so it’ll check the data directory first for local packages, then fall back to the shared cache directory, and only download if needed. I’ll also lift the @preview namespace restriction so that any namespace (like @local) works as you’d expect.

This should make the whole experience feel much more consistent with Typst itself. Thanks for nudging me in this direction!

1 Like

Thanks for flagging that — it was a pretty critical bug!

And it’s great to see typst-bake being used in skrytsam, hope it keeps working well for you!

Updated the architecture diagram and implementation to reflect your suggestion — typst-bake now checks local packages first and supports all namespaces. Thanks again!

1 Like

This looks like a really cool project, thanks! Given how you deep-dived into package resolution here, maybe you are the right person to ask for input for this issue, see also this forum post I made. Maybe you have some ideas there ;)

1 Like

Thanks for the kind words and for sharing those links!

I’m not really an expert on WASM or browser-side package resolution, but I did some research (with AI assistance) and here’s what I found:

First, good news: packages.typst.org already serves Access-Control-Allow-Origin: *, so a fully static-hosted solution should be feasible.

The overall flow for a browser-based implementation would look something like:

  1. Parse the Typst source to find #import "@namespace/name:version" statements
  2. fetch() the tar.gz from packages.typst.org/{namespace}/{name}-{version}.tar.gz
  3. Decompress in memory (e.g. with pako + an untar library)
  4. Parse typst.toml from the package to resolve transitive dependencies
  5. Recursively repeat for all dependencies
  6. Mount everything to a virtual filesystem and pass it to the Typst WASM compiler

For caching, you could store downloaded packages in IndexedDB to avoid re-fetching on every session.

This is roughly the same package resolution flow that typst-bake does at compile time, but done at runtime in the browser. The typst-bake source (especially the package resolution module) might serve as a reference for the dependency walking logic. Feel free to take a look!

Corrections are welcome if I got anything wrong!

2 Likes

I’m neither an expert on WASM, so thanks a lot for your research!