odf-kit/typst · v0.8.0 · Zero dependencies

Convert ODT to PDF in JavaScript
without LibreOffice

Use odf-kit's built-in Typst emitter to convert any .odt file to Typst markup, then compile to PDF with the Typst CLI. Pure JavaScript — no LibreOffice, no docxtemplater, no server dependencies.

The pipeline

odf-kit reads the .odt file and emits Typst markup. The Typst CLI compiles it to PDF. odf-kit handles the hard part — Typst handles the rendering.

.odt file
odtToTypst()
.typ markup
typst compile
.pdf

Try it online — no CLI needed

Convert your ODT file to Typst markup in your browser, then compile to PDF at typst.app. Free, private — your file never leaves your device.

Open the ODT to PDF tool →

Quick start

Install odf-kit and the Typst CLI:

# Install odf-kit
npm install odf-kit

# Install the Typst CLI (choose one)
npm install -g typst          # via npm
brew install typst            # macOS
winget install --id Typst.Typst  # Windows

Convert a .odt file to PDF:

import { odtToTypst } from "odf-kit/typst";
import { readFileSync, writeFileSync } from "node:fs";
import { execSync } from "node:child_process";

const bytes = new Uint8Array(readFileSync("document.odt"));

// Step 1 — convert .odt to Typst markup
const typ = odtToTypst(bytes);
writeFileSync("document.typ", typ);

// Step 2 — compile to PDF
execSync("typst compile document.typ document.pdf");

// document.pdf is ready

Parse once, emit to multiple formats

If you already have a parsed OdtDocumentModel — for example from readOdt() — use modelToTypst() directly to avoid re-parsing the file. This is the preferred approach when generating both HTML and PDF from the same document:

import { readOdt } from "odf-kit/reader";
import { modelToTypst } from "odf-kit/typst";
import { readFileSync, writeFileSync } from "node:fs";
import { execSync } from "node:child_process";

const bytes = new Uint8Array(readFileSync("document.odt"));

// Parse once
const model = readOdt(bytes);

// Emit to HTML
const html = model.toHtml({ fragment: true });
writeFileSync("document.html", html);

// Emit to Typst, compile to PDF
const typ = modelToTypst(model);
writeFileSync("document.typ", typ);
execSync("typst compile document.typ document.pdf");

What gets converted

ODT elementTypst outputStatus
Headings (levels 1–6)= H1, == H2, …
ParagraphsBlock text with blank-line separation
Text alignment#align(center)[…]
Bold*text*
Italic_text_
Underline#underline[text]
Strikethrough#strike[text]
Superscript / subscript#super[] / #sub[]
Hyperlinks#link("url")[text]
Font color#text(fill: rgb("…"))[…]
Font size#text(size: 14pt)[…]
Font family#text(font: "…")[…]
Highlight color#highlight(fill: rgb("…"))[…]
Unordered lists- item
Ordered lists+ item
Nested listsTwo-space indent per level
Tables#table(columns: N, …)
Table column widthscolumns: (5cm, 10cm)
Footnotes / endnotes#footnote[…]
Bookmarks<label>
Page number field#counter(page).display()
Page count field#counter(page).final().first()
Named sectionsComment header + body
Tracked changes#underline[] / #strike[] / transparent
Page geometry#set page(width:, height:, margin: (…))
ImagesComment placeholder (see below)Placeholder

Images in the PDF

Typst does not support inline base64 image data without filesystem access. Each image in the source .odt is emitted as a comment placeholder at its document position:

/* [image: logo 10cm × 6cm] */

To include images in the final PDF, extract the image data from the document model and write the files alongside the .typ output before compiling:

import { readOdt } from "odf-kit/reader";
import { modelToTypst } from "odf-kit/typst";
import { readFileSync, writeFileSync } from "node:fs";
import { execSync } from "node:child_process";

const bytes = new Uint8Array(readFileSync("document.odt"));
const model = readOdt(bytes);

// Extract images from the model and write them to disk
let typ = modelToTypst(model);
for (const node of model.body) {
  if (node.kind === "paragraph") {
    for (const span of node.spans) {
      if ("kind" in span && span.kind === "image" && span.name) {
        const ext = (span.mediaType ?? "image/png").split("/")[1];
        const filename = `${span.name}.${ext}`;
        writeFileSync(filename, Buffer.from(span.data, "base64"));
        // Substitute the placeholder with a real #image() call
        typ = typ.replace(
          `/* [image: ${span.name}`,
          `#image("${filename}") /* was:`
        );
      }
    }
  }
}

writeFileSync("document.typ", typ);
execSync("typst compile document.typ document.pdf");

Why not shell out to LibreOffice?

LibreOffice headless (libreoffice --headless --convert-to pdf) is the traditional approach. It works, but comes with real costs in production:

The Typst CLI is a single small binary that starts in milliseconds, produces deterministic output, and runs cleanly in containers. odf-kit's Typst emitter adds zero new JavaScript dependencies.

Comparison with other approaches

ApproachNo LibreOfficeNo subprocessServerlessnpm install
odf-kit + Typst CLI Typst only (container)
LibreOffice headless OS install
docxtemplater + LibreOffice Partial
Pandoc (ODT → PDF) Needs LaTeX OS install

Typst CLI installation options

# via npm (works on all platforms)
npm install -g typst

# macOS (Homebrew)
brew install typst

# Windows (winget)
winget install --id Typst.Typst

# Linux (many distros — check repology.org/project/typst)
sudo apt install typst       # Debian/Ubuntu (if available)
sudo pacman -S typst          # Arch Linux

# Verify installation
typst --version

ESM setup

odf-kit is ESM-only. Your script must use import syntax. Either add "type": "module" to your package.json, or use a .mjs file extension:

// package.json
{
  "type": "module"
}

// convert.mjs — or convert.js with "type": "module" above
import { odtToTypst } from "odf-kit/typst";
import { readFileSync, writeFileSync } from "node:fs";
import { execSync } from "node:child_process";

const [,, input, output] = process.argv;
const bytes = new Uint8Array(readFileSync(input));
writeFileSync("out.typ", odtToTypst(bytes));
execSync(`typst compile out.typ ${output}`);

odf-kit does more than convert

The same library that converts .odt files to PDF can also create .odt files from scratch, fill existing templates with data, and read .odt files as structured HTML. One install, one dependency.

Convert your first .odt to PDF

Install odf-kit and the Typst CLI. No LibreOffice required.

$ npm install odf-kit