docling 2.20.0

DocumentConverter and format backends for docling.rs (a Rust port of docling).
Documentation
# docling

The converter library of [docling.rs](https://github.com/docling-project/docling.rs)
— a Rust port of IBM's [docling](https://github.com/docling-project/docling).
`DocumentConverter` turns PDF, Office, HTML, scans, audio and 40-odd other
formats into a `DoclingDocument` and exports it as Markdown, docling JSON,
HTML, text, DocLang, LaTeX, Pandoc AST, WebVTT or RAG chunks. Every other
docling.rs surface — the `docling-rs` CLI, the HTTP server, the Python, Node,
wasm and C bindings — wraps this crate, so what is documented here applies
to all of them.

```bash
cargo add docling
```

PDF, image and audio conversion need the ONNX models
([`docs/MODELS.md`](../../docs/MODELS.md)); the declarative formats need
nothing. Overview, install options and benchmarks: the
[project README](../../README.md).

## Rust API

```rust
use docling::{DocumentConverter, SourceDocument};

let converter = DocumentConverter::new();
let result = converter
    .convert(SourceDocument::from_file("input.md").unwrap())
    .unwrap();

println!("{}", result.document.export_to_markdown()); // Markdown
println!("{}", result.document.export_to_json());     // docling DoclingDocument JSON
```

A failed conversion is a `ConversionError`; when the document is encrypted,
`err.encryption()` returns the typed reason (#636) — `NeedPassword` and
`WrongPassword` are worth prompting for a password (`needs_password()`),
`NotDecryptable` / `Malformed` are not:

```rust
match converter.convert(SourceDocument::from_file("locked.pdf").unwrap()) {
    Ok(result) => println!("{}", result.document.export_to_markdown()),
    Err(e) if e.encryption().is_some_and(|k| k.needs_password()) => ask_for_password(),
    Err(e) => eprintln!("{e}"),
}
```

`docling-rs serve` answers the same case with `422 {"error": …, "code":
"password_required" | "wrong_password" | "not_decryptable" |
"malformed_encryption"}`; the Python bindings raise `PasswordRequiredError` /
`WrongPasswordError` / `EncryptionError`, subclasses of `ConversionError`.

## One option set, every surface — `ConvertOptions`

Every conversion knob is one serializable struct,
[`docling::ConvertOptions`](src/options.rs) (#577), and every
surface fills it — the CLI from flags, serve from the query string / JSON
body / multipart parts, Python and Node from keyword arguments and option
objects, the C ABI and wasm from one JSON object — then runs the same
`validate()` and `apply()`. The defaults live in
`DocumentConverter::default()`; an unset option means that default
everywhere. The ones most conversions reach for:

| Wire name | CLI | Meaning |
|---|---|---|
| `pages` | `--pages A-B` | PDF page window (#80) |
| `no_ocr` · `text_layer_only` · `force_full_page_ocr` | `--no-ocr` · `--text-layer-only` · `--force-full-page-ocr` | never OCR, layout and tables kept · the text layer alone, no ML · OCR every page even with a text layer |
| `no_table_former` | `--no-table-former` | geometric tables instead of TableFormer |
| `ocr_lang` · `ocr_engine` | `--ocr-lang` · `--ocr-engine` | `en` / `ch` / BCP-47; `ppocr` or `tesseract` (#460) |
| `heading_hierarchy` | `--heading-hierarchy` | infer PDF heading levels (#302) |
| `images_scale` · `page_images` | `--images-scale` · `--page-images` | picture-crop resolution; keep page renders in the JSON (#520) |
| `document_timeout` | `--document-timeout` | per-document budget; the pages done so far become a partial success (#497) |
| `password` | `--password` | encrypted PDF and Office documents (#611, #625) |
| `image_sources` | `--image-sources` | `none` / `embedded` / `local` / `remote` — which `<img>` references resolve (#646) |
| `do_picture_classification` · `do_code_enrichment` · `do_formula_enrichment` · `do_picture_ocr` | `--enrich-picture-classes` · `--enrich-code` · `--enrich-formula` · `--picture-ocr` | the enrichment models (#423, #645) |
| `redact_pii` | `--redact-pii` | PII redaction before export (#621) |
| `pipeline` + `vlm_endpoint`, `vlm_model` | `--pipeline vlm --vlm-endpoint … --vlm-model …` | the remote vision-model pipeline (#77) |
| `strict` · `compact_tables` · `page_break_placeholder` | `--strict` · `--compact-tables` · `--page-break-placeholder` | Markdown dialect, unpadded tables, text between pages |

The full table — every option with its values, default and each surface's
spelling — is [`docs/OPTIONS.md`](../../docs/OPTIONS.md); an inventory test holds
every surface's documentation to it, so an option cannot ship on one surface
and silently miss another.

```rust
use docling::{ConvertOptions, DocumentConverter};

let options: ConvertOptions = serde_json::from_str(r#"{"pages": "1-3", "no_ocr": true}"#)?;
let converter = DocumentConverter::from_options(&options)?; // validated + applied
```

## Output formats

| `--to` / `to` | Library | What it is |
|---|---|---|
| `md` (default) | `export_to_markdown()` | docling's Markdown byte for byte; `--strict` for the cleaner dialect (below); `--images embedded\|referenced` for pictures |
| `json` | `export_to_json()` | docling-core's `DoclingDocument` wire format (schema 1.10.0): the `body` tree of `$ref`s into `texts` / `groups` / `tables` / `pictures`, labels, list groups, table grids, images as `data:` URIs. Loads straight into Python docling-core (`DoclingDocument.load_from_json`) and round-trips to the same Markdown |
| `html` | `export_to_html()` | docling-core's `HTMLDocSerializer` with its defaults: stylesheet, `<h{level+1}>`, inline groups, tables with spans and rich cells, `<figure>` pictures, MathML formulas (a literal port of `latex2mathml`). Byte-identical to docling-core on the whole declarative corpus (265/265) |
| `text` | `export_to_text()` | docling's `--to text` (#613): the Markdown with the decoration off — no `#`, no emphasis markers, links as their label, code without fences, no escaping; lists, checkboxes and table grids kept |
| `dclx` | `export_to_doclang()`, `docling::dclx::save_as_dclx` | DocLang — docling 2.110's XML of the tree (`<doclang version="0.7">`), pretty-printed like `minidom.toprettyxml`; `.dclx` is the OPC archive `save_as_doclang()` writes (`document.xml` + one PNG part per picture). Reads back in too: `.dclg` / `.dclx` are input formats (15/15 exact vs docling) |
| `latex` | `export_to_latex()` | docling 2.124's `LaTeXDocSerializer` with its defaults: `article` preamble, `\title` + `\maketitle`, sectioning, `itemize` / `enumerate`, `tabular` grids, `figure` placeholders, `verbatim`, `$$…$$`. 93 of 116 fixtures byte-exact against `docling --to latex` (98 once upstream's duplicated formatted list items — docling-core#740 — are normalized away) |
| `pandoc` | `export_to_pandoc_json()` | Pandoc's JSON AST (#515, `pandoc-api-version` 1.23.1.1), so every Pandoc writer — DOCX, ODT, EPUB, reST, Org, Typst, … — sits behind the parser; pictures embedded by default. `--pandoc-api-version` states the version a consumer needs |
| `vtt` | `export_to_vtt()` | WebVTT subtitles (#614, docling-core's `WebVTTDocSerializer`): one cue per timed item — an ASR transcript's segments, a WebVTT input's cues with voices and formatting. Untimed content is not represented |
| `chunks` | `docling::chunker` | both chunkers' records (below) |
| `images` | — | no conversion: a PDF's pages as PNG (`<stem>_page_NNNN.png`; `{"pages": […]}` on serve), honouring `--pages` and `--scale` 0.1–4.0 px/pt (#243) |

Every surface takes the same values — the CLI (batch mode writes
`<stem>.<ext>`, several formats at once with `--to md,json`), serve
(`to=`, the matching content type), Node / wasm / the C ABI (`to:`); the
Python bindings hand back docling-core's own `DoclingDocument`, so upstream's
`export_to_*` / `save_as_*` apply to it directly. Page breaks
(`--page-break-placeholder`) apply to Markdown and text.

```bash
docling-rs paper.pdf --to pandoc | pandoc -f json -t docx -o paper.docx
docling-rs talk.mp3 --to vtt > talk.vtt
docling-rs --to images --pages 2-3 --scale 1.5 paper.pdf   # paper_page_0002.png, paper_page_0003.png
```

How the JSON mirrors docling's item tree, the table-cell repair API and the
content layers the HTML / Markdown exports render: [`docs/MIGRATION.md` §
3](../../docs/MIGRATION.md#json-structure-and-content-layers); the Pandoc
mapping is in the same section's table.

## Chunking (docling's Hierarchical & Hybrid chunkers)

`docling_core.transforms.chunker` ported to Rust — the chunkers RAG
pipelines feed to embedding models. `HierarchicalChunker` yields one chunk
per document item (whole lists, triplet-serialized tables, picture
captions) with its heading path; `HybridChunker` refines them with a
tokenizer — splits oversized chunks (item boundaries, then docling's
`semchunk` inside text, tables line by line) and merges undersized
same-heading neighbours. Against docling's chunkers on the 83-document
corpus (`scripts/conformance/chunks_conformance.sh`): hierarchical 98.8 % /
hybrid 96.2 % identical chunk records.

```rust
use docling::chunker::{contextualize, HierarchicalChunker, HybridChunker, HuggingFaceTokenizer};

let chunks = HierarchicalChunker.chunk(&result.document);          // structure-driven
let tok = HuggingFaceTokenizer::from_file(".models/chunk/tokenizer.json", 256)?; // feature "chunking"
for chunk in HybridChunker::new(tok).chunk(&result.document) {
    let embed_me = contextualize(&chunk); // heading path + chunk text
}
```

```python
from docling_rs.chunking import HierarchicalChunker, HybridChunker
chunker = HybridChunker(max_tokens=256)
for chunk in chunker.chunk(doc):
    embed_me = chunker.contextualize(chunk)
```

`download_dependencies.sh` fetches MiniLM's tokenizer to
`.models/chunk/tokenizer.json`, which every surface picks up
(`DOCLING_CHUNK_TOKENIZER`, `DOCLING_CHUNK_MAX_TOKENS` override it;
per run `--chunker hierarchical|hybrid`, `--chunk-tokenizer`,
`--chunk-max-tokens`, `--no-chunk-merge-peers` and the matching serve
fields, #256). Also in the [Node bindings](../docling-node)
(`chunkFile` / `chunkDocument`), the [Python bindings](../docling-py)
(`docling_rs.chunking`) and the [RAG subsystem](../docling-rag).

## Image extraction

Backends that have the image populate `Node::Picture { image }`: the
PDF/image pipeline crops figure regions, DOCX / PPTX / MHTML pull embedded
blobs, Windows metafiles (EMF / WMF) in the office formats are rendered to
PNG in-process (#536). `ImageMode` — docling's `image_mode` — picks how
pictures render:

```rust
use docling::ImageMode;
// self-contained Markdown: ![Image](data:image/png;base64,…)
let (md, _) = result.document.export_to_markdown_with_images(ImageMode::Embedded, "artifacts");
// referenced: ![Image](artifacts/image_000000.png) + the bytes to write
let (md, files) = result.document.export_to_markdown_with_images(ImageMode::Referenced, "artifacts");
for (path, bytes) in files { std::fs::write(path, bytes).unwrap(); }
```

JSON always embeds the images as docling `ImageRef`s; the default Markdown
stays `<!-- image -->`, like docling (the pixels are real, the base64 is
not byte-identical to docling's — a different PNG encoder). Pictures that
are *references* — HTML / EPUB / MHTML / JATS / AsciiDoc / ODF `<img src>`,
Markdown `![…](…)`, an email's `cid:` — stay placeholders by default and
resolve under `image_sources` (#646): `embedded` (`data:` URIs and parts of
the same container; no filesystem, no network), `local` (plus files under
the source's directory), `remote` (plus `http(s)`, confined to
`image_hosts`); `max_image_bytes` / `max_images` / `max_image_total_mb` /
`min_image_bytes` bound a document — details in
[`docs/OPTIONS.md`](../../docs/OPTIONS.md) and [`docs/SECURITY.md`](../../docs/SECURITY.md).

## `strict` Markdown (Rust-only)

By default `export_to_markdown()` reproduces docling's output byte-for-byte,
quirks included (`***x*** .`, dropped code-fence languages, `\_` escaping).
`strict(true)` gives cleaner, more conformant Markdown
(`export_to_markdown_with(strict)` per call; Python docling has no such
switch):

```text
legacy:  Foo ***both*** .   |   ``` (lang dropped)   |   Name: \_\_\_
strict:  Foo ***both***.    |   ```rust (lang kept)  |   Name: ___
```

## Streaming Markdown

`convert_streaming` returns the Markdown as an iterator of chunks instead of
one string — for piping a long document to stdout, an HTTP response or a
socket as it is produced:

```rust
use std::io::Write;
use docling::{DocumentConverter, SourceDocument};

let source = SourceDocument::from_file("input.pdf").unwrap();
let mut out = std::io::stdout();
for chunk in DocumentConverter::new().convert_streaming(source).unwrap() {
    out.write_all(chunk.unwrap().as_bytes()).unwrap();
}
```

PDF is where it pays: the pipeline processes pages in parallel and streams
each page's Markdown in document order as soon as it is ready (a one-page
look-ahead lets paragraphs that wrap across a page break still merge). The
conversion runs on a background thread, the iterator applies backpressure,
dropping it cancels the work, and the concatenated chunks are byte-identical
to `export_to_markdown()`. Every image mode streams
(`convert_streaming_images(source, mode)`); `referenced` writes each page's
image files under `artifacts_dir` as that page is emitted and drops the
bytes. Streaming is Markdown-only — JSON needs every node up front. The CLI
streams by default (`--no-stream` buffers; `--to json` always does).

## PDF pipeline switches

All on every surface (library builder, CLI, serve, Python, Node) — values
and defaults in [`docs/OPTIONS.md`](../../docs/OPTIONS.md):

- `--pages A-B` (#80): converts that 1-based page window only; out-of-window
  pages are skipped before rasterization.
- `--document-timeout SECONDS` (#497): a per-document budget, checked
  between pages. Once spent, the pages already finished become the document
  and the result is docling's `PARTIAL_SUCCESS` with one error item; the CLI
  writes it and exits 0, serve answers `X-Docling-Status: partial_success`,
  a stream ends with a `Timeout` error item.
- `--text-layer-only`: no ML at all — the embedded text cells as flat
  paragraphs, the fastest path; a scanned PDF comes back empty rather than
  erroring. `--no-ocr` (docling's `--no-ocr` since 2.0, #611) keeps layout
  and TableFormer but never runs OCR; `--no-table-former` keeps OCR but
  reconstructs tables geometrically. `--force-full-page-ocr` is the
  opposite: OCR every page from its render even when a text layer exists
  (for layers that lie — broken encodings, garbage subset fonts).
- `--heading-hierarchy` (#302, docling's `HeadingHierarchyModel`): infers
  section-header levels after assembly from the PDF outline, legal/outline
  numbering and font style, in that precedence. Off by default, as in the
  docling groundtruth.
- `--no-text-panels`: keeps every detected picture a picture instead of
  demoting uncaptioned dense-text "pictures" into paragraphs (#173).
- `--skip-empty-cells`, `--compact-tables` (#271): sparse-spreadsheet
  relief — omit empty cells from XLSX table rows; unpadded `| a | b |`
  Markdown tables. `--page-break-placeholder TEXT`: docling's
  `page_break_placeholder` between rendered pages.

Scanned pages come upright before layout and OCR: a `/Rotate` flag is
applied, and a page rotated physically in the raster is probed under each
90° hypothesis on the text detector's lines and un-rotated when a rotated
reading is clearly more confident (#225, #571; `DOCLING_RS_OCR_ORIENTATION=off`
disables it). A one-line paragraph the layout model files as `page_footer`
directly under a heading with no other body — a CV's `Languages` line — is
read as that heading's text instead of vanishing with the furniture.

## OCR

OCR has docling's two stages (#429, #570): RapidOCR's PP-OCRv6 text detector
finds the lines on a scanned page or image — inside the layout regions its
boxes are the recognizer's crops, the lines no region covers come out as
orphan text — and the recognizer reads them, PP-OCRv6 when installed and the
PP-OCRv3 pairs otherwise. On FUNSD's 199 scanned forms word recall is 0.90
against the annotations (Python docling 2.133 with RapidOCR: 0.85). The
default language is `en`; the conformance corpus was generated with the
multilingual `ch` model, so byte-for-byte comparisons with Python docling
run with `--ocr-lang ch`. `--ocr-engine tesseract` (#460) runs the system
`tesseract` binary on the same crops instead, with any of its languages.
Languages, `ocr_mode`, `ocr_scale`, the Tesseract environment and the
detector knobs: [`docs/OPTIONS.md` § OCR](../../docs/OPTIONS.md#ocr-engines-and-languages).

## VLM pipeline (remote endpoint)

`--pipeline vlm` (#77) replaces the discriminative ML stack with a Vision
Language Model: each page is rendered in Rust and sent to any
OpenAI-compatible vision endpoint — LM Studio, Ollama, vLLM, a hosted
service — with docling's page-conversion prompt, and the answer is parsed by
the DocLang reader. No ONNX models load.

```bash
docling-rs --pipeline vlm \
  --vlm-endpoint http://localhost:11434/v1 \
  --vlm-model granite-docling \
  paper.pdf
```

The same on Node (`pipeline: 'vlm'`, `vlmEndpoint`, …), Python
(`pipeline="vlm", vlm_endpoint=…`) and serve (`pipeline=vlm` + the `vlm_*`
request options). Selecting the pipeline is always explicit: the
`DOCLING_RS_VLM_*` environment supplies values but never switches it on, and
the `vlm_*` options are inert under the standard pipeline. On serve a
request-supplied `vlm_endpoint` needs `--allow-url-fetch`; the safer
deployment pins `DOCLING_RS_VLM_ENDPOINT` / `DOCLING_RS_VLM_MODEL` on the
server and lets requests send only `pipeline=vlm`:

```bash
curl -F file=@paper.pdf 'localhost:5001/v1/convert?pipeline=vlm'
```

Answer grammars are detected per response (#322): DocTags (granite-docling),
DocLang XML, Chandra layout HTML, Unlimited-OCR grounding output and
DeepSeek-OCR Markdown; plain prose degrades to text. Measured against Python
docling's `VlmPipeline` on the same granite-docling endpoint: 87.7 % mean
similarity over 18 fixtures, 3 byte-exact
([docs/PDF_CONFORMANCE.md](../../docs/PDF_CONFORMANCE.md)). Flags, env
fallbacks, timeouts, retries and prompts: [`docs/OPTIONS.md` § VLM](../../docs/OPTIONS.md#vlm-pipeline).

## Headless-browser HTML pre-render (optional)

Almost everything in the HTML backend is pure Rust, but one thing a static
parse can't do is resolve the **CSS cascade** — whether a stylesheet- or
class-driven rule makes an element `display:none` (e.g. a collapsed nav menu).
The optional `--use-web-browser` flag renders the page in the system Chromium
first, drops every element the browser computes as hidden, then feeds the
cleaned HTML through the normal Rust backend (so all structure/table/KVP/
formatting logic still runs in Rust — the browser only decides visibility). It
applies to every HTML-routing input: direct HTML, plus MHTML and EPUB (which
assemble HTML from their archives). It's driven straight from Rust over the
DevTools protocol via
[`headless_chrome`](https://crates.io/crates/headless_chrome) — no Node,
Playwright, or other runtime.

It's gated behind the off-by-default `web-browser` Cargo feature, so the standard
build stays browser-dependency-free:

```bash
cargo run -p docling-cli --features web-browser -- --use-web-browser page.html
```

Chromium is located via `$DOCLING_RS_CHROME`/`$CHROME`, then
`$PLAYWRIGHT_BROWSERS_PATH/chromium`, else autodetected. The page's CSS must be
reachable for the cascade to resolve — inline `<style>` works offline, but a
saved page that links external stylesheets needs those fetchable (with a base
host). Without the feature, `--use-web-browser` is a clear error rather than a
silent no-op.


## Encrypted Office documents

A password-protected `.docx`, `.xlsx`, `.pptx`, `.doc`, `.xls` or `.ppt` converts
with its password (#625 — beyond docling, whose password option opens PDFs
only). It is the same option as the PDF password: `--password` (or docling's
`--pdf-password`), `--password-file PATH` to keep it out of the process list,
`password` in serve, Python and Node (`pdf_password` / `pdfPassword`, the
earlier name, still work). Files with only a *modify*
password convert without one (PowerPoint encrypts them with its built-in
default password, which is tried automatically, as is Excel's). Without the
password — or with a wrong one — the conversion fails saying so; the schemes
are listed in `docs/MIGRATION.md`.

```bash
docling-rs budget.xlsx --password-file ~/.budget-password
curl -F file=@deck.pptx -F password=1234 localhost:5001/v1/convert
```


## License

MIT, like the rest of docling.rs.