---
url: 'https://adk.nht.io/batteries/media/fleet.md'
description: >-
  Every bundled engine with its trade-offs stated plainly: jimp, sharp, sheetjs,
  exceljs, data, tesseract.js, audio-decode, transformers ASR, soffice — and why
  there are two spreadsheet engines.
---

# The Bundled Fleet

## LLM summary — Bundled media engines

* Each engine ships from its own subpath with an optional peer, lazy-loaded inside capability methods (constructing the pipeline never loads heavy deps).
* engines/jimp → `jimpEngine()` → mutate over png/jpg/bmp/gif/tiff + generate EMPTY\_MIME→png/jpg/jpeg/bmp/gif/tiff (1024×1024 white). Cross-env pure JS. No webp/avif; slower than sharp; re-encode strips EXIF inherently.
* engines/sharp → `sharpEngine()` + `fromSharp(sharp)` → mutate over `image/*` incl. webp/avif encodes + generate EMPTY\_MIME→those. Node-only (native bindings). Canvas is 3-channel so jpeg never trips on alpha.
* engines/sheetjs → `sheetjsEngine()` → convert FULL spreadsheet matrix (read xlsx/xlsm/xlsb/xls-all-BIFFs/ods/fods/csv/NUMBERS/sylk/dif/dbf; write those + txt/html/rtf/json) + generate EMPTY\_MIME→any write target + edit all `sheet.*` ops over the read matrix. Cross-env pure JS. ⚠️ SheetJS CE STRIPS STYLING (fonts/fills/comments — Pro feature). ⚠️ Install `xlsx` >=0.20.2 FROM THE SHEETJS CDN (`pnpm add xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz`) — the npm registry copy is frozen at 0.18.5 with CVE-2023-30533 + CVE-2024-22363. NUMBERS write lazy-loads the ~110 KB ZAHL payload.
* engines/exceljs → `exceljsEngine()` → edit all `sheet.*` ops over xlsx STYLING-PRESERVED + generate EMPTY\_MIME→xlsx. Cross-env pure JS. xlsx only (silently reads ODS as zero worksheets, throws on legacy XLS — why it lost the conversion job). Compose FIRST when formatting matters.
* engines/data → `dataEngine()` → convert json⇄yaml, json⇄csv (papaparse peer, lazy), json→txt + generate EMPTY\_MIME→txt/md/json/yaml/csv/html. Cross-env, smallest engine in the fleet.
* engines/tesseract\_js → `tesseractJsEngine({ languages })` → convert `image/*`→txt/hocr/json. Cross-env WASM. languages REQUIRED (packs download on first use; point langPath/cachePath deliberately). Worker per call.
* engines/audio\_decode → `audioDecodeEngine()` → convert mp3/aac/ogg/opus/flac/wav→pcm + generate EMPTY\_MIME→wav (1 s 16-bit mono silence, 44100 Hz, dependency-free — generation never loads the decode peer). PCM carries source rate in meta.sampleRate.
* engines/transformers\_asr → `transformersAsrEngine({ model })` → convert pcm→txt/srt/vtt/json. Cross-env ONNX/WASM/WebGPU. model REQUIRED (multi-hundred-MB downloads never triggered silently).
* engines/soffice → `sofficeEngine({ path, executor, workspace })` → convert: the office matrix (each silo to pdf/html + within-silo; pdf→html/txt/docx/odt; ODS/xls→xlsx rides the matrix) + generate EMPTY\_MIME→whole matrix via zero-byte seed files. LibreOffice binary — the one capability with no cross-env equal.
* engines/execa\_executor → `execaExecutor()` (BinaryExecutor, Node); engines/fs\_workspace → `fsScratchWorkspace({ root })` (ScratchWorkspace, Node; root REQUIRED — no tmpdir default).
* Spreadsheet division of labor: sheetjs CONVERTS (format crossings are already lossy; widest matrix), exceljs EDITS (same-format edits must preserve what they don't touch). Both declare `sheet.*` edits — array order picks: exceljs-first = fidelity, sheetjs-only = breadth without the second peer.

Every row in this table has a downside, and the downside is in the table. If a column reads like marketing, tell us — that's a bug. (The capability shapes these rows declare are [The Capability Model](./engines); how dispatch chooses among them is [Engine Dispatch](./dispatch); what `EMPTY_MIME` generation means is [Generating Media](./generating).)

| Subpath | Factory | Environment | Declares | Trade-offs, stated plainly |
| --- | --- | --- | --- | --- |
| `engines/jimp` | `jimpEngine()` | **Cross-env** (pure JS) | mutate over png/jpg/bmp/gif/tiff; generate (`EMPTY_MIME` → blank canvas, any supported encoding) | No webp/avif. Slower than sharp. Re-encoding strips EXIF inherently. Blank canvas is fixed 1024×1024 white — resize in the same statement. |
| `engines/sharp` | `sharpEngine()`, `fromSharp(sharp)` | Node only (native bindings) | mutate over `image/*`, encodes incl. webp/avif; generate (`EMPTY_MIME` → blank canvas) | Full format set, full `fit` modes, native speed. Canvas is 3-channel so jpeg never trips on alpha. |
| `engines/sheetjs` | `sheetjsEngine()` | **Cross-env** (pure JS) | convert: the FULL spreadsheet matrix (xlsx/xlsm/xlsb/xls/ods/fods/csv/numbers/sylk/dif/dbf in; those plus txt/html/rtf/json out); generate (`EMPTY_MIME` → any write target); edit: every `sheet.*` op over the whole read matrix | SheetJS. **Install from the SheetJS CDN, not the npm registry** — `pnpm add xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz` (the registry copy is frozen at 0.18.5 with two CVEs; the engine floors at `>=0.20.2`; SheetJS recommends vendoring the tarball). **CE strips styling** — fonts, fills, comments do not survive its edits or round-trips (styling is a SheetJS Pro feature). NUMBERS output lazy-loads a ~110 KB ZAHL payload. |
| `engines/exceljs` | `exceljsEngine()` | **Cross-env** (pure JS) | edit: every `sheet.*` op over xlsx, **styling preserved**; generate (`EMPTY_MIME` → xlsx) | xlsx only — it silently reads a real ODS as zero worksheets and throws on legacy XLS, which is why it lost the conversion job to sheetjs. What it keeps is fidelity: bold, fills, comments, formulas survive an edit untouched. |
| `engines/data` | `dataEngine()` | **Cross-env** (pure JS) | convert: json ⇄ yaml, json ⇄ csv, json → txt; generate (`EMPTY_MIME` → txt/md/json/yaml/csv/html) | The smallest engine in the fleet, deliberately: text-family generation is a literal and text-family conversion is a parse and a re-serialize. csv edges lazy-load the `papaparse` peer; yaml rides the library's own `js-yaml`. |
| `engines/tesseract_js` | `tesseractJsEngine({ languages })` | **Cross-env** (WASM) | convert `image/*` → txt/hocr/json | `languages` is required — packs download on first use (point `langPath`/`cachePath` somewhere deliberate). Slower than the native CLI. Worker per call: leak-free, not pooled. |
| `engines/audio_decode` | `audioDecodeEngine()` | **Cross-env** (JS/WASM codecs) | convert mp3/aac/ogg/opus/flac/wav → `pcm`; generate (`EMPTY_MIME` → wav) | No ffmpeg anywhere. PCM output carries the source rate in `meta.sampleRate`; the transcribe step resamples to 16 kHz. The generated wav is one second of 16-bit mono silence at 44100 Hz — a canonical RIFF envelope written by a dependency-free helper (generation never loads the decode peer). |
| `engines/transformers_asr` | `transformersAsrEngine({ model })` | **Cross-env** (ONNX / WASM / WebGPU) | convert `pcm` → txt/srt/vtt/json | `model` is required — Whisper models are multi-hundred-MB downloads and we never trigger one silently. srt/vtt assembled from chunk timestamps. |
| `engines/soffice` | `sofficeEngine(opts)` | Binary-backed | convert: the office matrix (each silo to pdf/html + within-silo; pdf → html/txt/docx/odt; **ODS/xls → xlsx rides the matrix** — there is no separate "normalize" engine anymore); generate (`EMPTY_MIME` → its whole matrix) | LibreOffice. Document conversion has **no mature cross-environment equivalent** — the candidates are a 200 MB browser-only beta and an AGPL license you'd have to explain to counsel. This is the honest exception, and it's why the [runtime contracts](./runtime) exist. |
| `engines/execa_executor` | `execaExecutor()` | Node | — (a `BinaryExecutor`, not an engine) | The local-process executor (execa under the hood, via resolver). |
| `engines/fs_workspace` | `fsScratchWorkspace({ root })` | Node | — (a `ScratchWorkspace`, not an engine) | `root` is required — there is no `os.tmpdir()` silent default. |

::: tip Catch missing peers at lint time, not at runtime
Every engine in this table that has an optional peer will fail with `E_INVALID_MEDIA_PIPELINE_CONFIG` the first time it runs if that peer isn't installed — not at pipeline construction, not at import, at the first op. The `adk-media/require-engine-peers` ESLint rule catches this at lint time instead: it checks that every peer for every engine you import resolves from your file's location, and reports the exact `pnpm add` command to fix it (including the CDN URL for `xlsx`). Enable it via the media battery's ESLint plugin — see [Developer Tools → ESLint Rules](../../developer-tools/eslint-rules).
:::

::: danger Install `xlsx` from the SheetJS CDN — `npm i xlsx` is a trap
`pnpm add xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz`. Not `pnpm add xlsx`. Here is the situation, stated plainly because someone is going to skim this and type the wrong command: the version of `xlsx` on the public npm registry is **0.18.5, last published in 2023**, and it carries two known CVEs (CVE-2023-30533 prototype pollution, CVE-2024-22363 ReDoS). SheetJS stopped publishing to npm and ships current builds — the ones with the fixes and the formats this engine needs — only from `cdn.sheetjs.com`. So the single most obvious thing a developer does to install a dependency installs a stale, vulnerable artifact, and the registry will hand it over with a straight face and no warning. We did not create this; we are not happy about it; and the only thing worse than tolerating it would be picking a worse-aligned library to avoid typing a URL. The engine floors at `>=0.20.2` so a 0.18.5 install fails fast at construction rather than mysteriously at runtime, and SheetJS recommends vendoring the tarball into your repo so you're not load-bearing on a CDN. Read [their installation page](https://docs.sheetjs.com/docs/getting-started/installation/frameworks); it says the same thing in calmer words.
:::

::: info Field note: the library that lost and the registry that lies
ExcelJS was the incumbent candidate for spreadsheet conversion (already the `sheet.*` workhorse) until empirical testing showed its real conversion matrix is xlsx/csv only — `xlsx.load()` on a real ODS *silently returns a zero-worksheet workbook* (it's a zip, so nothing throws; silent garbage, worse than an error), and legacy XLS throws `Can't find end of central directory`. SheetJS reads ODS, every XLS BIFF era, NUMBERS, SYLK, DIF, DBF. So the loser of "best conversion matrix" became the winner of "fidelity-preserving editor" and they both stayed — each doing the one job it's actually good at, neither pretending to do the other.
:::

## Two spreadsheet engines: the consumer picks the trade-off

The obvious question — "doesn't SheetJS just replace ExcelJS?" — got an empirical answer during development. Round-trip a styled workbook (bold red header, yellow fill, a comment, a formula) through ExcelJS: everything survives. Round-trip the same bytes through SheetJS CE with `cellStyles: true`: `bold: null, fill: null`, comment gutted — styling is a SheetJS *Pro* feature, and CE silently drops what it can't model. An edit that strips a user's formatting as a side effect is corruption with good intentions — but whether that matters is a property of YOUR workload, not ours, so both engines declare the edit capability and array order picks:

| You edit… | Compose | Why |
| --- | --- | --- |
| Human-authored workbooks (formatting matters) | `exceljsEngine()` first (or alone) | Edits preserve the styling they don't touch. |
| Data workbooks, CSV-land pipelines | `sheetjsEngine()` alone | The full read matrix in-process (ODS, XLS, NUMBERS, …), no second dependency, and there's no styling to lose. |
| Both kinds | `exceljsEngine(), sheetjsEngine()` | exceljs takes xlsx edits (fidelity); sheetjs takes everything exceljs can't read, plus all conversions. |

This is the [edit capability's fidelity-arbitration design](./engines#three-capabilities-thats-the-whole-taxonomy) doing exactly what it was built for: two engines truthfully declaring the same ops, supply order picking the winner, and the trade-off visible in your engines array instead of buried in a library's release notes.

Worth saying out loud: SheetJS's stated philosophy (the ["Zen of SheetJS"](https://docs.sheetjs.com/docs/getting-started/zen)) is the same bet this battery makes — *data processing should fit in any workflow* (no imposed lifecycle, plain objects) is BYO-everything; *file formats are implementation details* (HTML-saved-as-XLS parses like real XLS) is exactly the registry's MIME-edges-not-format-special-cases model; *data processing should be confidential* (everything local, zero telemetry) is the battery's in-process posture. The CDN-install friction is worth tolerating for a library that aligned this well before we ever met it.

## Read next

* [Generating Media](./generating) — what every "generate" cell in the table above actually means.
* [BYO Runtime & Custom Engines](./runtime) — the contracts behind `engines/soffice`, and rolling your own.
