Envisia.Typst 0.2.1

dotnet add package Envisia.Typst --version 0.2.1
                    
NuGet\Install-Package Envisia.Typst -Version 0.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Envisia.Typst" Version="0.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Envisia.Typst" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="Envisia.Typst" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Envisia.Typst --version 0.2.1
                    
#r "nuget: Envisia.Typst, 0.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Envisia.Typst@0.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Envisia.Typst&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=Envisia.Typst&version=0.2.1
                    
Install as a Cake Tool

Envisia.Typst

Compiles Typst markup to PDF from .NET, in process. The package bundles the Typst compiler (0.15.1) as a native library, so there is no CLI to install and no process to spawn.

dotnet add package Envisia.Typst

Supported runtimes: linux-x64, linux-arm64 (glibc 2.28 or newer), osx-x64, osx-arm64, win-x64 and win-arm64, on .NET 10. Each native library is about 35 MB. Publish with a runtime identifier (dotnet publish -r linux-x64, or a container build) so only the matching one ends up in the output; a publish without one carries all six.

Usage

var pdf = TypstCompiler.CompilePdf(
    new TypstCompileRequest
    {
        Markup = "#set text(font: \"Open Sans\")\n#text(\"Hallo\")",
        Fonts = [new TypstFont(openSansBytes)],
        Files = [new TypstFile("logo.svg", svgBytes)],
        Today = new DateOnly(2024, 5, 17),
    });
  • TypstCompiler.CompilePdf returns the PDF bytes or throws TypstCompileException, which carries Typst's own diagnostics (with source positions and hints) in Diagnostics.
  • Everything the document needs is passed in. The compiler reads no fonts from the machine, opens no files and makes no network calls, so a rendered document depends only on its inputs. No font is bundled either: pass at least one.
  • Today feeds Typst's datetime.today(). Left unset that call fails to compile rather than silently reading a wall clock inside the renderer.
  • A document is expected to be a fixed template that reads its data from a file handed in through Files, for example #let d = json("data.json"). A value read that way is data: Typst shows a string as text instead of parsing it, so untrusted input can never turn into executable markup. Files also serves #import, so a template can be split across several documents.
  • Creator sets the application the PDF names as its creator (/Creator and XMP CreatorTool). Left unset, Typst names itself (Typst 0.15.1); an empty string leaves the entry out. Typst writes no /Producer, and typst-pdf offers no way to set one.
  • Standards makes Typst enforce PDF standards, for example [TypstPdfStandard.PdfA3b] for an archivable PDF/A-3b. Typst checks conformance while it writes the file and throws TypstCompileException when the document violates one (PDF/A needs a creation date, #set document(date: datetime.today()) with Today set). At most one PDF/A and one PDF/UA standard can be combined, plus a PDF version both allow. Left empty the output is PDF 1.7.
  • Tagged (on by default, as in Typst) writes a structure tree for screen readers. PDF/UA and the PDF/A "a" levels need it; a long report nobody reads with assistive technology renders faster and smaller without it.
  • CompilePdf blocks the calling thread while Typst compiles.

Characters the fonts cannot show

Typst only has the fonts it is handed. A character none of them has is drawn as the font's missing glyph box, and under a PDF/A standard Typst refuses the whole document (the text ... could not be displayed). User supplied text can go through TypstFontCoverage first:

var coverage = TypstFontCoverage.Of(fonts); // once per set of fonts, safe to share
var text = coverage.Displayable(userText);

Of reads the cmap tables of the fonts (every face of a collection). Displayable keeps what they can show and replaces the rest where there is an obvious substitute: other spaces become a space, line and paragraph separators a line break, the hyphens that only differ in their break behaviour a hyphen-minus, ←/→/⇒ become <-/->/=> and characters with a compatibility decomposition (₂, full width forms) their decomposition. Anything else, such as emoji, symbol font code points or scripts the fonts lack, is dropped. Text that needs no change is returned as is.

Thread safety

CompilePdf can be called from any number of threads at once. The binding keeps no state between calls: every call builds its own Typst world from the request's markup, fonts, files, date and creator, and nothing of it outlives the call. A test compiles eight different documents that share a file name on twelve threads and checks every PDF is byte for byte what the same request produces on its own.

Typst itself keeps three things process wide, which a binding cannot scope to one call:

  • comemo's memoization cache. It is lock protected, and a cached result is only reused when every input Typst tracked is identical, so it can make a call faster but never hand it another call's data. The native layer evicts it after every document without taking what running calls still use (see below), so calls running in parallel keep their cache hits.
  • The interner for file names. Each distinct name stays allocated for the life of the process, so use stable names such as data.json rather than a new name per call.
  • rayon's global thread pool, which Typst lays out and exports on in parallel.

Native layer

src/Envisia.Typst/native/ is a Rust cdylib that implements Typst's World over the caller's fonts and files.

The C ABI is deliberately small:

Symbol Purpose
envisia_typst_abi_version Version handshake, checked on every call.
envisia_typst_compile_pdf Compiles markup into an EvTypstResult.
envisia_typst_result_free Releases everything the result owns.

Memory ownership: the caller owns the input buffers and only has to keep them alive for the duration of the call (the managed side pins them). Everything in the result is allocated and freed by Rust; the managed side copies the bytes out and always calls envisia_typst_result_free, including on the error paths. A Rust panic is caught at the boundary and reported as a status rather than unwinding into the CLR.

Typst memoizes layout in comemo's process wide cache. Each document here is rendered once, and a large one would otherwise stay resident until later calls aged it out, so the native layer evicts the cache whenever a call ends, also when it panics. The call that ends last clears it (comemo::evict(0)). A call that ends while others still run only evicts entries older than the oldest of them: every eviction ages each entry by one and a cache hit resets it, so what a running call has used since it started stays. Clearing everything instead makes a call lose the layout its next pass reuses whenever another one ends next to it. Measured on macOS arm64: a 2,000 entry report rendered next to a stream of small ones took 1.8 s instead of 1.1 s (1.0 s on its own), and four threads rendering 1,000 entry reports back to back finished 120 of them in 40 s instead of 25 s. The price is memory while calls overlap, up to about 1.3 GB of native memory in use in that run instead of 0.5 GB; once no call runs the cache is empty either way.

The crate must depend on the comemo version Typst itself uses; with another one comemo::evict clears a cache Typst never fills and the memory grows with every distinct document. same_comemo_as_typst in lib.rs turns such a mismatch into a build error.

Building

A local dotnet build or dotnet test compiles the native library for the host platform with cargo build --release (through Envisia.Typst.Native.targets) and copies it next to the assembly.

  • A Rust toolchain is required. native/rust-toolchain.toml pins the channel.
  • EnvisiaTypstSkipNativeBuild=true reuses whatever is in native/target.
  • EnvisiaTypstLibraryPath=<path> uses a prebuilt library instead of building one.

A cold cargo build --release compiles roughly 300 crates. Subsequent builds are incremental and cost well under a second when nothing changed.

dotnet pack does not compile Rust. It takes one prebuilt library per runtime from EnvisiaTypstRuntimesDirectory (laid out as <rid>/<library>) and fails when one is missing; EnvisiaTypstAllowMissingRuntimes=true packs whatever is there, for a local test package.

CI and releases

  • .github/workflows/build.yml builds the native library on each platform's own runner (Linux inside the manylinux_2_28 images, so the .so runs on glibc 2.28 and newer), packs them into one package and runs the tests on every platform against that package.
  • ci.yml runs it for pushes to main and pull requests.
  • publish.yml runs it for every pushed tag and pushes the package to nuget.org through trusted publishing (policy for publish.yml, environment release). The tag is the version: v1.2.3, 1.2.3 or a prerelease such as v1.2.3-beta.1; any other tag fails the workflow before anything is built.

To release, tag the commit and push the tag:

git tag v0.1.0
git push origin v0.1.0

Licenses

Envisia.Typst is MIT licensed, see LICENSE. The native library contains Typst (Apache-2.0) and about 290 Rust crates under permissive licenses; THIRD-PARTY-NOTICES.md lists them and ships in the package. CI regenerates it with cargo about and fails when it is out of date. The test font is Open Sans under the SIL Open Font License, see tests/Envisia.Typst.Tests/Fonts/OFL.txt.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.1 155 9/28/2026
0.2.0 113 9/25/2026
0.1.0 111 9/25/2026