Native Node.js / Bun bindings for
docling.rs — a Rust port of
docling. Convert Markdown, HTML,
DOCX, PPTX, XLSX, EPUB, ODF, LaTeX, email, PDF, images and more into a unified
DoclingDocument, and export it as Markdown or docling-core JSON.
Built with napi-rs, so it ships a real native addon (.node)
that loads in both Node.js and Bun (Bun implements N-API) — the same binary, no
rebuild between runtimes.
Released versions ship prebuilt native binaries, so no Rust toolchain is needed to use the package:
npm install docling.rs # or: bun add docling.rsPrebuilt platforms: Linux x64 / arm64 (glibc) and Windows x64. (macOS isn't
prebuilt — build from source, see below.) The right binary is pulled in
automatically as a platform-specific optionalDependency (docling.rs-<triple>). Releases are published to npm by
manually running the npm publish workflow
(.github/workflows/npm-publish.yml) — by default it builds the latest master
(the workspace version); optionally pass a release tag to build that instead.
Decoupled from the crates.io release.
This package lives in the docling.rs Cargo workspace and can also build the addon from Rust source — needed for local development or an unsupported platform. You need a Rust toolchain (1.82+) and Node.js 14+ (or Bun).
cd crates/docling-node
npm install # installs @napi-rs/cli
npm run build # release build → docling.rs.<platform>.node + native.js/.d.ts
# npm run build:debug # faster, unoptimizedThe addon statically links the ONNX runtime used by the PDF/image pipeline, so the built
.nodeis large. Declarative formats (Markdown, HTML, DOCX, …) don't touch it; only PDF/image conversion loads the ML models (downloaded on first use, like the CLI).
The regular docling.rs npm binaries are CPU-only. For NVIDIA GPU inference
in the PDF/image ML pipeline (issue #74 — same mechanism as the CLI and the
docling-rs-cuda Python wheel) there are two routes:
Install the docling.rs-cuda
package (Linux x64). Same JS API; the npm tarball is a few KB and its
postinstall downloads the CUDA addon + ONNX Runtime provider libraries from
this repo's matching npm-cuda-v<version> GitHub release, verifying each
file against the release's sha256 manifest (the binaries are far past npm's
practical size limits — the same fetch-at-install model onnxruntime-node
uses). Requires glibc ≥ 2.38 (Ubuntu 24.04+ era), CUDA 12 + cuDNN 9 at
runtime, and github.com access at install time — or set
DOCLING_RS_NPM_CUDA_URL to a mirror base URL / local directory with the
same assets (air-gapped installs). To keep existing require('docling.rs')
code unchanged, install it under an npm alias:
npm install docling.rs-cuda
# or: npm install docling.rs@npm:docling.rs-cudaThe package is published by the npm publish workflow's cuda input
(.github/workflows/npm-publish.yml), which also uploads the release assets
(crates/docling-node/cuda/ holds the shim sources).
Or build the addon from source with the cuda feature:
cd crates/docling-node
export RUSTFLAGS='-C link-arg=-Wl,-rpath,$ORIGIN' # Linux: find provider libs next to the addon
npm run build:cuda # = napi build ... --features cuda; fetches the CUDA ONNX Runtime (large)
cp ../../target/release/libonnxruntime_providers_{shared,cuda}.so .The CUDA execution provider is two separate shared libraries that ONNX
Runtime dlopens at session start; the $ORIGIN rpath makes it look next to
the .node addon, so ship them alongside it (without them a CUDA build
falls back to CPU with a warning). CUDA 12 + cuDNN 9 must be installed on the
system. A GPU build defaults to DOCLING_RS_EP=auto — GPU when usable, CPU
fallback; set DOCLING_RS_EP=cuda to fail loudly instead of falling back, or
DOCLING_RS_EP=cpu to force CPU. tensorrt / directml (Windows) /
coreml (macOS) features exist too, matching the Rust crates.
import { convertFile, convert, DocumentConverter } from 'docling.rs'
// Convert a file — format detected from the extension.
const { content } = convertFile('report.docx')
console.log(content) // Markdown
// Convert in-memory bytes (e.g. an upload) — pass the format explicitly.
const md = convert({ name: 'notes', data: Buffer.from('# Hi\n'), format: 'md' })
// docling-core JSON instead of Markdown.
const json = convertFile('report.docx', { to: 'json' })
// Reuse a converter across many documents.
const converter = new DocumentConverter({ strict: true })
const a = converter.convert({ name: 'a.md', data: Buffer.from('# A\n') })CommonJS works too: const { convertFile } = require('docling.rs').
Conversion is CPU-bound; the *Async variants run it on the libuv thread pool
so the event loop stays free. Prefer these for PDF/image and for servers.
import { convertFileAsync } from 'docling.rs'
const res = await convertFileAsync('paper.pdf', { to: 'json' })streamFileMarkdown yields Markdown chunks in document order as conversion
progresses. For PDF (whose pages convert in parallel) output starts flowing
before the whole document is done; concatenating the chunks reproduces the
buffered content byte-for-byte.
import { streamFileMarkdown } from 'docling.rs'
for await (const chunk of streamFileMarkdown('paper.pdf')) {
process.stdout.write(chunk)
}chunkFile / chunk / chunkDocument (each with an …Async variant) run
docling's chunkers over a converted document and return embedding-ready
records. The default is the structure-driven hierarchical chunker (one
chunk per document item — whole lists, triplet-serialized tables — with its
heading path); pass chunker: 'hybrid' to refine against a token budget
(split oversized chunks, merge undersized same-heading neighbours), matching
docling's HybridChunker. The hybrid token counts come from a HuggingFace
tokenizer.json: pass a path via tokenizer, or omit it to use
models/chunk/tokenizer.json (all-MiniLM-L6-v2's — fetched by
scripts/install/download_dependencies.sh alongside the ML models, resolved through
the same install-home logic).
import { chunkFileAsync, Pipeline, chunkDocumentAsync } from 'docling.rs'
const chunks = await chunkFileAsync('report.docx', {
chunker: 'hybrid',
tokenizer: 'tokenizer.json', // e.g. all-MiniLM-L6-v2's
maxTokens: 256,
})
for (const c of chunks) {
await embed(c.contextualized) // heading path + text, ready for the embedder
}
// Chunk something you already converted (no re-conversion), e.g. a PDF
// that went through the warm Pipeline:
const { content } = new Pipeline().convertFile('paper.pdf', { to: 'json' })
const pdfChunks = await chunkDocumentAsync(content)Each Chunk is { text, headings?, docItems, contextualized } — docItems
holds the source items' JSON-pointer refs ("#/texts/12"), contextualized
is docling's contextualize() rendering to feed the embedding model.
streamFileChunks / streamChunks / streamDocumentChunks are the streaming
counterparts: async generators that yield each chunk as the chunkers produce
it — the first chunk is ready for embedding while the rest of the document
is still being chunked, and no all-chunks array is materialized. Abandoning
the generator early (break) cancels the background chunking.
import { streamFileChunks } from 'docling.rs'
for await (const c of streamFileChunks('report.docx', {
chunker: 'hybrid',
tokenizer: 'tokenizer.json',
maxTokens: 256,
})) {
await embed(c.contextualized) // embedding overlaps the remaining chunking
}Declarative formats (Markdown, HTML, DOCX, XLSX, …) are pure Rust and need
nothing. The PDF/image path needs native assets that are not bundled in the
addon — pdfium plus the ONNX models (layout, OCR, TableFormer). Converting a
PDF/image/METS input throws until they're on disk. Fetch them with a
one-liner from your app's directory (where you'll npm install docling.rs):
curl -fsSL https://raw.githubusercontent.com/docling-project/docling.rs/master/scripts/install/download_dependencies.sh | shimport { convertFileAsync } from 'docling.rs'
const res = await convertFileAsync('paper.pdf', { to: 'markdown' }) // ✅ worksscripts/install/download_dependencies.sh fetches everything from this repo's
GitHub Releases straight into
./models and ./.pdfium — which this package (and the Rust CLI) look for by
default, relative to the process's current directory, so no env vars or setup
call are needed afterwards:
| Asset | Destination |
|---|---|
| pdfium | .pdfium/lib/libpdfium.so |
layout (layout_heron.onnx) |
models/layout_heron.onnx |
| OCR rec model + dictionary | models/ocr_rec.onnx, models/ppocr_keys_v1.txt |
| TableFormer | models/tableformer/{encoder,decoder,bbox}.onnx |
layout + TableFormer are PyTorch→ONNX exports (
docling-project/docling-layout-heron, Apache-2.0;docling-project/docling-models, CDLA-Permissive-2.0/Apache-2.0 — seedocs/MODELS_NOTICE.mdfor full attribution), not docling.rs's own weights — docling.rs hosts the converted.onnxas a GitHub Release purely so you don't need a local Python/torch toolchain. pdfium and the OCR model are re-hosted, unmodified, from their own public releases, on the same host for convenience.Run it from wherever your app lives — the script only writes to
./modelsand./.pdfiumunder the current directory, e.g. in a container build step:cd /path/to/your/app && curl -fsSL https://raw.githubusercontent.com/docling-project/docling.rs/master/scripts/install/download_dependencies.sh | shTo use your own export/host instead, point the env vars at it directly:
DOCLING_LAYOUT_ONNX,DOCLING_OCR_REC_ONNX,DOCLING_OCR_DICT,DOCLING_TABLEFORMER_{ENCODER,DECODER,BBOX},PDFIUM_DYNAMIC_LIB_PATH— an env var always wins over the./models/./.pdfiumdefault.
checkDependencies() // { home, pdfium, layout, ocr, tableformer, chunkTokenizer, ready, missing }The one-shot convertFile / convertFileAsync rebuild the pipeline — reloading
every ONNX model — on each call. To convert many PDFs/images, reuse a Pipeline
so the models load once:
import { Pipeline } from 'docling.rs'
const pipeline = new Pipeline({ strict: true })
for (const path of pdfPaths) {
const { content } = await pipeline.convertFileAsync(path, { to: 'json' }) // warm models, off the event loop
}
// Or stream a PDF's Markdown as pages finish converting:
for await (const chunk of pipeline.streamFileMarkdown('paper.pdf')) {
process.stdout.write(chunk)
}Pipeline handles pdf and image inputs (the ML pipeline). The sync
convertFile / convert block the event loop; the *Async variants run on the
libuv thread pool, and streamFileMarkdown yields Markdown chunks in document
order as pages finish. Conversions on one instance run one at a time (the
models are mutable sessions) — overlapping *Async calls queue in submission
order, so batch throughput comes from keeping the models warm, not from
parallel calls.
Pick how pictures render in Markdown with imageMode:
// Inline, self-contained: 
convertFile('slides.pptx', { imageMode: 'embedded' })
// Referenced: links + the image bytes to write yourself.
const res = convertFile('slides.pptx', { imageMode: 'referenced', artifactsDir: 'assets' })
for (const img of res.images) {
await fs.writeFile(img.path, img.data) // e.g. assets/image_000000.png
}JSON output always embeds extracted images as data URIs.
For scanned PDFs/images, ocrLang: 'en' | 'ch' picks the OCR recognition
model (en is the default — proper Latin word spacing; ch is the
multilingual docling-conformance model), and pages: 'A-B' converts only that
1-based PDF page window.
| Function | Returns | Notes |
|---|---|---|
convertFile(path, options?) |
ConvertResult |
Detects format from the extension. |
convert(input, options?) |
ConvertResult |
In-memory bytes ({ name, data, format? }). |
convertFileAsync(path, options?) |
Promise<ConvertResult> |
Off the event loop. |
convertAsync(input, options?) |
Promise<ConvertResult> |
Off the event loop. |
streamFileMarkdown(path, options?) |
AsyncGenerator<string> |
Markdown chunks in document order. |
chunkFile(path, options?) |
Chunk[] |
Convert + run docling's hierarchical/hybrid chunker. |
chunk(input, options?) |
Chunk[] |
Same, over in-memory bytes. |
chunkDocument(documentJson, options?) |
Chunk[] |
Chunk an already-converted docling JSON document. |
chunkFileAsync / chunkAsync / chunkDocumentAsync |
Promise<Chunk[]> |
Off the event loop. |
streamFileChunks / streamChunks / streamDocumentChunks |
AsyncGenerator<Chunk> |
Chunks yielded as produced; break cancels. |
supportedFormats() |
string[] |
Supported input format ids. |
formatFromName(name) |
string | null |
Detect a format id from a filename/extension. |
checkDependencies(options?) |
DependencyStatus |
Report which PDF/image deps are present. |
Pipeline is the reusable warm PDF/image converter: new Pipeline(converterOptions)
then convertFile / convert / convertFileAsync / convertAsync /
convertFileStreaming / streamFileMarkdown.
DocumentConverter is the reusable form: new DocumentConverter(converterOptions)
then convert / convertFile / convertFileAsync / convertAsync /
convertFileStreaming. Converter config (strict, fetchImages,
allowedFormats) is set once on the constructor; output options (to,
imageMode, artifactsDir) are per call.
to:"markdown"(default) or"json".imageMode:"placeholder"(default),"embedded", or"referenced".artifactsDir: directory name used inreferencedlinks (default"artifacts").strict: cleaner, more conformant Markdown instead of docling's byte-for-byte legacy output (Markdown only).fetchImages: for HTML/EPUB, resolve and embed external<img src>. Off by default; fetches http(s) URLs over the network — enable only for trusted input.allowedFormats: restrict the converter to these format ids/extensions.
interface ConvertResult {
content: string // Markdown or JSON, per `to`
format: string // detected input format id
status: string // "success" | "partial_success" | "failure"
inputName: string
images: { path: string; data: Buffer }[] // for the `referenced` image mode
}Full TypeScript types are generated into index.d.ts / native.d.ts.
The examples/ folder is a self-contained project that depends on
the published docling.rs package — npm install there, then run any of them:
cd examples
npm install
node node-basic.mjs # ESM: file, bytes, JSON, reuse
bun run bun-basic.ts # Bun + TypeScript: async + streaming
node pdf-pipeline.mjs # warm Pipeline for PDFs (run scripts/install/download_dependencies.sh first)examples/node-basic.mjs— Node.js (ESM): file, bytes, JSON, reuse.examples/bun-basic.ts— Bun + TypeScript, with async and streaming.examples/pdf-pipeline.mjs— warmPipelinefor PDFs.
The smoke test exercises the locally-built addon instead: npm run build once at
the package root, then node test/smoke.mjs (or bun test/smoke.mjs).
MIT, same as the rest of docling.rs.