The problem with LibreOffice headless
LibreOffice headless (libreoffice --headless --convert-to odt) is the traditional solution for server-side ODF generation and conversion. It works, but it has significant costs in production:
- Startup time — LibreOffice takes 2–5 seconds to start, even for small documents. Every cold start adds this latency.
- Installation size — LibreOffice is 300–500MB. It dominates Docker image sizes and increases build and deployment times.
- Serverless incompatibility — LibreOffice cannot run on AWS Lambda, Vercel Edge Functions, or Cloudflare Workers.
- Browser impossible — LibreOffice headless cannot run client-side under any circumstances.
- Concurrency issues — Concurrent conversions require separate home directories and careful process management.
- Security surface — Spawning a process with filesystem access from a web server requires careful sandboxing.
Install
$ npm install odf-kit
HTML → ODT (replaces --convert-to odt)
// Before: libreoffice --headless --convert-to odt document.html // After: import { htmlToOdt } from "odf-kit"; import { writeFileSync } from "fs"; const bytes = await htmlToOdt(htmlString, { pageFormat: "A4", metadata: { title: "My Document" }, }); writeFileSync("document.odt", bytes); // No process spawn, no 500MB install, works on Lambda
DOCX → ODT (replaces --convert-to odt)
// Before: libreoffice --headless --convert-to odt report.docx // After: import { docxToOdt } from "odf-kit/docx"; import { readFileSync, writeFileSync } from "fs"; const { bytes, warnings } = await docxToOdt(readFileSync("report.docx")); writeFileSync("report.odt", bytes); // Pure ESM, browser-safe, spec-validated against ECMA-376
ODT → HTML (replaces --convert-to html)
// Before: libreoffice --headless --convert-to html document.odt // After: import { odtToHtml } from "odf-kit/reader"; import { readFileSync } from "fs"; const html = odtToHtml(readFileSync("document.odt"), { fragment: true }); // Returns clean HTML string, no temp files
Generate ODT from scratch
import { OdtDocument } from "odf-kit"; const doc = new OdtDocument(); doc.setPageLayout({ width: "8.5in", height: "11in" }); doc.addHeading("Quarterly Report", 1); doc.addTable([["Region", "Revenue"], ["North", "$2.1M"]]); const bytes = await doc.save(); // Valid .odt, passes OASIS ODF validator
What LibreOffice still does better
odf-kit is not a complete drop-in for every LibreOffice use case. LibreOffice still has advantages for:
- ODT → PDF — LibreOffice produces high-fidelity PDF output. odf-kit converts ODT → Typst markup which then compiles to PDF via the Typst CLI — good quality but a two-step process.
- Complex DOCX fidelity — LibreOffice handles text boxes, SmartArt, charts, and embedded objects. odf-kit/docx covers the most common content but skips complex layout elements.
- Legacy formats — LibreOffice opens .doc, .xls, .ppt. odf-kit handles .docx and .xlsx only.
Comparison
| Capability | odf-kit | LibreOffice headless |
|---|---|---|
| Generate ODT from scratch | ✓ Pure JS | ✗ Not supported |
| HTML → ODT | ✓ Pure JS | ✓ Subprocess |
| DOCX → ODT | ✓ Pure JS, browser-safe | ✓ Higher fidelity |
| ODT → HTML | ✓ Pure JS | ✓ |
| ODT → PDF | Via Typst CLI | ✓ Direct |
| Browser support | ✓ Full | ✗ Impossible |
| Serverless (Lambda, Vercel) | ✓ | ✗ |
| Startup time | ✓ Milliseconds | ✗ 2–5 seconds |
| Installation size | ✓ ~50KB | ✗ 300–500MB |
| Concurrent conversions | ✓ No state | Requires separate profiles |
| Legacy formats (.doc, .xls) | ✗ | ✓ |