Skip to content

A fast WebAssembly LaTeX compiler. TeX Live 2025 in your browser with lazy loading and on-demand package resolution.

License

Notifications You must be signed in to change notification settings

SiglumProject/siglum

Repository files navigation

Siglum

npm version license

A browser-based LaTeX compiler. TeX Live 2025 running in WebAssembly, with lazy bundle loading and on-demand package fetching.

On initialization, the engine downloads:

  • WASM binary (~29MB): the TeX engine
  • Core bundles (~16MB for pdfLaTeX): format files, fonts, base packages

Common packages (amsmath, tikz, biblatex, etc.) are pre-bundled for fast loading. Less common packages are fetched from TexLive/CTAN automatically during compilation. Everything is cached in the browser for offline use.

The full bundle is ~195MB to deploy, but clients only download what their documents need.

Guides:

Quick Start (Local Demo)

Clone the repo, download the WASM engine and pre-built bundles from cdn.siglum.org (or GitHub Releases), then start the dev server.

We use Bun for development, it's faster than Node.js and runs TypeScript directly. Install it with:

curl -fsSL https://bun.sh/install | bash

Then:

git clone git@github.com:SiglumProject/siglum-engine.git
cd siglum-engine
bun install

# Download WASM and bundles
mkdir -p busytex/build/wasm
curl -Lo busytex/build/wasm/busytex.wasm https://cdn.siglum.org/tl2025/busytex.wasm
curl -Lo busytex/build/wasm/busytex.js https://cdn.siglum.org/tl2025/busytex.js
curl -LO https://cdn.siglum.org/tl2025/siglum-bundles-v0.1.0.tar.gz
tar -xzf siglum-bundles-v0.1.0.tar.gz -C packages/

# Start dev server
bun serve-local.ts

Open http://localhost:8787 to try the playground.

For disk-persistent package caching, run the CTAN proxy in a separate terminal:

bun packages/ctan-proxy.ts

Installation

npm install @siglum/engine

Download Runtime Assets

Download WASM and bundles from cdn.siglum.org or GitHub Releases:

curl -LO https://cdn.siglum.org/tl2025/busytex.wasm
curl -LO https://cdn.siglum.org/tl2025/busytex.js
curl -LO https://cdn.siglum.org/tl2025/siglum-bundles-v0.1.0.tar.gz

# Extract to your public directory
tar -xzf siglum-bundles-v0.1.0.tar.gz -C public/
mv busytex.wasm busytex.js public/

Then configure:

import { SiglumCompiler } from '@siglum/engine';

const compiler = new SiglumCompiler({
    bundlesUrl: '/bundles',
    wasmUrl: '/busytex.wasm',
});

Or use the CDN directly (no self-hosting required):

const compiler = new SiglumCompiler({
    bundlesUrl: 'https://cdn.siglum.org/tl2025/bundles',
    wasmUrl: 'https://cdn.siglum.org/tl2025/busytex.wasm',
});

Usage

Note: If using a bundler (Vite, Webpack, etc.), see Usage with Bundlers for required setup.

import { SiglumCompiler } from '@siglum/engine';

const compiler = new SiglumCompiler({
    bundlesUrl: '/bundles',
    wasmUrl: '/wasm/busytex.wasm',
});

await compiler.init();

const result = await compiler.compile(`
\\documentclass{article}
\\begin{document}
Hello, World!
\\end{document}
`);

if (result.success) {
    const blob = new Blob([result.pdf], { type: 'application/pdf' });
    window.open(URL.createObjectURL(blob));
}

API

new SiglumCompiler(options)

const compiler = new SiglumCompiler({
    // URLs
    bundlesUrl: '/bundles',           // URL to bundle files
    wasmUrl: '/wasm/busytex.wasm',    // URL to WASM binary
    jsUrl: null,                      // URL to busytex.js (derived from wasmUrl if null)
    ctanProxyUrl: null,               // CTAN proxy URL (enables CTAN fetching when set)
    workerUrl: null,                  // Custom worker URL (required for bundlers)

    // Feature flags
    enableCtan: false,                // Auto-enabled when ctanProxyUrl is set
    enableLazyFS: true,               // Load files on-demand (faster startup)
    enableDocCache: true,             // Cache compiled documents by preamble hash

    // Performance tuning
    maxRetries: 15,                   // Max retries for CTAN/bundle fetches per compile
    eagerBundles: {},                 // Bundles to load immediately (see below)
    verbose: false,                   // Log TeX stdout (disable for performance)

    // Callbacks
    onLog: (msg) => {},               // Log callback
    onProgress: (stage, detail) => {},  // Progress callback
});

eagerBundles

By default, large bundles like cm-super (fonts) are loaded on-demand when TeX requests them. This saves bandwidth but adds latency on first use. To pre-load specific bundles:

// Load cm-super fonts eagerly for all engines
const compiler = new SiglumCompiler({
    eagerBundles: ['cm-super'],
});

// Or per-engine configuration
const compiler = new SiglumCompiler({
    eagerBundles: {
        pdflatex: ['cm-super'],
        xelatex: [],  // XeLaTeX uses system fonts
    },
});

maxRetries

Controls how many times the compiler retries when a package or bundle fetch fails. With pre-scanning (which batch-fetches most packages before compilation), retries are rare. Lower values fail faster if something is broken:

const compiler = new SiglumCompiler({
    maxRetries: 5,  // Fail fast (default: 15)
});

verbose

Controls whether TeX stdout is sent to the onLog callback. Disabled by default for performance—a typical compilation generates ~4,000 log lines, each requiring a postMessage call from the worker.

const compiler = new SiglumCompiler({
    verbose: true,  // Enable TeX stdout logging (default: false)
    onLog: (msg) => console.log(msg),
});

// Can also be changed after instantiation
compiler.verbose = true;   // Enable for next compile
await compiler.compile(source);
compiler.verbose = false;  // Disable again

When verbose: false:

  • TeX stdout ([TeX] ...) is suppressed
  • TeX errors ([TeX ERR] ...) are always logged
  • Worker status messages are always logged
  • Error detection still works (stdout is captured internally)

compiler.compile(source, options?)

const result = await compiler.compile(source, {
    engine: 'pdflatex',  // 'pdflatex' | 'xelatex' | 'auto'
    additionalFiles: {   // Include custom files
        'mypackage.sty': '\\ProvidesPackage{mypackage}...',
        'image.png': uint8Array,
    },
});

// result.success  — boolean
// result.pdf      — Uint8Array (if successful)
// result.log      — TeX log output
// result.error    — error message (if failed)

compiler.clearCache()

Clear all cached packages and compiled PDFs.

compiler.unload()

Free memory by unloading the WASM module. Call init() again to reload.

createBatchedLogger(onFlush)

Helper to batch log messages and avoid DOM thrashing. The TeX compiler emits hundreds of log lines during compilation and updating the DOM on each message can cause significant slowdowns.

import { SiglumCompiler, createBatchedLogger } from '@siglum/engine';

const compiler = new SiglumCompiler({
    bundlesUrl: '/bundles',
    wasmUrl: '/wasm/busytex.wasm',
    onLog: createBatchedLogger((messages) => {
        // Called once per animation frame with all buffered messages
        logDiv.textContent += messages.join('\n') + '\n';
        logDiv.scrollTop = logDiv.scrollHeight;
    }),
});

Performance Tips

Batch log updates

If you're displaying compiler logs in the UI, always use createBatchedLogger or implement your own batching. Unbatched DOM updates can add 2-3 seconds to compilation time.

Pre-warm the compiler

Call compiler.init() early (e.g., on page load) so bundles are ready when the user compiles:

// On page load
const compiler = new SiglumCompiler(options);
compiler.init(); // Fire and forget — bundles download in background

// Later, when user clicks compile
await compiler.compile(source); // Already warmed up

Usage with Bundlers (Vite, Webpack, etc.)

Bundlers pre-bundle dependencies, which breaks the automatic worker loading. You need to copy the worker file and pass an explicit URL:

// package.json
{
  "scripts": {
    "postinstall": "cp node_modules/@siglum/engine/src/worker.js public/worker.js"
  }
}
const compiler = new SiglumCompiler({
    bundlesUrl: '/bundles',
    wasmUrl: '/busytex.wasm',
    workerUrl: '/worker.js',  // Explicit path to copied worker
});

The postinstall script runs automatically on npm install, keeping the worker in sync with the package version.

Engines

Engine Status
pdfLaTeX Full support, format caching
XeLaTeX Full support, custom fonts via fontspec

Use engine: 'auto' to auto-detect based on document content.

Hosting / Production

Download assets from cdn.siglum.org or GitHub Releases:

Asset Size Description
busytex.wasm 29 MB WebAssembly TeX engine
busytex.js 292 KB Emscripten glue code
siglum-bundles-v0.1.0.tar.gz ~195 MB LaTeX packages & fonts

Serve with these headers (required for SharedArrayBuffer):

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Access-Control-Allow-Origin: *
Cross-Origin-Resource-Policy: cross-origin

Expected structure after extracting:

your-server.com/
├── busytex.wasm
├── busytex.js
└── bundles/
    ├── bundles.json
    ├── file-manifest.json
    ├── file-to-package.json
    ├── package-deps.json
    └── *.data.gz (54 bundle files)

CTAN Proxy

The CTAN proxy fetches missing LaTeX packages on-demand:

bun packages/ctan-proxy.ts

Packages are cached permanently. The proxy tries TexLive 2025 archives first, then falls back to CTAN mirrors.

For configuration and deployment options, see docs/ctan-proxy.md.

Browser Requirements

  • Modern browser with WebAssembly support
  • SharedArrayBuffer (requires COOP/COEP headers)
  • ~512MB RAM for compilation (max heap size)

Acknowledgments

Built on BusyTeX.

License

MIT

About

A fast WebAssembly LaTeX compiler. TeX Live 2025 in your browser with lazy loading and on-demand package resolution.

Topics

Resources

License

Stars

Watchers

Forks