# Technical Documentation: run-llama/liteparse

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/run-llama/liteparse)

### Overview (`README.md`)

# LiteParse

[](https://github.com/run-llama/liteparse/actions/workflows/ci.yml)
|
[](https://crates.io/crates/liteparse)
|
[](https://www.npmjs.com/package/@llamaindex/liteparse)
|
[](https://www.npmjs.com/package/@llamaindex/liteparse-wasm)
|
[](https://pypi.org/project/liteparse/)
|
[](https://opensource.org/licenses/Apache-2.0)
|
[Docs](https://developers.llamaindex.ai/liteparse/)

English | [简体中文](README.zh-CN.md)

<img src="https://github.com/user-attachments/assets/07ba6a82-6bb1-4dea-b0ef-cad7df7d1622" alt="out" width="600">

> Looking for LiteParse V1? Follow this link to [the old code](https://github.com/run-llama/liteparse/tree/logan/liteparse-v1)

LiteParse is a standalone OSS PDF parsing tool focused exclusively on **fast and light** parsing. It provides high-quality spatial text parsing with bounding boxes, without proprietary LLM features or cloud dependencies. Everything runs locally on your machine.

**Hitting the limits of local parsing?**
For complex documents (dense tables, multi-column layouts, charts, handwritten text, or
scanned PDFs), you'll get significantly better results with [LlamaParse](https://developers.llamaindex.ai/python/cloud/llamaparse/?utm_source=github&utm_medium=liteparse),
our cloud-based document parser built for production document pipelines. LlamaParse handles the
hard stuff so your models see clean, structured data and markdown.

>  [Sign up for LlamaParse free](https://cloud.llamaindex.ai?utm_source=github&utm_medium=liteparse)

## Overview

- **Fast Text Parsing**: Spatial text parsing using PDFium, ~2-5ms per page
- **Flexible OCR System**:
  - **Built-in**: Tesseract (zero setup, bundled with the library)
  - **HTTP Servers**: Plug in any OCR server (EasyOCR, PaddleOCR, custom)
  - **Standard API**: Simple, well-defined OCR API specification
- **Complexity Detection**: Cheaply check whether a document needs OCR or heavier parsing — route, reject, or estimate cost before a full parse
- **Screenshot Generation**: Generate high-quality page screenshots for LLM agents
- **Multiple Output Formats**: Markdown, JSON, and Text
- **Markdown Output**: Structured Markdown with headings, tables, lists, images, and links — great for feeding LLMs and RAG pipelines
- **Bounding Boxes**: Precise text positioning information
- **Multi-language**: Use from Rust, Node.js/TypeScript, Python, or the browser (WASM)
- **Worker Pool Mode** (Python & Node.js): Parse in persistent worker processes for true parallelism (PDFium otherwise serializes concurrent parses) and hard per-parse timeouts — rogue documents are killed, identified by name, and never stall the pipeline
- **Multi-platform**: Linux, macOS (Intel/ARM), Windows

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Benchmarks

LiteParse is measured on multiple public Doc→Markdown benchmarks. All numbers below were produced on
this machine from one command (see [Reproducing](#reproducing-the-benchmarks)), with every tool at
its latest release as of 2026-09-09. LiteParse and the "model-free" competitors use **no ML model at
all** (no LLM, no layout model, no GPU). All tested methods have permissive licenses and can be
run locally with minimal dependencies.

| Benchmark | Metric | LiteParse | + Tesseract OCR | + PaddleOCR | Best other model-free tool |
|---|---|---|---:|---:|---:|
| [ParseBench](https://github.com/run-llama/parse-bench) (2,049 docs) | Overall (mean of 5 categories) | 0.364 | 0.380 | **0.389** | pdf-inspector 0.283 |
| [opendataloader-bench](https://github.com/opendataloader-project/opendataloader-bench) (200 docs) | Overall (NID + TEDS + MHS) | 0.886 | 0.896 | **0.901** | opendataloader 0.842 |
| [olmOCR-bench](https://github.com/allenai/olmocr/tree/main/olmocr/bench) (1,403 pages) | % tests passed | 39.6 | 41.1 | **42.2** | pdf-inspector 33.7 |

<details>
<summary><b>ParseBench</b></summary>

Rule-based scoring, no LLM judge. Each column is [ParseBench's](https://parsebench.ai) canonical per-category metric
(Tables = GriTS/TRM composite; the others are rule pass-rates). Overall is the mean of the five,
as on the ParseBench leaderboard.

| Pipeline | Overall | Tables | Charts | Content Faithfulness | Semantic Formatting | Visual Grounding |
|---|---:|---:|---:|---:|---:|---:|
| **LiteParse + PaddleOCR** | 0.389 | 0.430 | 0.013 | 0.787 | 0.402 | 0.314 |
| **LiteParse + Tesseract** | 0.380 | 0.428 | 0.012 | 0.751 | 0.399 | 0.307 |
| **LiteParse (no OCR)** | 0.364 | 0.424 | 0.013 | 0.700 | 0.385 | 0.297 |
| pdf-inspector 1.19 | 0.283 | 0.277 | 0.017 | 0.598 | 0.426 | 0.099 |
| opendataloader 2.5.7 | 0.277 | 0.349 | 0.006 | 0.663 | 0.258 | 0.110 |
| markitdown 0.1.7 | 0.185 | 0.158 | **0.020** | 0.652 | 0.001 | 0.110 |

Notes:
- **Visual Grounding** scores layout blocks with bounding boxes (`lit parse --extract-blocks`).
- **Charts** is near zero for every tool here — none reconstruct chart data.
</details>

<details>
<summary><b>opendataloader-bench</b></summary>

NID = reading-order similarity, TEDS = table structure, MHS = heading hierarchy. Overall is the
harness's own mean.

| Engine | Overall | NID | TEDS | MHS |
|---|---:|---:|---:|---:|
| **LiteParse + PaddleOCR** | **0.901** | **0.932** | **0.832** | **0.840** |
| **LiteParse + Tesseract** | 0.896 | 0.928 | 0.829 | 0.828 |
| **LiteParse (no OCR)** | 0.886 | 0.917 | 0.818 | 0.821 |
| nutrient *(commercial)* | 0.885 | 0.925 | 0.708 | 0.819 |
| opendataloader 2.5.7 | 0.842 | 0.912 | 0.483 | 0.757 |
| markitdown 0.1.7 | 0.589 | 0.844 | 0.273 | 0.000 |

Notes:
- nutrient has no runnable parser in this harness (commercial)
- The corpus is native-text PDFs, so OCR gains come from embedded figures and a handful of
  scanned pages.

</details>

<details>
<summary><b>olmOCR-bench</b></summary>

Score = average of per-category pass rates. The two math categories require LaTeX output and
are 0% for every tool here; they still count in the average.

| Engine | Overall | baseline | headers_footers | multi_column | table_tests | long_tiny_text | old_scans | arxiv_math | old_scans_math |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| **LiteParse + PaddleOCR** | **42.2** | **99.9** | 48.7 | **69.1** | 54.0 | **46.4** | **19.4** | 0.0 | 0.0 |
| **LiteParse + Tesseract** | 41.1 | **99.9** | 52.1 | 66.2 | 54.1 | 42.5 | 13.9 | 0.0 | 0.0 |
| **LiteParse (no OCR)** | 39.6 | **99.9** | 55.8 | 66.3 | 52.5 | 29.2 | 13.3 | 0.0 | 0.0 |
| pdf-inspector 1.19 | 33.7 | 82.9 | **62.1** | 49.7 | 43.6 | 17.6 | 13.3 | 0.0 | 0.0 |
| opendataloader 2.5.7 | 32.5 | 86.9 | 36.6 | 63.7 | 24.9 | 34.8 | 13.3 | 0.0 | 0.0 |
| markitdown 0.1.7 | 28.7 | 86.8 | 38.8 | 39.3 | 19.9 | 31.2 | 13.3 | 0.0 | 0.0 |

Notes:
- **headers_footers** expects letterhead and footer text to be *absent*. OCR recovers that text
  from logo and address images on single-page documents, where the repeated-header filter cannot
  fire, so the OCR rows score lower there by design. We chose not to drop text by page position.
  pdf-inspector 1.19 refuses scanned and image-based pages outright (192 of the 1,403), which is
  why it tops this category while scoring nothing on those pages elsewhere.
- **old_scans** is largely cursive handwriting; Tesseract cannot read it, PaddleOCR partially can.

</details>

<details>
<summary><b>How the runs are configured</b></summary>

- **Same scorer for every tool.** Each benchmark's own evaluator, unmodified. The ground truth in
  all three corpora is plain text, so every tool including LiteParse runs with hyperlink syntax
  off (`--no-links`); everything else is the default `lit parse --format markdown`.
- **OCR modes.** *No OCR* is `--no-ocr`. *Tesseract* is the built-in engine with no setup.
  *PaddleOCR* is the PP-OCRv5 mobile detector and English recognizer served over the
  [OCR HTTP API](OCR_API_SPEC.md) by [`ocr/rapidocr`](ocr/rapidocr), which runs the PaddleOCR
  models on ONNX Runtime — the same models as [`ocr/paddleocr`](ocr/paddleocr), just 20–40× faster
  per page on CPU-only machines.
- **Competitors** are the free converters we could run locally, each at its latest release on
  2026-09-09: markitdown 0.1.7, opendataloader-pdf 2.5.7 (its non-hybrid mode),
  pdf-inspector 1.19.0. Each benchmark's own runner for the tool is used as-is. Numbers from public
  leaderboards or LLM-assisted modes are not mixed in.
- **Machine:** Apple M2 Max, 12 cores, 32 GB. LiteParse rows: v2.14.4 (2026-09-09).

</details>

### Reproducing the benchmarks

```sh
./run_benchmarks.sh --liteparse-only                # all three benches, no OCR
./run_benchmarks.sh --liteparse-only --ocr=tesseract
( cd ocr/rapidocr && uv run server.py ) &           # then:
./run_benchmarks.sh --liteparse-only --ocr=paddle
./run_benchmarks.sh --competitors-only              # re-run the free competitors only
./run_benchmarks.sh                                 # everything
```

Results and a `SUMMARY.md` land in `bench_results/latest/`.

## Installation

Install via your preferred package manager. All versions (except WASM) ship with the same `lit` CLI.

| Language | Install | Library Docs |
|----------|---------|--------------|
| **Node.js / TypeScript** | `npm i @llamaindex/liteparse` | [Node.js README](packages/node/README.md) |
| **Python** | `pip install liteparse` | [Python README](packages/python/README.md) |
| **Rust** | `cargo install liteparse` (CLI) / `cargo add liteparse` (lib) | [Rust README (crates.io)](crates/liteparse/README.md) |
| **Browser (WASM)** | `npm i @llamaindex/liteparse-wasm` | [WASM README](packages/wasm/README.md) |

### Agent Skill

You can use `liteparse` as an agent skill, downloading it with the `skills` CLI tool:

```bash
npx skills add run-llama/llamaparse-agent-skills --skill liteparse
```

Or copy-pasting the [`SKILL.md`](https://github.com/run-llama/llamaparse-agent-skills/blob/main/skills/liteparse/SKILL.md) file to your own skills setup.

See the [Agent Skill guide](https://developers.llamaindex.ai/liteparse/guides/agent-skill/?utm_source=github&utm_medium=liteparse) for requirements and usage patterns.

## CLI Usage

The CLI is the same across all installations (`npm`, `pip`, `cargo install`).

### Parse Files

```bash
# Basic parsing
lit parse document.pdf

# Parse to Markdown — headings, tables, lists, images, links
lit parse document.pdf --format markdown -o output.md

# Parse with specific format
lit parse document.pdf --format json -o output.json

# Parse specific pages
lit parse document.pdf --target-pages "1-5,10,15-20"

# Parse without OCR
lit parse document.pdf --no-ocr

# Include page-scoped vector path data in JSON
lit parse document.pdf --format json --extract-vector-graphics

# Include rich per-item PDF text metadata
lit parse document.pdf --format json --extract-text-metadata

# Include page annotations in structured JSON
lit parse document.pdf --format json --extract-annotations

# Include AcroForm widget fields and values (repairs orphaned widgets in memory)
lit parse document.pdf --format json --extract-form-fields

# Parse a remote PDF
curl -sL https://example.com/report.pdf | lit parse -
```

### Markdown Output

LiteParse can render documents directly to Markdown. This means reconstructing headings,
tables, lists, images, and links from the spatial layout. This is ideal for
feeding documents to LLMs and RAG pipelines. This mode is purely heuristics and rule-based,
so complex documents may not render perfectly, but it will be fast.

```bash
# Render to Markdown
lit parse document.pdf --format markdown -o output.md

# Strip images instead of emitting placeholders
lit parse document.pdf --format markdown --image-mode off

# Extract embedded images to disk and reference them from the markdown
lit parse document.pdf --format markdown --image-mode embed --extract-images --image-output-dir ./images

# Extract image bytes and metadata without changing Markdown image handling
lit parse document.pdf --format json --extract-images

# Emit link text as plain text (no [text](url) syntax)
lit parse document.pdf --format markdown --no-links

# Include tagged-PDF logical structure in JSON
lit parse document.pdf --format json --extract-structure-tree

# Include the classified layout blocks (with bounding boxes) in JSON
lit parse document.pdf --format json --extract-blocks
```

Image handling is controlled by `--image-mode`:

| Mode | Behavior |
|------|----------|
| `placeholder` (default) | Emits `` references in reading order |
| `off` | Strips images entirely |
| `embed` | Emits the same image references as `placeholder` |

`--extract-images` is the only option that enables embedded-image extraction.
`--image-output-dir` requires it and writes the extracted bytes to disk. JSON output
contains each image's `name`, `path`, page bbox, intrinsic pixel dimensions, rotation,
format, and duplicate relationship; pixel bytes are never embedded in JSON. Identical
image resources reuse the same output file.

Library callers can opt in with `extract_images: true` (Rust), `extractImages: true`
(Node/WASM), or `extract_images=True` (Python). It defaults to false. Markdown image
mode controls presentation only; placeholder refs are still discovered without bytes.

> Markdown reconstruction quality varies with document complexity. For the
> hardest documents (dense tables, multi-column layouts, scans),
> [LlamaParse](https://developers.llamaindex.ai/python/cloud/llamaparse/?utm_source=github&utm_medium=liteparse)
> remains the most accurate option.

### Vector Graphics

Vector path output is opt-in because path-heavy PDFs can produce large payloads.
Enable it with `--extract-vector-graphics`, Rust/Python
`extract_vector_graphics = true`, or JavaScript/WASM
`extractVectorGraphics: true`. Each page then includes `vector_graphics`
(`vectorGraphics` in JavaScript) with:

- `shapes`: path bounding box, stroke/fill paint state and ARGB colors, and
  whether the path contains a Bezier curve.
- `lines`: compatible horizontal/vertical segments merged using stroke width
  and paint colors, with top-left 72-DPI viewport coordinates.

The representation follows LlamaParse PDFium path extraction; LiteParse calls
the shape rectangle `bbox` rather than PDFium's `coords`, and uses `width` /
`height` rather than `w` / `h`. The field is absent (or `None`/`undefined`) by
default. Diagonal and curved segments are represented by their parent shape but
are not emitted as lines.

### Tagged PDF structure tree

Enable `--extract-structure-tree` (Rust/Python `extract_structure_tree`,
JavaScript/WASM `extractStructureTree`) to add a page-scoped `structure_tree`.
It preserves every root and recursively exposes element type, ID, actual/alternate
text, title, typed scalar attributes, marked-content IDs, children, and referenced
link annotations. The field is absent by default; enabled untagged pages contain
`roots: []`.

### Layout blocks

The Markdown renderer works by classifying each page into blocks — headings,
paragraphs, list items, tables, code, rules, figures — and then rendering them.
Enable `--extract-blocks` (Rust/Python `extract_blocks`, JavaScript/WASM
`extractBlocks`) to get that decomposition as data instead of only as rendered
text, with the coordinates the classifier used.

Each page gains a `blocks` array in reading order — the same order, and the same
blocks, the Markdown output is built from. Every block carries:

- `kind`: one of `heading`, `paragraph`, `list_item`, `code`, `table`,
  `grid_fallback`, `rule`, `figure`.
- `bbox`: the region it occupies, in the same top-left 72-DPI viewport space as
  `text_items`. This is the union of every source line that fed the block, so a
  wrapped heading or a multi-line paragraph reports its whole band.
- Kind-specific fields, omitted when they don't apply: `text` and `level` for
  headings; `ordered` / `marker` for list items; `lines` and `lang` for code;
  `header` and `rows` for tables; `id` / `format` for figures.

Table cells are objects, not bare strings — each has `text` and its own `bbox`,
so a cell can be mapped back to the region of the page it was read from. For
ruled tables that box is the drawn grid cell; for borderless tables it is the
extent of the spans the cell was built from. Cells that exist only to square off
a ragged grid carry no `bbox`, since they have no ink behind them.

```json
{
  "kind": "table",
  "bbox": { "x": 72.0, "y": 310.5, "width": 468.0, "height": 96.0 },
  "header": [
    { "text": "Territory Code", "bbox": { "x": 72.0, "y": 310.5, "width": 120.0, "height": 24.0 } },
    { "text": "Factor",         "bbox": { "x": 192.0, "y": 310.5, "width": 96.0, "height": 24.0 } }
  ],
  "rows": [
    [
      { "text": "001", "bbox": { "x": 72.0, "y": 334.5, "width": 120.0, "height": 24.0 } },
      { "text": "1.25", "bbox": { "x": 192.0, "y": 334.5, "width": 96.0, "height": 24.0 } }
    ]
  ]
}
```

### Document metadata, content bounds, and XFA packets

Parse results (Rust/Node/Python APIs) carry the document's `/Info` `creator`
and `producer` entries when present; these are API-only and never appear in
CLI JSON output. Enable `extract_document_metadata` (JavaScript/WASM
`extractDocumentMetadata`) to add `doc_meta`/`docMeta`, a provenance object
with the `/Info` creation/modification dates, PDF version and encryption
permissions, signature state, incremental-save markers, trailer ID comparison,
the document catalog's XMP packet (capped at 64 KiB, with `xmp_truncated`
when it was cut), and source file size. It is off by default because it
streams the whole source file once; it is absent for inputs converted from a
non-PDF format, where the facts would describe the intermediate PDF rather
than your file. `xmp` needs a structural parse of the document, so it is
skipped (left absent) for sources over 16 MiB and in WASM builds — the other
fields are unaffected.
Enable `--extract-content-bounds` (Rust/Python
`extract_content_bounds`, JavaScript/WASM `extractContentBounds`) to add a
per-page `content_bounds`: the union bbox of the page's top-level content
objects in viewport coords (absent for empty pages). Enable
`--extract-xfa-packets` (Rust/Python `extract_xfa_packets`, JavaScript/WASM
`extractXfaPackets`) to add `xfa_packets` with each raw XFA packet's index,
name, byte length, and XML content; non-XFA documents yield an empty list.
All of these are off by default, so default JSON output is unchanged.

### Screenshot raster signals

Screenshots draw AcroForm field appearances (filled values, checkbox states)
on top of the page raster, so form data is visible in the render and to OCR.
Each screenshot result reports `is_solid_fill` (blank page after render), and
with `detect_screenshot_rects` (Node `detectScreenshotRects`) also `rects`:
solid same-color rectangles and lines found in the raster in viewport coords,
which covers scanned/flattened pages that carry no vector paths.

### Check Complexity

Before committing to a full parse, check whether a document actually needs OCR or
heavier processing. This is a cheap, text-layer-only pass — useful for routing
documents to different pipelines, rejecting ones you can't handle, or estimating cost.

```bash
# Print the complexity verdict and per-page JSON
lit is-complex document.pdf

# Use as a shell predicate — only parse with --no-ocr when the document is simple
lit is-complex document.pdf --quiet && lit parse document.pdf --no-ocr

# List the pages that need OCR
lit is-complex document.pdf --compact | jq '[.[] | select(.needs_ocr) | .page_number]'
```

It always prints per-page JSON to **stdout**, a human-readable verdict to **stderr**, and
exits **non-zero when any page needs OCR**. Each page carries a `needs_ocr` verdict and a
list of `reasons` (`scanned`, `no-text`, `sparse-text`, `embedded-images`, `garbled`,
`vector-text`, `annotation-text`).

### Batch Parsing

Parse an entire directory of documents:

```bash
lit batch-parse ./input-directory ./output-directory
```

### Generate Screenshots

Screenshots are essential for LLM agents to extract visual information that text alone cannot capture.

```bash
# Screenshot all pages
lit screenshot document.pdf -o ./screenshots

# Screenshot specific pages
lit screenshot document.pdf --target-pages "1,3,5" -o ./screenshots

# Custom DPI
lit screenshot document.pdf --dpi 300 -o ./screenshots
```

### CLI Reference

#### Parse Command

```
/* Detailed source-code truncated for AI context efficiency. */
```

#### Batch Parse Command

```
lit batch-parse [OPTIONS] <input-dir> <output-dir>

Options:
      --format <format>        Output format: json|text|markdown [default: text]
      --no-ocr                 Disable OCR
      --ocr-language <lang>    OCR language [default: eng]
      --ocr-server-url <url>   HTTP OCR server URL
      --tessdata-path <path>   Path to tessdata directory
      --max-pages <n>          Max pages per file [default: 1000]
      --dpi <dpi>              Rendering DPI [default: 150]
      --recursive              Recursively search input directory
      --extension <ext>        Only process files with this extension (e.g., ".pdf")
      --password <password>    Password for encrypted documents
      --num-workers <n>        Concurrent OCR workers
  -q, --quiet                  Suppress progress output
  -h, --help                   Print help
```

#### Screenshot Command

```
lit screenshot [OPTIONS] <file>

Options:
  -o, --output-dir <dir>       Output directory [default: ./screenshots]
      --target-pages <pages>   Pages to screenshot (e.g., "1,3,5" or "1-5")
      --dpi <dpi>              Rendering DPI [default: 150]
      --password <password>    Password for encrypted documents
  -q, --quiet                  Suppress progress output
  -h, --help                   Print help
```

#### Is-Complex Command

```
lit is-complex [OPTIONS] <file>

Options:
      --compact                Emit dense, whitespace-free JSON instead of pretty-printed
      --max-pages <n>          Max pages to check [default: 1000]
      --target-pages <pages>   Pages to check (e.g., "1-5,10,15-20")
      --password <password>    Password for encrypted documents
  -q, --quiet                  Suppress the stderr verdict
  -h, --help                   Print help
```

Prints per-page JSON to stdout and a `COMPLEX`/`SIMPLE` verdict to stderr; exits non-zero
when any page needs OCR, so it composes as a shell predicate.

## OCR Setup

### Default: Tesseract

Tesseract is bundled and works out of the box:

```bash
lit parse document.pdf                    # OCR enabled by default
lit parse document.pdf --ocr-language fra # Specify language
lit parse document.pdf --no-ocr           # Disable OCR
```

For offline or air-gapped environments, set `TESSDATA_PREFIX` to a directory containing pre-downloaded `.traineddata` files:

```bash
export TESSDATA_PREFIX=/path/to/tessdata
lit parse document.pdf --ocr-language eng
```

Or pass the path directly:

```bash
lit parse document.pdf --tessdata-path /path/to/tessdata
```

### Optional: HTTP OCR Servers

For higher accuracy or better performance, you can use an HTTP OCR server. We provide ready-to-use example wrappers for popular OCR engines:

- [EasyOCR](ocr/easyocr/README.md)
- [PaddleOCR](ocr/paddleocr/README.md)

You can integrate any OCR service by implementing the simple LiteParse OCR API specification (see [`OCR_API_SPEC.md`](OCR_API_SPEC.md)).

The API requires:
- POST `/ocr` endpoint
- Accepts `file` and `language` parameters
- Returns JSON: `{ results: [{ text, bbox: [x1,y1,x2,y2], confidence }] }`

## Multi-Format Input Support

LiteParse supports **automatic conversion** of various document formats to PDF before parsing.

### Supported Input Formats

#### Office Documents (via LibreOffice)
- **Word**: `.doc`, `.docx`, `.docm`, `.odt`, `.rtf`, `.pages`
- **PowerPoint**: `.ppt`, `.pptx`, `.pptm`, `.odp`, `.key`
- **Spreadsheets**: `.xls`, `.xlsx`, `.xlsm`, `.ods`, `.csv`, `.tsv`, `.numbers`

Install LibreOffice for automatic conversion:

```bash
# macOS
brew install --cask libreoffice

# Ubuntu/Debian
apt-get install libreoffice

# Windows
choco install libreoffice-fresh
```

> _On Windows, you may need to add LibreOffice's program directory (usually `C:\Program Files\LibreOffice\program`) to your PATH._

#### Images (native support)
- **Formats**: `.jpg`, `.jpeg`, `.png`, `.gif`, `.bmp`, `.tiff`, `.webp`, `.svg`

> ![NOTE]
>
> As of [v2.8.0](https://github.com/run-llama/liteparse/releases/tag/crates-v2.8.0), `imagemagick` is no longer required to convert images to PDF. Conversion is natively
> handled by the rust code.

## Environment Variables

| Variable | Description |
|----------|-------------|
| `TESSDATA_PREFIX` | Path to a directory containing Tesseract `.traineddata` files. Used for offline/air-gapped environments. |

## Development

The project is a Rust workspace with the core library and language-specific binding crates.

```
crates/
├── liteparse/          # Core library + CLI binary
├── liteparse-napi/     # Node.js bindings (napi-rs)
├── liteparse-python/   # Python bindings (PyO3)
├── liteparse-wasm/     # WASM bindings (wasm-bindgen)
├── pdfium/             # PDFium Rust wrapper
└── pdfium-sys/         # PDFium FFI bindings
packages/
├── node/               # npm package (TS wrapper + native binary)
├── python/             # PyPI package (Python wrapper + native binary)
└── wasm/               # WASM npm package
```

### Building

```bash
# Build the CLI
cargo build --release -p liteparse

# Build Node.js bindings
cd packages/node && npm run build

# Build Python bindings
cd packages/python && maturin develop --release

# Build WASM
cd packages/wasm && npm run build
```

We provide a fairly rich `AGENTS.md`/`CLAUDE.md` that we recommend using to help with development + coding agents.

## License

Apache 2.0

## Credits

Built on top of:

- [PDFium](https://pdfium.googlesource.com/pdfium/) - PDF rendering and text extraction
- [Tesseract](https://github.com/tesseract-ocr/tesseract) - OCR engine (via tesseract-rs)
- [EasyOCR](https://github.com/JaidedAI/EasyOCR) - HTTP OCR server (optional)
- [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR) - HTTP OCR server (optional)
- [napi-rs](https://napi.rs/) - Node.js native bindings
- [PyO3](https://pyo3.rs/) - Python native bindings
- [wasm-bindgen](https://github.com/wasm-bindgen/wasm-bindgen) - WebAssembly bindings

---

### Overview (`dataset_eval_utils/README.md`)

# LiteParse Eval Utils

Utilities for generating and evaluating datasets for PDF parsing performance. Compares text extraction quality across multiple PDF parsers using LLM-based QA evaluation.

## Setup

Requires Python 3.12+.

```bash
# Install the package (from the dataset_eval_utils directory)
pip install -e .
```

You'll need an `ANTHROPIC_API_KEY` environment variable set for the LLM-based evaluation and dataset processing tools.

## Dataset

An existing dataset that was generated and evaluated using this framework can be found on [huggingface]().

You can download the dataset using the Hugging Face CLI:

```bash
hf download run-llama/liteparse-eval-dataset --repo-type dataset --local-dir ./liteparse-eval-dataset
```

## CLI Tools

### `lp-process` — Generate Ground Truth Datasets

Processes PDF and image files using Claude's vision capabilities to generate structured QA ground truth data.

```bash
lp-process /path/to/documents --output-dir ./ground_truth
```

Options:
- `--output-dir` — Directory to save output JSON files (default: `./output`)
- `--model` — Claude model to use (default: `claude-sonnet-4-5-20250929`)
- `--api-key` — Anthropic API key (or set `ANTHROPIC_API_KEY` env var)

Each output JSON file contains document metadata and QA pairs extracted from the document pages.

### `lp-evaluate` — Run QA Evaluation

Evaluates parser text extraction quality by having an LLM answer questions from extracted text and judging correctness against ground truth answers.

```bash
lp-evaluate \
  --data-dir ./documents \
  --ground-truth-dir ./ground_truth \
  --parse-provider liteparse \
  --output ./results/run1
```

Options:
- `--data-dir` — Directory containing source PDF documents (required)
- `--ground-truth-dir` — Directory containing ground truth JSON files (required)
- `--output` — Path to save results (JSON + HTML report)
- `--parse-provider` — Parser to evaluate: `liteparse`, `pymupdf`, `pypdf`, `markitdown` (default: `liteparse`)
- `--llm-provider` — LLM for answering questions: `anthropic` (default: `anthropic`)

Outputs:
- `<output>.json` — Aggregate results with pass rates
- `<output>_detailed.json` — Per-document results with extracted text and individual QA results
- `<output>_report.html` — Interactive HTML report with PDF previews and QA breakdowns

### `lp-benchmark` — Performance Benchmarking

Measures parse latency and memory usage across providers.

```bash
lp-benchmark ./documents --providers pymupdf liteparse --warmup-runs 0
```

Takes a **directory** of PDFs (searched recursively), not a single file.

Options:
- `--providers` — Providers to benchmark (default: all local providers)
- `--warmup-runs` — Full passes over the corpus before timing starts (default: 5).
  Each warmup run parses **every** document, so on a large corpus this dominates
  wall time; use `0` for a quick measurement.
- `--output` — Path to save JSON results

Reports per-document latency plus TOTAL / AVG-per-doc / MS-per-page rows.

## Parser Providers

| Provider | Library | Notes |
|----------|---------|-------|
| `liteparse` | [liteparse](https://github.com/run-llama/liteparse) | Spatial text extraction with OCR support |
| `pymupdf` | [PyMuPDF](https://pymupdf.readthedocs.io/) | Fast, mature PDF library |
| `pypdf` | [pypdf](https://pypdf.readthedocs.io/) | Pure-Python PDF library |
| `markitdown` | [MarkItDown](https://github.com/microsoft/markitdown) | Microsoft's document-to-markdown converter |

## Evaluation Pipeline

1. **Extract text** from PDF using the selected parser provider
2. **Answer questions** — LLM reads the extracted text and answers ground truth questions
3. **Judge answers** — A separate LLM judge evaluates whether predicted answers are semantically equivalent to expected answers
4. **Aggregate** — Pass rates are computed per-document and overall

---

### Overview (`ocr/README.md`)

# ocr/

Example OCR server implementations that conform to the LiteParse OCR API specification.

These servers allow you to use alternative OCR engines instead of the built-in Tesseract.js.

## Why Use an External OCR Server?

| Feature | Tesseract.js (built-in) | EasyOCR | PaddleOCR | Surya |
|---------|-------------------------|---------|-----------|-------|
| Setup | Zero (included) | uv | uv | uv |
| Speed | Moderate | Moderate | Fast (2-3x) | Moderate (GPU recommended) |
| Accuracy (Latin) | Good | Good | Good | Excellent |
| Accuracy (CJK) | Fair | Good | Excellent | Excellent |
| Languages | 100+ | 80+ | 80+ | 91 |
| Memory | In-process | Separate | Separate | Separate |

**Recommendations:**
- **Quick start**: Use built-in Tesseract (no setup)
- **Asian languages**: Use PaddleOCR (best CJK support)
- **Multilingual / broad scripts**: Use Surya (strong multilingual accuracy)
- **General use**: EasyOCR (good balance)

## Available Servers

### [easyocr/](./easyocr/)
Flask server wrapping EasyOCR library.
- Port: **8828**
- Good general-purpose OCR
- 80+ languages

### [paddleocr/](./paddleocr/)
Flask server wrapping PaddleOCR library.
- Port: **8829**
- Excellent for Chinese, Japanese, Korean
- 2-3x faster than EasyOCR

### [suryaocr/](./suryaocr/)
FastAPI server wrapping Surya OCR 2.
- Port: **8830**
- Multilingual foundation model (90+ languages)
- Block-level output; GPU recommended

## Quick Start

```bash
# Start EasyOCR server
cd ocr/easyocr
uv run server.py

# OR start PaddleOCR server
cd ocr/paddleocr
uv run server.py

# OR start Surya OCR server
cd ocr/suryaocr
uv run server.py
```

Then use with LiteParse:

```bash
# CLI
lit parse document.pdf --ocr-server-url http://localhost:8828/ocr

# Code
const parser = new LiteParse({
  ocrServerUrl: 'http://localhost:8828/ocr',
  ocrLanguage: 'en',
});
```

## API Specification

All servers implement the same API (defined in `OCR_API_SPEC.md`):

**Endpoint:** `POST /ocr`

**Request:**
- Content-Type: `multipart/form-data`
- Fields:
  - `file` - Image file
  - `language` - Language code (e.g., 'en', 'zh', 'ja')

**Response:**
```json
{
  "results": [
    {
      "text": "recognized text",
      "bbox": [x1, y1, x2, y2],
      "confidence": 0.95
    }
  ]
}
```

## Creating a Custom OCR Server

To implement your own OCR server:

1. Create a Flask/FastAPI/Express server
2. Accept `POST /ocr` with multipart form data
3. Return JSON with `results` array containing:
   - `text` - Recognized text string
   - `bbox` - Bounding box as `[x1, y1, x2, y2]`
   - `confidence` - Confidence score (0-1)

4. (Optional) Implement `GET /health` endpoint

See the existing servers as reference implementations.

## Language Codes

Most servers accept ISO 639-1 codes (e.g., 'en', 'zh', 'ja') and map them internally:

| ISO Code | Language | Notes |
|----------|----------|-------|
| en | English | |
| zh | Chinese (Simplified) | |
| zh-tw | Chinese (Traditional) | |
| ja | Japanese | |
| ko | Korean | |
| fr | French | |
| de | German | |
| es | Spanish | |
| ar | Arabic | |
| hi | Hindi | |

---

### Overview (`crates/liteparse/README.md`)

# LiteParse

[](https://crates.io/crates/liteparse)
[](https://opensource.org/licenses/Apache-2.0)

Rust library and CLI for fast, lightweight PDF and document parsing with spatial text extraction. Runs entirely locally with zero cloud dependencies.

> LiteParse is also available for [Node.js/TypeScript](https://www.npmjs.com/package/@llamaindex/liteparse), [Python](https://pypi.org/project/liteparse/), and the [browser (WASM)](https://www.npmjs.com/package/@llamaindex/liteparse-wasm). See the [project README](https://github.com/run-llama/liteparse) for all options.

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
liteparse = "2"
```

Or install the CLI:

```bash
cargo install liteparse
```

## Quick Start

```rust
use liteparse::{LiteParse, LiteParseConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let parser = LiteParse::new(LiteParseConfig::default());
    let result = parser.parse("document.pdf").await?;

    println!("{}", result.text);

    for page in &result.pages {
        println!("Page {}: {} text items", page.page_num, page.text_items.len());
    }

    Ok(())
}
```

## Configuration

```rust
use liteparse::{LiteParse, LiteParseConfig, OutputFormat};

let config = LiteParseConfig {
    ocr_enabled: true,                    // Enable OCR (default: true)
    ocr_language: "eng".to_string(),      // Tesseract language code
    ocr_server_url: None,                 // HTTP OCR server URL (optional)
    tessdata_path: None,                  // Path to tessdata directory (optional)
    max_pages: 1000,                      // Max pages to parse
    target_pages: Some("1-5,10".into()),  // Specific pages (optional)
    dpi: 150.0,                           // Rendering DPI
    output_format: OutputFormat::Json,    // Json | Text | Markdown
    extract_annotations: false,           // Include page annotations in output
    extract_structure_tree: false,        // Include tagged-PDF logical structure
    preserve_very_small_text: false,      // Keep tiny text
    extract_text_metadata: false,         // Opt in to rich PDF text metadata
    password: None,                       // Password for protected documents
    quiet: false,                         // Suppress progress output
    ..Default::default()
};

let parser = LiteParse::new(config);
```

Set `extract_annotations: true` to populate `ParsedPage::annotations` with
annotation subtype, contents, author/title, PDF date strings, viewport-space
rectangle and quadpoint rectangles, and external link URI. It is independent
of `extract_links`, which controls Markdown link rendering. The field is
`None` when extraction is disabled.

Set `extract_structure_tree: true` to populate `ParsedPage::structure_tree` with
the complete tagged-PDF hierarchy: all roots, element type/ID, actual and alternate
text, title, typed scalar attributes, MCIDs, recursive children, and referenced link
annotations. Disabled pages use `None`; enabled untagged pages have no roots.

## Markdown Output

LiteParse can render documents directly to Markdown, including headings, tables, lists,
images, and links reconstructed from the spatial layout. Set
`output_format: OutputFormat::Markdown`; the rendered Markdown is returned on
`result.text`. Two related knobs control Markdown rendering:

- `image_mode` (`ImageMode::Placeholder` default | `Off` | `Embed`) — how raster
  images are surfaced in the output.
- `extract_images` (default `false`) — return embedded image bytes and metadata
  without changing Markdown image handling. This is the only option that enables
  extraction.
- `image_output_dir` — write extracted image files and return their names/paths;
  requires `extract_images: true`. Duplicate image resources reuse the same file.
- `extract_links` (default `true`) — render hyperlink annotations as
  `[text](url)`; set `false` for plain anchor text.

```rust
use liteparse::config::{ImageMode, LiteParseConfig, OutputFormat};

let config = LiteParseConfig {
    output_format: OutputFormat::Markdown,
    image_mode: ImageMode::Placeholder,
    extract_images: true,
    image_output_dir: Some("./images".into()),
    extract_links: true,
    ..Default::default()
};
let result = LiteParse::new(config).parse("document.pdf").await?;
println!("{}", result.text); // rendered Markdown
```

> Reconstruction quality varies with document complexity.

## Parsing from Bytes

```rust
use liteparse::types::PdfInput;

let pdf_bytes: Vec<u8> = std::fs::read("document.pdf")?;
let result = parser.parse_input(PdfInput::Bytes(pdf_bytes)).await?;
println!("{}", result.text);
```

## Document Complexity

Before committing to a full parse, check whether a document needs OCR or heavier
processing. `is_complex` is a cheap, text-layer-only pass that returns a
`PageComplexityStats` per page with a `needs_ocr` verdict and the signals behind it —
useful for routing documents to different pipelines, rejecting ones you can't handle, or
estimating cost.

```rust
use liteparse::types::PdfInput;

let parser = LiteParse::new(LiteParseConfig::default());
let pages = parser.is_complex(PdfInput::Path("document.pdf".into())).await?;

if pages.iter().any(|p| p.needs_ocr) {
    // Route to the OCR-enabled pipeline, inspect `p.reasons`, etc.
    for page in pages.iter().filter(|p| p.needs_ocr) {
        println!("Page {} needs OCR: {:?}", page.page_number, page.reasons);
    }
}
```

`reasons` is a `Vec<ComplexityReason>` (`Scanned`, `NoText`, `SparseText`,
`EmbeddedImages`, `Garbled`, `VectorText`, `AnnotationText`); new variants may be added over time, so match
leniently.

## Custom OCR Engine

Implement the `OcrEngine` trait to plug in your own OCR backend:

```rust
use liteparse::ocr::OcrEngine;
use std::sync::Arc;

let parser = LiteParse::new(LiteParseConfig::default())
    .with_ocr_engine(Arc::new(my_engine));
```

For a native ONNX backend, enable `oar-ocr` and supply a detection model,
recognition model, and matching character dictionary:

```rust,no_run
use liteparse::ocr::oar::OarOcrEngine;
use liteparse::{LiteParse, LiteParseConfig};
use std::path::Path;
use std::sync::Arc;

let models = Path::new("models");
let engine = OarOcrEngine::from_models(
    models.join("pp-ocrv6_small_det.onnx"),
    models.join("pp-ocrv6_small_rec.onnx"),
    models.join("ppocrv6_dict.txt"),
)?;

let parser = LiteParse::new(LiteParseConfig::default())
    .with_ocr_engine(Arc::new(engine));
# Ok::<(), Box<dyn std::error::Error>>(())
```

For opt-in model downloads, enable `oar-ocr-auto-download` and use a preset. On
first use, `oar-ocr` downloads the detection model, recognition model, and
matching dictionary from ModelScope, verifies their SHA-256 digests, and caches
them under `$OAR_HOME` (default `~/.oar`):

```rust,no_run
use liteparse::ocr::oar::OarOcrEngine;

// Smallest / fastest PP-OCRv6 configuration.
let engine = OarOcrEngine::ppocr_v6_tiny()?;
# Ok::<(), Box<dyn std::error::Error>>(())
```

Presets cover the current and previous PP-OCR generations, from fastest to most
accurate: `ppocr_v6_tiny`, `ppocr_v6_small`, and `ppocr_v6_medium`. Each wires the correct
detector/recognizer/dictionary trio. For PP-OCRv4, a language-specific
recognizer, or a custom mix, use `from_models` with a matching dictionary.

To mix a detector, recognizer, and dictionary yourself, pass registered bare
file names to `from_models`. Pair the recognizer with its matching dictionary —
the tiny recognizer needs `ppocrv6_tiny_dict.txt`, while the larger models use
`ppocrv6_dict.txt`; a mismatched dictionary silently produces garbled text:

```rust,no_run
use liteparse::ocr::oar::OarOcrEngine;

let engine = OarOcrEngine::from_models(
    "pp-ocrv6_small_det.onnx",
    "pp-ocrv6_small_rec.onnx",
    "ppocrv6_dict.txt",
)?;
# Ok::<(), Box<dyn std::error::Error>>(())
```

All three artifacts also accept in-memory bytes, so the whole pipeline can be
embedded with `include_bytes!` rather than shipped as files. Use `OAROCRBuilder`
with `OarOcrEngine::from_builder` for model-specific settings or optional
orientation and rectification models. The fallible constructors return
`liteparse::LiteParseError`. All constructors use conservative batch sizes and
serialize page inference to avoid multiplying inference memory across
concurrently scheduled pages.
`OcrOptions::language` is not interpreted: the recognition model and character
dictionary define the supported languages, and a configured `ocr_language`
triggers a one-time warning to make that explicit.

## Features

- **`tesseract`** (default) — Built-in Tesseract OCR via `tesseract-rs`. Disable with `default-features = false` if you don't need OCR or want to use an HTTP OCR server instead.
- **`oar-ocr`** — Optional native ONNX backend via `oar-ocr`. Local or in-memory models; non-WASM Rust API only.
- **`oar-ocr-auto-download`** — Enables SHA-256-verified download and caching of registered model file names through `oar-ocr`.
- **`oar-ocr-cuda`**, **`oar-ocr-tensorrt`**, **`oar-ocr-directml`**, **`oar-ocr-coreml`**, **`oar-ocr-webgpu`**, **`oar-ocr-openvino`** — Forward the selected ONNX Runtime execution provider to `oar-ocr`.

The Node.js, Python, and WASM bindings build LiteParse with default features
disabled and do not expose these OAR features, so their published binaries do
not inherit the OAR model or runtime dependency footprint.

## Supported Formats

- PDF (`.pdf`)
- Microsoft Office (`.docx`, `.xlsx`, `.pptx`, etc.) — requires LibreOffice
- OpenDocument (`.odt`, `.ods`, `.odp`) — requires LibreOffice
- Images (`.png`, `.jpg`, `.tiff`, etc.)

## CLI

The crate also builds the `lit` CLI binary:

```bash
lit parse document.pdf
lit parse document.pdf --format json -o output.json
lit parse document.pdf --format markdown -o output.md
lit screenshot document.pdf -o ./screenshots
lit batch-parse ./input ./output
lit is-complex document.pdf
```

See `lit --help` for all options.

## License

Apache-2.0

---

### Overview (`ocr/easyocr/README.md`)

# EasyOCR Service

This is a simple Flask server that wraps EasyOCR to conform to the LiteParse OCR API specification (see `../../OCR_API_SPEC.md`).

## Build and Run

```bash
# install and run (in one command)
uv run server.py
```

## Usage

The service exposes a single endpoint:

- `POST /ocr` - Perform OCR on an uploaded image

### Parameters

- `file` - Image file (multipart/form-data)
- `language` - Language code (e.g., 'en', 'fr', 'de')

### Example

```bash
curl -X POST -F "file=@image.png" -F "language=en" http://localhost:8828/ocr
```

### Response Format

```json
{
  "results": [
    {
      "text": "recognized text",
      "bbox": [x1, y1, x2, y2],
      "confidence": 0.95
    }
  ]
}
```

This conforms to the LiteParse OCR API specification.

## Supported Languages

EasyOCR supports 80+ languages. Common language codes:

- `en` - English
- `fr` - French
- `de` - German
- `es` - Spanish
- `zh` - Chinese
- `ja` - Japanese
- `ko` - Korean
- `ar` - Arabic

Full list: https://www.jaided.ai/easyocr/

## Use with LiteParse

Once the server is running, use it with LiteParse OSS:

```bash
# Parse with EasyOCR
lit parse document.pdf --ocr-server-url http://localhost:8828/ocr

# With specific language
lit parse document.pdf --ocr-server-url http://localhost:8828/ocr --ocr-language zh
```

Or in code:

```typescript
import { LiteParse } from 'liteparse';

const parser = new LiteParse({
  ocrServerUrl: 'http://localhost:8828/ocr',
  ocrLanguage: 'en',
});

const result = await parser.parse('document.pdf');
```

## Testing

If you make changes to the server, make sure to adapt and run tests:

```bash
uv run pytest test_server.py
```

---

### Overview (`ocr/paddleocr/README.md`)

# PaddleOCR Service

This is a simple Flask server that wraps PaddleOCR to conform to the LiteParse OCR API specification (see `../../OCR_API_SPEC.md`).

PaddleOCR is especially fast and accurate for Chinese, Japanese, and Korean languages.

## Build and Run

```bash
# install and run (in one command)
uv run server.py
```

## Usage

The service exposes a single endpoint:

- `POST /ocr` - Perform OCR on an uploaded image

### Parameters

- `file` - Image file (multipart/form-data)
- `language` - Language code (e.g., 'en', 'zh', 'ja', 'ko')

### Example

```bash
curl -X POST -F "file=@image.png" -F "language=zh" http://localhost:8829/ocr
```

### Response Format

```json
{
  "results": [
    {
      "text": "recognized text",
      "bbox": [x1, y1, x2, y2],
      "confidence": 0.95
    }
  ]
}
```

This conforms to the LiteParse OCR API specification.

## Supported Languages

PaddleOCR supports 80+ languages with excellent support for CJK:

- `en` - English
- `zh` / `zh-cn` - Chinese (Simplified)
- `zh-tw` / `zh-hant` - Chinese (Traditional)
- `ja` - Japanese
- `ko` - Korean
- `fr` - French
- `de` - German
- `es` - Spanish
- `pt` - Portuguese
- `ru` - Russian
- `ar` - Arabic
- `hi` - Hindi/Devanagari

Full list: https://github.com/PaddlePaddle/PaddleOCR

## Performance

PaddleOCR is optimized for speed and accuracy:

- **Fast**: 2-3x faster than EasyOCR
- **Accurate**: Especially for Asian languages (Chinese, Japanese, Korean)
- **Lightweight**: Smaller model sizes

## Use with LiteParse

Once the server is running, use it with LiteParse:

```bash
# Parse with PaddleOCR
lit parse document.pdf --ocr-server-url http://localhost:8829/ocr

# With specific language
lit parse document.pdf --ocr-server-url http://localhost:8829/ocr --ocr-language zh
```

Or in code:

```typescript
import { LiteParse } from 'liteparse';

const parser = new LiteParse({
  ocrServerUrl: 'http://localhost:8829/ocr',
  ocrLanguage: 'zh',
});

const result = await parser.parse('document.pdf');
```

## Testing

If you make changes to the server, make sure to adapt and run tests:

```bash
uv run pytest test_server.py
```

## GPU Support

For GPU acceleration, set `use_gpu=True` in `server.py`.

## Notes

- First request may be slow as PaddleOCR downloads models
- Models are cached after first use
- Default port is 8829 (different from EasyOCR's 8828)

---

### Overview (`ocr/suryaocr/README.md`)

# Surya OCR Service

A FastAPI server wrapping [Surya OCR 2](https://github.com/datalab-to/surya) to
conform to the LiteParse OCR API specification (see `../../OCR_API_SPEC.md`).

Surya 2 is a multilingual OCR foundation model with strong accuracy across many
languages — a single model handles all languages with no per-language setup.

## Build and Run

```bash
# install and run (in one command)
uv run server.py
```

The first run downloads Surya model weights from Hugging Face and may take a few
minutes; weights are cached afterward.

## Inference backend (required)

Surya 2 is a VLM-backed model: text recognition runs through a separate
inference backend, which you must provide. Surya does **not** bundle it.

- **CPU / local (llama.cpp):** install the `llama-server` binary so Surya's
  `llamacpp` backend can spawn it:
  - macOS: `brew install llama.cpp`
  - Linux: `brew install llama.cpp`, or download a release from
    https://github.com/ggml-org/llama.cpp/releases and put `llama-server` on
    your `PATH` (or set `LLAMA_CPP_BINARY=/path/to/llama-server`).
- **GPU (vllm):** set `SURYA_INFERENCE_BACKEND=vllm` (requires a CUDA GPU).
- **External server:** point `SURYA_INFERENCE_URL` at an already-running Surya
  inference server to attach without spawning one locally.

Without a backend, startup succeeds and `GET /health` works, but `POST /ocr`
returns a 500 with "llama-server binary not found".

## Docker (GPU, single image)

The provided `Dockerfile` builds **one image that runs both the API and the
model server on a GPU**. It is based on the official CUDA llama.cpp image
(`ghcr.io/ggml-org/llama.cpp:server-cuda`), which ships a `qwen35`-capable
`llama-server` (Surya 2's custom GGUF architecture) with `libggml-cuda` and the
CUDA runtime. Surya spawns `llama-server` from `PATH` inside the same container,
so no sidecar or separate inference service is needed.

```bash
# Build
docker build -t suryaocr-liteparse:cuda .

# Run (requires the NVIDIA container runtime)
docker run --rm --gpus all -p 8830:8830 \
  -v "$HOME/.cache/suryaocr-models:/models" \
  suryaocr-liteparse:cuda
```

- `--gpus all` exposes the host GPUs; all model layers are offloaded
  (`LLAMA_CPP_NGL=99`).
- The `-v …:/models` mount caches weights (`HF_HOME=/models`). The **first**
  `POST /ocr` downloads the GGUF weights from Hugging Face and spawns
  `llama-server` (a few minutes); subsequent requests reuse the cache and the
  running server.

The image bakes in the backend configuration: `SURYA_INFERENCE_BACKEND=llamacpp`,
`LLAMA_CPP_NGL=99`, and `SURYA_GUIDED_LAYOUT=false`. The last one is required on
the upstream `llama-server` build: Surya's layout step is the only request that
sends a grammar, and its regex `pattern` breaks llama.cpp's
json-schema→GBNF converter (`failed to parse grammar`), which otherwise yields
empty results. With guided layout off, layout runs as free generation (Surya
parses the output itself) and the rest of the pipeline never uses a grammar.

To use a CUDA GPU via vLLM instead of bundled llama.cpp, set
`SURYA_INFERENCE_BACKEND=vllm` (see the backend section above).

Verify it is up:

```bash
curl http://localhost:8830/health
curl -X POST -F "file=@image.png" http://localhost:8830/ocr
```

## Usage

The service exposes:

- `POST /ocr` — Perform OCR on an uploaded image
- `GET /health` — Health check

### Parameters

- `file` — Image file (multipart/form-data)
- `language` — Language code (accepted for API compatibility; **ignored**, since
  Surya 2 is multilingual)

### Example

```bash
curl -X POST -F "file=@image.png" http://localhost:8830/ocr
```

### Response Format

```json
{
  "results": [
    {
      "text": "recognized text",
      "bbox": [x1, y1, x2, y2],
      "confidence": 0.95,
      "polygon": [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]
    }
  ]
}
```

Results are **block-level** (one entry per detected layout block), with each
block's HTML stripped to plain text. This conforms to the LiteParse OCR API spec.

## Supported Languages

Surya 2 is a single multilingual model — no `language` parameter is required
(the `language` field is accepted but ignored).

Per Surya's own benchmark, it scores an **87.2% overall pass rate across 91
languages**, with 38 of the 91 languages scoring ≥ 90% and 76 scoring ≥ 80%,
covering text accuracy, layout, tables, math, and reading order. It has strong
performance across both Latin and non-Latin scripts.

- Full 91-language results: https://github.com/datalab-to/surya/blob/master/static/docs/multilingual.md
- Project overview: https://github.com/datalab-to/surya

## Device / GPU

There are two device knobs, for two different parts of the pipeline:

- `LLAMA_CPP_NGL` controls how many layers of the **VLM** (the model that reads
  text) the `llamacpp` backend offloads to the GPU. `99` offloads everything;
  `0` keeps it on CPU. The Docker image sets `99`.
- `TORCH_DEVICE` controls the device for Surya's **helper torch models**
  (e.g. layout). Surya auto-detects, or you can force it:

  ```bash
  TORCH_DEVICE=cuda uv run server.py   # GPU
  TORCH_DEVICE=cpu  uv run server.py   # CPU
  ```

For a turnkey GPU deployment, prefer the Docker image above — it wires both up.

## Use with LiteParse

```bash
lit parse document.pdf --ocr-server-url http://localhost:8830/ocr
```

Or in code:

```typescript
import { LiteParse } from 'liteparse';

const parser = new LiteParse({
  ocrServerUrl: 'http://localhost:8830/ocr',
});

const result = await parser.parse('document.pdf');
```

## Testing

```bash
uv run pytest test_server.py
```

Tests mock the Surya predictor, so they run without downloading any models.

## Notes

- First request/startup may be slow while models download.
- Default port is 8830 (easyocr 8828, paddleocr 8829).
- Output is block-granular; Surya 2 has no per-line text API.

---

### Overview (`packages/node/README.md`)

# LiteParse Node.js

Node.js/TypeScript bindings for [LiteParse](https://github.com/run-llama/liteparse) — fast, lightweight PDF and document parsing with spatial text extraction.

## Installation

```bash
npm i @llamaindex/liteparse
```

This also installs the `lit` CLI command (use `npm i -g` for global access).

## Quick Start

```typescript
import { LiteParse } from '@llamaindex/liteparse';

const parser = new LiteParse();
const result = await parser.parse('document.pdf');
console.log(result.text);
console.log(`Source document pages: ${result.totalPages}`);

// Access structured data
for (const page of result.pages) {
  console.log(`Page ${page.pageNum}: ${page.textItems.length} text items`);
}
```

CommonJS is supported as well:

```javascript
const { LiteParse } = require('@llamaindex/liteparse');

(async () => {
  const parser = new LiteParse();
  const result = await parser.parse('document.pdf');
  console.log(result.text);
})();
```

### Bounded-memory parsing

For documents with many text items, consume page batches without retaining
earlier results:

```typescript
const parser = new LiteParse();
for await (const batch of parser.parseBatches('large.pdf', { batchSize: 20 })) {
  await processPages(batch.result.pages);
}
```

Each batch is an ordinary parse result covering `batch.startPage` through
`batch.endPage`, and becomes collectible as soon as you advance the iterator.
A non-PDF source is converted once, not once per batch.

Cross-page passes only see the pages in their own batch, so repeated
header/footer removal and image deduplication are batch-local and the output
can differ from `parse()`. Prefer `parse()` unless the size of the
materialized result is the problem.

## Markdown Output

LiteParse can render documents directly to Markdown including headings, tables, lists,
images, and links reconstructed from the spatial layout. Great for feeding LLMs
and RAG pipelines. The rendered Markdown is returned on `result.text`:

```typescript
const parser = new LiteParse({
  outputFormat: 'markdown',     // "json" | "text" | "markdown"
  imageMode: 'placeholder',     // "placeholder" | "off" | "embed"
  extractLinks: true,           // render [text](url) link syntax (default: true)
});
const result = await parser.parse('document.pdf');
console.log(result.text); // rendered Markdown
```

> Reconstruction quality varies with document complexity.

## Configuration

All options are passed to the constructor:

```
/* Detailed source-code truncated for AI context efficiency. */
```

When `extractImages` is true, image extraction is enabled. `imageOutputDir` requires
that explicit opt-in and writes the extracted bytes to disk. Each
`result.images` entry includes its page bbox, intrinsic pixel dimensions, rotation,
format, `name`, and `path`. Valid source JPEGs are preserved, exact duplicates reuse
one file, and JSON CLI output contains metadata only (no base64 image data).
`imageMode` controls Markdown presentation only and does not imply extraction. With
`extractImages: false`, lightweight Markdown placement refs are still collected and
`result.images` stays empty.

The Node CLI uses the same snake_case JSON schema as the Rust CLI; camelCase is
reserved for the programmatic Node API.

When `extractAnnotations` is enabled, each parsed page has an `annotations`
array containing the subtype, contents, author/title, PDF date strings,
viewport-space rectangle and quadpoint rectangles, and URI for external link
annotations. It is independent of `extractLinks`, which controls Markdown link
rendering. The field is omitted when extraction is disabled.

When `extractStructureTree` is enabled, each page has a `structureTree` containing
all tagged-PDF roots and recursive elements with type, ID, actual/alternate text,
title, typed attributes, MCIDs, children, and referenced link annotations. Untagged
pages have an empty `roots` array; the field is omitted when disabled.

Every result also carries the document's `/Info` `creator`/`producer` when
present. With `extractDocumentMetadata`, `result.docMeta` adds a provenance
object with dates, PDF version/security, signature state, incremental-save
markers, trailer ID comparison, the catalog's XMP packet (capped at 64 KiB;
skipped for sources over 16 MiB), and source size — off by default since it
streams the whole file, and absent for inputs converted from a non-PDF
format. These are API-level only, not in CLI JSON. With `extractContentBounds`
each page carries a `contentBounds` union bbox of its top-level content
objects. With `extractXfaPackets`, `result.xfaPackets` lists each raw XFA
packet (index, name, content length, XML content); non-XFA documents yield an
empty array. Screenshots draw form-field appearances into the raster and
report `isSolidFill` per page, plus detected solid `rects` when
`detectScreenshotRects` is enabled.

## Parsing from Bytes

Pass a `Buffer` or `Uint8Array` directly — useful for HTTP responses or in-memory data:

```typescript
import { readFile } from 'fs/promises';

const pdfBytes = await readFile('document.pdf');
const result = await parser.parse(pdfBytes);
console.log(result.text);
```

## Worker Pool and Hard Timeouts

PDFium is not thread-safe, so all parses inside one process serialize on a
process-global lock — even `Promise.all` over multiple `parse()` calls runs
them one at a time.

For high-throughput services, or for cases when you need to enforce a
runtime timeout, run parses in a pool of persistent worker processes instead:

```typescript
import { LiteParse, ParseTimeoutError } from '@llamaindex/liteparse';

const parser = new LiteParse({ poolSize: 4, parseTimeoutMs: 15_000 });
await parser.warmUp(); // optional: pre-initialize workers (~45ms total)

try {
  const result = await parser.parse('document.pdf');
} catch (e) {
  if (e instanceof ParseTimeoutError) {
    console.warn(`killed rogue document: ${e.source} (deadline ${e.timeoutMs}ms)`);
  }
}

parser.close(); // frees workers immediately; an idle pool never blocks exit
```

## Screenshots

Generate PNG screenshots of document pages:

```typescript
const screenshots = parser.screenshot('document.pdf', [1, 2, 3]);
for (const s of screenshots) {
  console.log(`Page ${s.pageNum}: ${s.width}x${s.height}`);
  // s.imageBuffer contains PNG bytes
}
```

## Document Complexity

Before committing to a full parse, check whether a document needs OCR or heavier
processing. `isComplex` is a cheap, text-layer-only pass that returns one entry per page
with a `needsOcr` verdict and the signals behind it — useful for routing documents to
different pipelines, rejecting ones you can't handle, or estimating cost.

```typescript
const parser = new LiteParse();
const pages = await parser.isComplex('document.pdf');

if (pages.some((p) => p.needsOcr)) {
  // Route to the OCR-enabled pipeline
  const result = await parser.parse('document.pdf');
} else {
  // Cheap path — skip OCR entirely
  const result = await new LiteParse({ ocrEnabled: false }).parse('document.pdf');
}

// Inspect why specific pages were flagged
for (const page of pages.filter((p) => p.needsOcr)) {
  console.log(`Page ${page.pageNumber}: ${page.reasons.join(', ')}`);
}
```

`reasons` is one of `"scanned"`, `"no-text"`, `"sparse-text"`, `"embedded-images"`,
`"garbled"`, `"vector-text"`, or `"annotation-text"`. Raw bytes work here too.

## Supported Formats

- PDF (`.pdf`)
- Microsoft Office (`.docx`, `.xlsx`, `.pptx`, etc.) — requires LibreOffice
- OpenDocument (`.odt`, `.ods`, `.odp`) — requires LibreOffice
- Images (`.png`, `.jpg`, `.tiff`, etc.)
- And more!

## CLI

The npm package includes the `lit` CLI:

```bash
lit parse document.pdf
lit parse document.pdf --format json -o output.json
lit parse document.pdf --format json --extract-annotations
lit parse document.pdf --format json --extract-form-fields
lit screenshot document.pdf -o ./screenshots
lit batch-parse ./input ./output
lit is-complex document.pdf
```

---

### Overview (`packages/python/README.md`)

# LiteParse Python

Python bindings for [LiteParse](https://github.com/run-llama/liteparse) — fast, lightweight PDF and document parsing with spatial text extraction.

## Installation

```bash
pip install liteparse
```

This also installs the `lit` CLI command.

## Quick Start

```python
from liteparse import LiteParse

parser = LiteParse()
result = parser.parse("document.pdf")
print(result.text)
print(f"Source document pages: {result.total_pages}")

# Access structured data
for page in result.pages:
    print(f"Page {page.page_num}: {len(page.text_items)} text items")
```

## Markdown Output

LiteParse can render documents directly to Markdown including headings, tables, lists,
images, and links reconstructed from the spatial layout. Great for feeding LLMs
and RAG pipelines. The rendered Markdown is returned on `result.text`:

```python
parser = LiteParse(
    output_format="markdown",   # "json" | "text" | "markdown"
    image_mode="placeholder",   # "placeholder" | "off" | "embed"
    extract_links=True,         # render [text](url) link syntax (default: True)
)
result = parser.parse("document.pdf")
print(result.text)  # rendered Markdown
```

> Reconstruction quality varies with document complexity.

## Configuration

All options are passed to the constructor:

```
/* Detailed source-code truncated for AI context efficiency. */
```

When ``extract_images=True``, image extraction is enabled. ``image_output_dir``
requires that explicit opt-in and writes the extracted bytes to disk. Each
``result.images`` entry includes its page bbox, intrinsic pixel dimensions, rotation,
format, ``name``, and ``path``. Valid source JPEGs are preserved, exact duplicates
reuse one file, and JSON CLI output contains metadata only (no base64 image data).
``image_mode`` controls Markdown presentation only and does not imply extraction.
With ``extract_images=False``, lightweight Markdown placement refs are still collected
and ``result.images`` stays empty.

When `extract_annotations` is enabled, each parsed page has an `annotations`
list containing the subtype, contents, author/title, PDF date strings,
viewport-space rectangle and quadpoint rectangles, and URI for external link
annotations. It is independent of `extract_links`, which controls Markdown
link rendering. The field is `None` when extraction is disabled.

When ``extract_structure_tree=True``, each page has a ``structure_tree`` containing
all tagged-PDF roots and recursive elements with type, ID, actual/alternate text,
title, typed attributes, MCIDs, children, and referenced link annotations. Untagged
pages have an empty ``roots`` list; the field is ``None`` when disabled.

Every result also carries ``creator``/``producer`` from the PDF ``/Info``
dictionary. With ``extract_document_metadata=True``, ``result.doc_meta`` adds a
provenance object with dates, PDF version/security, signature state,
incremental-save markers, trailer ID comparison, the catalog's XMP packet
(capped at 64 KiB; skipped for sources over 16 MiB), and source size. It is off by default because it streams the whole source file,
and it is ``None`` for inputs converted from a non-PDF format. These document
fields are API-only and do not alter default CLI JSON.

## Parsing from Bytes

Pass raw PDF bytes directly — useful for web uploads or downloaded files:

```python
with open("document.pdf", "rb") as f:
    result = parser.parse(f.read())
print(result.text)
```

## Worker Pool and Hard Timeouts

PDFium is not thread-safe, so in-process parses serialize on a process-global
lock. For high-throughput services, or cases where you need to enforce a timeout,
run parses in a pool of persistent worker processes instead:

```python
from liteparse import LiteParse, ParseTimeoutError

parser = LiteParse(pool_size=4, parse_timeout=15)
parser.warm_up()  # optional: pre-initialize workers (~60ms total)

try:
    result = parser.parse("document.pdf")
except ParseTimeoutError as e:
    print(f"killed rogue document: {e.source} (deadline {e.timeout}s)")

parser.close()  # or use `with LiteParse(...) as parser:`
```

## Screenshots

Generate PNG screenshots of document pages:

```python
screenshots = parser.screenshot("document.pdf", page_numbers=[1, 2, 3])
for s in screenshots:
    print(f"Page {s.page_num}: {s.width}x{s.height}")
    with open(f"page_{s.page_num}.png", "wb") as f:
        f.write(s.image_bytes)
```

## Document Complexity

Before committing to a full parse, check whether a document needs OCR or heavier
processing. `is_complex` is a cheap, text-layer-only pass that returns one entry per page
with a `needs_ocr` verdict and the signals behind it — useful for routing documents to
different pipelines, rejecting ones you can't handle, or estimating cost.

```python
parser = LiteParse()
pages = parser.is_complex("document.pdf")

if any(p.needs_ocr for p in pages):
    # Route to the OCR-enabled pipeline
    result = parser.parse("document.pdf")
else:
    # Cheap path — skip OCR entirely
    result = LiteParse(ocr_enabled=False).parse("document.pdf")

# Inspect why specific pages were flagged
for page in pages:
    if page.needs_ocr:
        print(f"Page {page.page_number}: {', '.join(page.reasons)}")
```

`reasons` is one of `"scanned"`, `"no-text"`, `"sparse-text"`, `"embedded-images"`,
`"garbled"`, `"vector-text"`, or `"annotation-text"`. Raw bytes work here too.

## Supported Formats

- PDF (`.pdf`)
- Microsoft Office (`.docx`, `.xlsx`, `.pptx`, etc.) — requires LibreOffice
- OpenDocument (`.odt`, `.ods`, `.odp`) — requires LibreOffice
- Images (`.png`, `.jpg`, `.tiff`, etc.)
- And more!

## CLI

The Python package includes the `lit` CLI:

```bash
lit parse document.pdf
lit parse document.pdf --format json -o output.json
lit parse document.pdf --format json --extract-annotations
lit parse document.pdf --format json --extract-form-fields
lit screenshot document.pdf -o ./screenshots
lit batch-parse ./input ./output
lit is-complex document.pdf
```

---

### Overview (`packages/wasm/README.md`)

# @llamaindex/liteparse-wasm

Browser/WebAssembly build of [LiteParse](https://github.com/run-llama/liteparse) — a fast, lightweight PDF parser with spatial text extraction.

This package runs entirely in the browser. No server, no cloud calls.

## Install

```sh
npm install @llamaindex/liteparse-wasm
```

## Quick start

```ts
import init, { LiteParse } from "@llamaindex/liteparse-wasm";

// Load the wasm module (point at the file shipped with the package).
await init();

const parser = new LiteParse({
  ocrEnabled: false, // OCR requires a JS-side engine (see below)
  outputFormat: "json",
});

// `data` is a Uint8Array (e.g. from fetch / File / drag-drop).
const bytes = new Uint8Array(await file.arrayBuffer());
const result = await parser.parse(bytes);

console.log(result.text);          // full document text
console.log(result.pages[0]);      // per-page items with bboxes
```

## Document complexity

Before committing to a full parse, check whether a document needs OCR or heavier
processing. `isComplex` is a cheap, text-layer-only pass that returns one entry per page
with a `needsOcr` verdict and the signals behind it — useful for routing documents or
deciding whether the JS-side OCR engine is worth wiring up.

```ts
const parser = new LiteParse({ ocrEnabled: false });
const bytes = new Uint8Array(await file.arrayBuffer());
const pages = await parser.isComplex(bytes);

if (pages.some((p) => p.needsOcr)) {
  // This document would benefit from OCR — see "OCR in the browser" below
  for (const page of pages.filter((p) => p.needsOcr)) {
    console.log(`Page ${page.pageNumber}: ${page.reasons.join(", ")}`);
  }
}
```

`reasons` is one of `"scanned"`, `"no-text"`, `"sparse-text"`, `"embedded-images"`,
`"garbled"`, `"vector-text"`, or `"annotation-text"`.

## Config options

All optional, camelCase:

| Option | Type | Default | Description |
|---|---|---|---|
| `ocrLanguage` | `string` | `"eng"` | Language code passed to the OCR engine |
| `ocrEnabled` | `boolean` | `false` | Run OCR on text-sparse pages |
| `maxPages` | `number` | `1000` | Stop after this many pages |
| `targetPages` | `string` | — | e.g. `"1-5,10,15-20"` |
| `extractScreenshots` | `boolean` | `false` | Return parsed pages as PNG bytes on `result.screenshots` |
| `dpi` | `number` | `150` | Render DPI for OCR / screenshots |
| `outputFormat` | `"json" \| "text" \| "markdown"` | `"json"` | Output format; `"markdown"` returns rendered Markdown on `result.text` |
| `imageMode` | `"off" \| "placeholder" \| "embed"` | `"placeholder"` | How raster images are surfaced in markdown output |
| `extractLinks` | `boolean` | `true` | Render hyperlink annotations as `[text](url)` in markdown output |
| `extractVectorGraphics` | `boolean` | `false` | Include page-scoped shapes and merged horizontal/vertical lines |
| `extractAnnotations` | `boolean` | `false` | Include page annotations and their metadata/geometry in structured output |
| `extractStructureTree` | `boolean` | `false` | Include the tagged-PDF logical structure tree |
| `preserveVerySmallText` | `boolean` | `false` | Keep tiny text that's normally filtered |
| `password` | `string` | — | Password for protected PDFs |
| `quiet` | `boolean` | `false` | Suppress progress logging |
| `numWorkers` | `number` | `1` | Maximum concurrent OCR calls |
| `ocrEngine` | `object` | — | JS-side OCR engine (see below) |

## OCR in the browser

The native HTTP-OCR and Tesseract backends are not available in the browser.
To use OCR, set `ocrEnabled: true` and supply an `ocrEngine` object with a
`recognize` method. Supplying `ocrEngine` alone does not enable OCR.

```ts
const parser = new LiteParse({
  ocrEnabled: true,
  ocrLanguage: "eng",
  // Use more than 1 only if the engine supports concurrent jobs.
  numWorkers: 2,
  ocrEngine: {
    /**
     * @param imageData PNG-encoded image bytes
     * @param width  rendered page width  in pixels
     * @param height rendered page height in pixels
     * @param language e.g. "eng"
     * @returns array of { text, bbox: [x1,y1,x2,y2], confidence }
     */
    async recognize(imageData, width, height, language) {
      // e.g. call a worker that wraps tesseract.js, or a remote OCR service
      return [
        { text: "Hello", bbox: [10, 20, 80, 40], confidence: 0.98 },
      ];
    },
  },
});
```

LiteParse keeps at most `numWorkers` calls to `recognize` active. When one call
finishes, LiteParse prepares and starts the next page without waiting for the
other calls. An OCR engine can send the calls to a Web Worker pool or to
concurrent HTTP requests. Results stay assigned to their source pages when
jobs finish out of order. Active calls can keep up to `numWorkers` rendered
page rasters in memory, so use a small value for high-DPI documents.

A failed `recognize` call only affects its own page. With `ocrFailureFatal:
true` (the default), `parse()` rejects only when every OCR call failed and at
least one failed page had little native text. If parsing fails, OCR calls that
have already started can continue.

## Building from source

Requires Rust + [`wasm-pack`](https://rustwasm.github.io/wasm-pack/):

```sh
# from packages/wasm
npm run build           # web target (default)
npm run build:bundler   # for webpack/rollup/vite
npm run build:nodejs    # for node.js
```

Output goes to `pkg/`.

## License

Apache-2.0

---

### Src Content Docs Liteparse Index (`docs/src/content/docs/liteparse/index.md`)

---
title: What is LiteParse?
description: Fast, local PDF parsing with spatial text parsing, OCR, and bounding boxes.
sidebar:
  order: 0
---

LiteParse is an open-source document parsing library that parses text with spatial layout information and bounding boxes. Written in Rust for speed and reliability, it runs entirely on your machine with no cloud dependencies, no LLMs, and no API keys.

LiteParse is designed specifically for use cases that require fast, accurate text parsing: real-time applications, coding agents, and local workflows. It provides a simple CLI and library API for parsing PDFs, Office documents, and images, with built-in OCR support.

<img src="https://github.com/user-attachments/assets/07ba6a82-6bb1-4dea-b0ef-cad7df7d1622" alt="out">

## What can LiteParse do?

- **Parse PDFs** with precise spatial layout. Text comes back positioned where it appears on the page
- **Render to Markdown** with headings, tables, lists, images, and links — clean structured output for LLMs and RAG pipelines
- **Extract bounding boxes** for every text line, ready for downstream processing or visualization
- **OCR scanned documents** using built-in Tesseract or plug in your own OCR server
- **Parse Office files and images** with support for DOCX, XLSX, PPTX, PNG, JPG, and more via automatic conversion
- **Screenshot PDF pages** as high-quality images for LLM-based workflows
- **Pull structured PDF data** — embedded images, vector graphics, annotations, AcroForm fields, and tagged-structure trees
- **Score document complexity** before you parse, so you can route scanned or multi-column documents to the right pipeline
- **Use from Node.js/TypeScript, Python, Rust, or the browser (WASM)** — whatever fits your stack

## Get started

- [Getting started](/liteparse/getting_started/): Install LiteParse and parse your first document.
- [Markdown output](/liteparse/guides/markdown/): Render documents to clean, structured Markdown.
- [Extraction options](/liteparse/guides/extraction/): Opt in to images, form fields, annotations, and more.
- [Document complexity](/liteparse/guides/complexity/): Detect scanned, multi-column, and table-heavy pages up front.
- [Library usage](/liteparse/guides/library-usage/): Use LiteParse from TypeScript or Python code.
- [Browser usage (WASM)](/liteparse/guides/browser-usage/): Run LiteParse in the browser with zero server dependencies.
- [Agent skill](/liteparse/guides/agent-skill/): Give Claude Code, Cursor, or Codex the ability to parse documents locally.
- [CLI reference](/liteparse/cli-reference/): Complete command and option reference.
- [API reference](/liteparse/api/): Detailed API documentation (rust) for all public types and functions. The same types apply across all language bindings.

---

### Src Content Docs Liteparse Cli Reference (`docs/src/content/docs/liteparse/cli-reference.md`)

---
title: CLI Reference
description: Complete reference for all LiteParse CLI commands and options.
sidebar:
  order: 5
---

LiteParse provides the `lit` CLI with four commands: `parse`, `batch-parse`, `screenshot`, and `is-complex`. The CLI is the same whether installed via `npm`, `pip`, or built from Rust source.

## `lit parse`

Parse a single document.

```
lit parse [options] <file>
```

### Arguments

| Argument | Description |
|----------|-------------|
| `file` | Path to the document file, or `-` to read from stdin |

### Options

| Option | Description | Default |
|--------|-------------|---------|
| `-o, --output <file>` | Write output to a file instead of stdout | — |
| `--format <format>` | Output format: `json`, `text`, or `markdown` | `text` |
| `--image-mode <mode>` | Markdown image handling: `off`, `placeholder`, or `embed` | `placeholder` |
| `--image-output-dir <dir>` | Directory to write extracted images to. Requires `--extract-images` (or `--image-mode embed`, which implies it) | — |
| `--no-links` | Emit link anchor text as plain text (no `[text](url)`) in markdown | — |
| `--keep-headers-footers` | Keep running header/footer chrome in markdown instead of stripping it | — |
| `--no-ocr` | Disable OCR entirely | — |
| `--ocr-language <lang>` | OCR language code (Tesseract format) | `eng` |
| `--ocr-server-url <url>` | HTTP OCR server URL | — (uses Tesseract) |
| `--ocr-server-header <header>` | Extra HTTP header for OCR server requests, as `"Name: Value"`. Repeatable | — |
| `--tessdata-path <path>` | Path to tessdata directory | — (uses `TESSDATA_PREFIX` env var) |
| `--num-workers <n>` | Pages to OCR in parallel | CPU cores - 1 |
| `--max-pages <n>` | Maximum pages to parse | `1000` |
| `--target-pages <pages>` | Pages to parse (e.g., `"1-5,10"`) | — (all pages) |
| `--dpi <dpi>` | Rendering DPI | `150` |
| `--preserve-small-text` | Keep very small text | — |
| `--password <password>` | Password for encrypted/protected documents | — |
| `-q, --quiet` | Suppress progress output | — |

### Extraction options

By default, JSON output contains only text, geometry, and page metadata. Each option below is **off by default** and adds a new key to the JSON output. Enable only what you need — every one of them costs parse time and output size.

| Option | Adds to JSON | Description |
|--------|--------------|-------------|
| `--extract-images` | `images[]`, `image_error_count` | Extract embedded raster image bytes and metadata. Pair with `--image-output-dir` to write files to disk |
| `--extract-vector-graphics` | `pages[].vector_graphics` | Vector shapes and lines (`{ shapes, lines }`) with bounding boxes, stroke/fill colors |
| `--extract-annotations` | `pages[].annotations` | PDF annotations: subtype, contents, timestamps, rects, link URIs |
| `--extract-form-fields` | `pages[].form_fields` | AcroForm widgets: type, name, value, flags, options, rect |
| `--extract-structure-tree` | `pages[].structure_tree` | Tagged-PDF logical structure tree (`{ roots }`), including per-element attributes |
| `--extract-blocks` | `pages[].blocks` | Classified layout blocks in reading order (headings, paragraphs, list items, tables with per-cell boxes, code, rules, figures), each with a bounding box |
| `--extract-content-bounds` | `pages[].content_bounds` | Bounding box of actual content on the page, as a `Rect` |
| `--extract-xfa-packets` | `xfa_packets[]` (document level) | Raw XFA packets from XFA-based forms |
| `--extract-text-metadata` | extra keys on each `text_items[]` entry | Rich per-item typography: `font_height`, `font_ascent`, `font_descent`, `font_weight`, `text_width`, `font_is_buggy`, `mcid`, `fill_color`, `stroke_color`, plus `rotation` |
| `--complexity` | `pages[].complexity` | Per-page complexity signals, including the nested `layout` object. See the [Document Complexity guide](/liteparse/guides/complexity/) |

### Examples

```bash
# Basic text parsing
lit parse report.pdf

# JSON output with bounding boxes
lit parse report.pdf --format json -o report.json

# Markdown output (headings, tables, lists, images, links)
lit parse report.pdf --format markdown -o report.md

# Markdown with extracted images written to disk
lit parse report.pdf --format markdown --extract-images --image-output-dir ./images

# Pull form fields and annotations out of a filled-in PDF
lit parse form.pdf --format json --extract-form-fields --extract-annotations

# Rich typography metadata on every text item
lit parse report.pdf --format json --extract-text-metadata

# Inline complexity signals (including layout) alongside the parse
lit parse report.pdf --format json --complexity

# Authenticated OCR server
lit parse scan.pdf --ocr-server-url https://ocr.internal/ocr \
  --ocr-server-header "Authorization: Bearer $TOKEN"

# Parse pages 1-5 only, no OCR
lit parse report.pdf --target-pages "1-5" --no-ocr

# High-DPI rendering with French OCR
lit parse report.pdf --dpi 300 --ocr-language fra

# Use an external OCR server
lit parse report.pdf --ocr-server-url http://localhost:8828/ocr

# Pipe output to another tool
lit parse report.pdf -q | wc -l

# Parse a remote file via stdin
curl -sL https://example.com/report.pdf | lit parse --no-ocr -
```

---

## `lit batch-parse`

Parse multiple documents in a directory.

```
lit batch-parse [options] <input-dir> <output-dir>
```

### Arguments

| Argument | Description |
|----------|-------------|
| `input-dir` | Directory containing documents to parse |
| `output-dir` | Directory for output files |

### Options

| Option | Description | Default |
|--------|-------------|---------|
| `--format <format>` | Output format: `json`, `text`, or `markdown` | `text` |
| `--no-ocr` | Disable OCR entirely | — |
| `--ocr-language <lang>` | OCR language code | `eng` |
| `--ocr-server-url <url>` | HTTP OCR server URL | — (uses Tesseract) |
| `--ocr-server-header <header>` | Extra HTTP header for OCR server requests, as `"Name: Value"`. Repeatable | — |
| `--tessdata-path <path>` | Path to tessdata directory | — |
| `--num-workers <n>` | Pages to OCR in parallel | CPU cores - 1 |
| `--max-pages <n>` | Maximum pages per file | `1000` |
| `--dpi <dpi>` | Rendering DPI | `150` |
| `--recursive` | Search subdirectories | — |
| `--extension <ext>` | Only process this extension (e.g., `".pdf"`) | — (all supported) |
| `--password <password>` | Password for encrypted/protected documents (applied to all files) | — |
| `-q, --quiet` | Suppress progress output | — |

`batch-parse` accepts the same [extraction options](#extraction-options) as `lit parse`: `--extract-images`, `--extract-vector-graphics`, `--extract-annotations`, `--extract-form-fields`, `--extract-structure-tree`, `--extract-blocks`, `--extract-content-bounds`, `--extract-xfa-packets`, `--extract-text-metadata`, and `--complexity`.

It does **not** accept `--target-pages`, `--preserve-small-text`, `--image-mode`, `--image-output-dir`, `--no-links`, or `--keep-headers-footers`. Use `lit parse` per file when you need those.

### Examples

```bash
# Parse all supported files in a directory
lit batch-parse ./documents ./output

# Recursively parse only PDFs
lit batch-parse ./documents ./output --recursive --extension ".pdf"

# Batch parse with JSON output and no OCR
lit batch-parse ./documents ./output --format json --no-ocr
```

---

## `lit screenshot`

Generate page images from a document (PDF, DOCX, XLSX, images, etc.).

```
lit screenshot [options] <file>
```

### Arguments

| Argument | Description |
|----------|-------------|
| `file` | Path to the document file |

### Options

| Option | Description | Default |
|--------|-------------|---------|
| `-o, --output-dir <dir>` | Output directory | `./screenshots` |
| `--target-pages <pages>` | Pages to screenshot (e.g., `"1,3,5"` or `"1-5"`) | — (all pages) |
| `--dpi <dpi>` | Rendering DPI | `150` |
| `--password <password>` | Password for encrypted/protected documents | — |
| `-q, --quiet` | Suppress progress output | — |

### Examples

```bash
# Screenshot all pages of a PDF
lit screenshot document.pdf -o ./pages

# Screenshot a Word document (requires LibreOffice)
lit screenshot report.docx -o ./pages

# First 5 pages at high DPI
lit screenshot document.pdf --target-pages "1-5" --dpi 300 -o ./pages

# Specific pages only
lit screenshot document.pdf --target-pages "1,5,10" -o ./pages
```

---

## `lit is-complex`

Check whether a document needs OCR or heavier parsing — a cheap pre-parse pass over the text layer only (no rasterization, no OCR). See the [Document Complexity guide](/liteparse/guides/complexity/) for details.

```
lit is-complex [options] <file>
```

The command prints per-page JSON to **stdout**, a human-readable verdict to **stderr**, and exits **non-zero when any page needs OCR** — so it works as a shell predicate or a `jq` source.

### Arguments

| Argument | Description |
|----------|-------------|
| `file` | Path to the document file |

### Options

| Option | Description | Default |
|--------|-------------|---------|
| `--compact` | Emit dense, whitespace-free JSON instead of pretty-printed | — |
| `--max-pages <n>` | Maximum pages to check | `1000` |
| `--target-pages <pages>` | Pages to check (e.g., `"1-5,10,15-20"`) | — (all pages) |
| `--password <password>` | Password for encrypted/protected documents | — |
| `-q, --quiet` | Suppress the stderr verdict | — |

### Examples

```bash
# Print the complexity verdict and per-page JSON
lit is-complex document.pdf

# Use as a shell predicate: only parse with --no-ocr when simple
lit is-complex document.pdf --quiet && lit parse document.pdf --no-ocr

# List the page numbers that need OCR
lit is-complex document.pdf --compact | jq '[.[] | select(.needs_ocr) | .page_number]'
```

---

## Global options

These options are available on all commands:

| Option | Description |
|--------|-------------|
| `-h, --help` | Show help for a command |
| `-V, --version` | Show version number |

---

### Src Content Docs Liteparse Getting Started (`docs/src/content/docs/liteparse/getting_started.mdx`)

---
title: Getting Started
description: Install LiteParse and parse your first document in under a minute.
sidebar:
  order: 1
---

LiteParse is available for Node.js, Python, and as a standalone Rust binary. 

All versions (except WASM) ship the same CLI and core library capabilities.

## Installation

<Tabs>
<TabItem label="Node.js / TypeScript" value="node">


```bash
npm i @llamaindex/liteparse
```
</TabItem>
<TabItem label="Python" value="python">

```bash
pip install liteparse
```
</TabItem>
<TabItem label="Rust" value="rust">

```bash
cargo install liteparse
```

</TabItem>
<TabItem label="Browser (WASM)" value="wasm">

```bash
npm i @llamaindex/liteparse-wasm
```

Since the WASM package is designed for browser usage, it does not include the `lit` CLI command. Instead, you can use the exported functions directly from JavaScript to parse documents in the browser environment.

For full browser usage and limitations, see the [WASM package guide](/liteparse/guides/browser-usage/).
</TabItem>
</Tabs>

## Quick start

Once installed, parse from the command line:

```bash
# Parse a PDF and print text to stdout
lit parse document.pdf

# Save output to a file
lit parse document.pdf -o output.txt

# Get structured JSON with bounding boxes
lit parse document.pdf --format json -o output.json

# Render to Markdown (headings, tables, lists, images, links)
lit parse document.pdf --format markdown -o output.md

# Parse only specific pages
lit parse document.pdf --target-pages "1-5,10,15-20"
```

### Batch parsing

Parse an entire directory of documents at once:

```bash
lit batch-parse ./pdfs ./outputs
```

### Screenshots

Generate page images from a PDF for LLM agents or visual workflows:

```bash
lit screenshot document.pdf -o ./screenshots
```

## Next steps

- [Markdown output](/liteparse/guides/markdown/): Render documents to clean, structured Markdown.
- [Extraction options](/liteparse/guides/extraction/): Opt in to embedded images, vector graphics, annotations, form fields, and structure trees.
- [Library usage](/liteparse/guides/library-usage/): Use LiteParse programmatically from TypeScript or Python.
- [OCR configuration](/liteparse/guides/ocr/): Configure Tesseract, use an external OCR server, or bring your own.
- [Multi-format support](/liteparse/guides/multi-format/): Parse DOCX, XLSX, PPTX, images, and more.
- [Document complexity](/liteparse/guides/complexity/): Check whether a document needs OCR / heavy processing before parsing, and route accordingly.
- [Browser usage (WASM)](/liteparse/guides/browser-usage/): Run LiteParse entirely in the browser.
- [Agent skill](/liteparse/guides/agent-skill/): Add LiteParse as a skill for coding agents.
- [CLI reference](/liteparse/cli-reference/): Complete command and option reference.

---

### Src Content Docs Liteparse Guides Agent Skill (`docs/src/content/docs/liteparse/guides/agent-skill.md`)

---
title: Agent Skill
description: Add LiteParse as a skill for coding agents like Claude Code, Cursor, and others.
sidebar:
  order: 11
---

LiteParse ships as a **coding agent skill** — a self-contained capability that Claude Code, Cursor,
Codex, and other compatible agents load on demand. Installing it gives your agent the ability to
parse documents, extract text and bounding boxes, and generate screenshots, all locally and with no
API key.

## Installation

Add the LiteParse skill to your project with the [`skills`](https://github.com/vercel-labs/skills)
CLI:

```bash
npx skills add run-llama/llamaparse-agent-skills --skill liteparse
```

This downloads a skill file that compatible coding agents pick up automatically.

You can also copy [`SKILL.md`](https://github.com/run-llama/llamaparse-agent-skills/blob/main/skills/liteparse/SKILL.md)
into your own skills setup by hand, or install the `liteparse` plugin from the
[agent plugins marketplace](https://github.com/run-llama/llamaparse-agent-plugins) to enable it from
inside Claude Code or Codex.

### Requirements

| Requirement | Needed for |
| --- | --- |
| Node 18+ and `npm i @llamaindex/liteparse` | The `lit` CLI the skill drives — verify with `lit --version` |
| [LibreOffice](https://www.libreoffice.org/) | Parsing Office files (DOCX, XLSX, PPTX) |
| [`uv`](https://docs.astral.sh/uv/) | The bundled ranked-search helper |

No API key is required since everything runs on your machine.

## What the skill teaches

The skill is more than an install: it encodes the extraction patterns that keep an agent's context
small and its runs cheap. Without it, agents commonly re-parse the same document on every search and
dump entire pages into the conversation.

- **Parse once to a file, then search that file.** Each `lit parse` re-extracts the whole document,
  so the skill has the agent parse to a temp file and run all subsequent searches against it.
- **Minimize round-trips.** Fetch a match and its surrounding context in a single command, and batch
  independent lookups together rather than spending a turn per search term.
- **Bound every output.** Results are capped so a single lookup can't flood the context window.
- **Escalate to ranked search** when keyword matching stalls, instead of firing off keyword variants
  one turn at a time.

## Example prompts

Once the skill is installed, you can ask your coding agent things like:

- "Parse this PDF and extract the text as JSON"
- "Extract text from all the DOCX files in the `./contracts` folder"
- "Screenshot pages 1-5 of this PDF at 300 DPI"
- "Parse this scanned document using the PaddleOCR server on localhost:8828"
- "Get the bounding boxes for all text on page 3"

## Related

LiteParse is one of several ways to give an agent document-processing capabilities. For cloud parsing
of complex documents, MCP servers, and workflow nodes, see
[Using LlamaIndex with AI Agents](/for-agents/).

---

### Src Content Docs Liteparse Guides Browser Usage (`docs/src/content/docs/liteparse/guides/browser-usage.md`)

---
title: Browser Usage (WASM)
description: Run LiteParse entirely in the browser with the WASM package.
sidebar:
  order: 9
---

LiteParse ships a WebAssembly package that runs entirely in the browser — no server, no cloud calls. It supports PDF parsing and custom OCR engines implemented in JavaScript.

## Install

```bash
npm install @llamaindex/liteparse-wasm
```

## Quick start

```typescript
import init, { LiteParse } from "@llamaindex/liteparse-wasm";

// Load the WASM module
await init();

const parser = new LiteParse({
  ocrEnabled: false,
  outputFormat: "json",
});

// data is a Uint8Array (e.g. from <input type="file"> or fetch)
const bytes = new Uint8Array(await file.arrayBuffer());
const result = await parser.parse(bytes);

console.log(result.text);
console.log(result.pages[0]);
```

## What works in the browser

- **PDF parsing** from `Uint8Array` input (use `file.arrayBuffer()` to get bytes from a file picker for example)
- **Custom OCR** via the `ocrEngine` callback interface (see below)
- **Text, JSON, and markdown output formats**
- **Document complexity** via `parser.isComplex(bytes)` — see the [complexity guide](/liteparse/guides/complexity/)
- **The extraction options** — annotations, form fields, structure trees, vector graphics, and the rest. See [Extraction options](/liteparse/guides/extraction/)

## What doesn't work

- **File path input** — pass `Uint8Array` instead
- **DOCX/XLSX/PPTX conversion** — requires LibreOffice, which isn't available in the browser
- **Built-in Tesseract or HTTP OCR** — use the custom `ocrEngine` interface instead
- **Screenshots** — not available in the WASM build
- **`imageOutputDir`** — there is no filesystem to write to. Use `extractImages` and read the bytes from the result instead

## OCR in the browser

The native Tesseract and HTTP OCR backends are not available in WASM. To use OCR, pass a custom `ocrEngine` object with a `recognize` method:

```typescript
const parser = new LiteParse({
  ocrEnabled: true,
  ocrLanguage: "eng",
  // Use more than 1 only if the engine supports concurrent jobs.
  numWorkers: 2,
  ocrEngine: {
    /**
     * @param imageData PNG-encoded image bytes
     * @param width  rendered page width in pixels
     * @param height rendered page height in pixels
     * @param language e.g. "eng"
     * @returns array of { text, bbox: [x1, y1, x2, y2], confidence }
     */
    async recognize(imageData, width, height, language) {
      // e.g. call a Web Worker wrapping tesseract.js, or a remote OCR service
      return [
        { text: "Hello", bbox: [10, 20, 80, 40], confidence: 0.98 },
      ];
    },
  },
});
```

This lets you plug in any OCR implementation — a Web Worker running tesseract.js, a cloud OCR API, or anything else that returns text with bounding boxes.

LiteParse keeps at most `numWorkers` calls to `recognize` active. When one call
finishes, LiteParse prepares and starts the next page without waiting for the
other calls. An OCR engine can send the calls to a Web Worker pool or to
concurrent HTTP requests. Results stay assigned to their source pages when
jobs finish out of order. Active calls can keep up to `numWorkers` rendered
page rasters in memory, so use a small value for high-DPI documents.

PDF parsing and rendering remain single-threaded in WASM. `numWorkers`
defaults to `1` and limits concurrent OCR calls. LiteParse does not create
Web Workers. To process OCR jobs in parallel, supply an engine that supports
concurrent jobs.

## Config options

All optional, camelCase:

| Option | Type | Default | Description |
|---|---|---|---|
| `ocrLanguage` | `string` | `"eng"` | Language code passed to the OCR engine |
| `ocrEnabled` | `boolean` | `false` | Run OCR on text-sparse pages. Off by default in WASM — there is no built-in engine, so this does nothing without `ocrEngine` |
| `ocrEngine` | `object` | — | Custom JS-side OCR engine (see above) |
| `numWorkers` | `number` | `1` | Maximum concurrent OCR calls |
| `ocrFailureFatal` | `boolean` | `true` | When `false`, OCR failures return partial results instead of throwing |
| `ocrHedgeDelaysMs` | `number[]` | `[]` | Request-hedging schedule for a remote `ocrEngine` |
| `maxPages` | `number` | `1000` | Stop after this many pages |
| `targetPages` | `string` | — | e.g. `"1-5,10,15-20"` |
| `dpi` | `number` | `150` | Render DPI for OCR |
| `outputFormat` | `"json" \| "text" \| "markdown"` | `"json"` | Shape of `result.text`. Also accepts `"md"`. Throws on any other value |
| `preserveVerySmallText` | `boolean` | `false` | Keep tiny text that's normally filtered |
| `skipDiagonalText` | `boolean` | `false` | Drop text more than 2° off the nearest right angle |
| `cropBox` | `{ top, right, bottom, left }` | — | Fraction to crop from each side of every page |
| `pageOrientationCorrections` | `{ page, angle }[]` | — | Counter-rotate listed pages (1-based) by the clockwise `angle` (0/90/180/270) their content appears rotated |
| `password` | `string` | — | Password for protected PDFs |
| `quiet` | `boolean` | `false` | Suppress progress logging |
| `imageMode` | `"off" \| "placeholder" \| "embed"` | `"placeholder"` | How image references appear in markdown. Also accepts `"none"` for `off` |
| `extractLinks` | `boolean` | `true` | Render `[text](url)` in markdown |
| `keepHeadersFooters` | `boolean` | `false` | Keep running header/footer chrome in markdown |
| `emitWordBoxes` | `boolean` | `false` | Per-word sub-boxes on each text item |

The [extraction options](/liteparse/guides/extraction/) — `extractImages`, `extractVectorGraphics`, `extractAnnotations`, `extractFormFields`, `extractStructureTree`, `extractContentBounds`, `extractXfaPackets`, `extractTextMetadata`, `includeComplexity`, and `renderFormFields` — are all available here too, with the same camelCase names and the same `false` defaults.

---

### Src Content Docs Liteparse Guides Complexity (`docs/src/content/docs/liteparse/guides/complexity.mdx`)

---
title: Document Complexity
description: Use is_complex to decide whether a document needs OCR or heavier parsing before you commit to it.
sidebar:
  order: 4
---

The `is_complex` command in LiteParse checks whether a document is "complex". This means looking for images, broken/garbled text, large amounts of vector graphics, and sparse text.

Use it to:

- **Route documents** to cheaper or more expensive parsing/OCR backends.
- **Reject or flag** documents you can't handle — e.g. when running with `--no-ocr`, find the pages that would come back empty.
- **Estimate cost** up front by counting how many pages actually need OCR.

## How it works

Complexity is computed **per page**. Each page gets a `needs_ocr` verdict and a list of `reasons` explaining why it was flagged. A document is "complex" if any of its pages needs OCR.

The reasons are derived from a few signals:

| Reason | Meaning |
|--------|---------|
| `scanned` | A single raster covers ~the whole page with little or no text behind it — a scanned/photographed page. |
| `no-text` | Almost no extractable text and no full-page image — a blank page, cover, or divider. |
| `sparse-text` | Some real text, but it covers very little of the page — typically a figure with a thin caption. |
| `embedded-images` | Substantial embedded raster figures sit alongside the native text. |
| `garbled` | The native text decodes to garbage and text is likely unreadable. |
| `vector-text` | Text is painted as filled vector outlines outside the text layer, so no native text items represent it. |
| `annotation-text` | Meaningful text lives in PDF annotations rather than the page content stream. |

> The set of reasons may grow over time as the router learns to recommend heavier pipelines. Treat `reasons` as an open-ended list and route on the values you care about rather than assuming it's exhaustive.

### Layout signals

Alongside the OCR verdict, each page carries a nested `layout` object describing **how hard the page is to lay out** — multi-column bodies, tables, and dense figures. These are computed after spatial projection, so they see the reconstructed region tree rather than raw text items.

Layout complexity is deliberately **separate from `needs_ocr`**. A crisp, fully-digital two-column paper needs no OCR at all but is layout-complex; layout reasons never make `needs_ocr` true. Route on whichever you actually care about.

| Reason | Meaning |
|--------|---------|
| `multi-column` | The page splits into more than one text column. |
| `table-likely` | Ruled or borderless table structure was detected on the page. |
| `dense-graphics` | Figures cover a large fraction of the page. |

| Field | Description |
|-------|-------------|
| `column_count` | Number of detected text columns. Always ≥ 1. |
| `ruled_table_count` | Number of tables detected from ruling lines. |
| `ruled_table_coverage` | Fraction of the page covered by ruled tables (0–1). |
| `text_table_run_count` | Number of borderless, text-aligned table runs detected. |
| `figure_count` | Number of detected figure regions. |
| `figure_coverage` | Fraction of the page covered by figures (0–1). |
| `is_complex` | Verdict for layout specifically. Equivalent to `reasons` being non-empty. |
| `reasons` | Every layout reason the page was flagged with (see above). |

## CLI

```bash
lit is-complex document.pdf
```

The command always prints per-page JSON to **stdout**, a human-readable verdict to **stderr**, and sets its **exit code** to reflect the result — so you can consume it however fits your workflow.

```jsonc
// stdout
[
  {
    "page_number": 1,
    "text_length": 0,
    "text_coverage": 0.0,
    "has_substantial_images": false,
    "image_block_count": 0,
    "image_coverage": 0.0,
    "largest_image_coverage": 0.0,
    "full_page_image": true,
    "uncovered_vector_area": null,
    "is_garbled": false,
    "page_area": 482400.0,
    "needs_ocr": true,
    "reasons": ["scanned"],
    "layout": {
      "column_count": 2,
      "ruled_table_count": 1,
      "ruled_table_coverage": 0.18,
      "text_table_run_count": 0,
      "figure_count": 3,
      "figure_coverage": 0.31,
      "is_complex": true,
      "reasons": ["multi-column", "table-likely", "dense-graphics"]
    }
  }
]
```

```
# stderr
COMPLEX — 1/1 page(s) need OCR
```

The exit code is **non-zero when any page needs OCR**, so the command works as a shell predicate:

```bash
# Only parse with --no-ocr when the document is simple
lit is-complex document.pdf --quiet && lit parse document.pdf --no-ocr
```

Pipe the JSON into `jq` to act on individual pages. The example below will list the page numbers that need OCR:

```bash
lit is-complex document.pdf --compact | jq '[.[] | select(.needs_ocr) | .page_number]'
```

### Options

| Flag | Description |
|------|-------------|
| `--compact` | Emit dense, whitespace-free JSON instead of pretty-printed. |
| `--max-pages <N>` | Maximum number of pages to check (default: 1000). |
| `--target-pages <SPEC>` | Check only specific pages, e.g. `"1-5,10,15-20"`. |
| `--password <PASS>` | Password for encrypted/protected documents. |
| `-q`, `--quiet` | Suppress the stderr logging output. |

### Inline with a parse

`lit is-complex` is a standalone pre-parse pass. If you're parsing the document anyway and just want the signals alongside the content, pass `--complexity` to `lit parse` instead — it adds a `complexity` object to each page of the JSON output, with the same fields described here.

```bash
lit parse document.pdf --format json --complexity
```

This is off by default: computing layout signals requires the full spatial projection, so it only runs when you ask for it.

> `lit is-complex` always populates `layout`. Within `lit parse`, the `complexity` object (and its `layout`) appears only when `--complexity` is set.

## Library

The same check is available programmatically. It returns one entry per page.

<Tabs>
<TabItem label="TypeScript" value="typescript">

```typescript
import { LiteParse } from "@llamaindex/liteparse";

const parser = new LiteParse({ ocrEnabled: false });
const pages = await parser.isComplex("document.pdf");

const complex = pages.some((p) => p.needsOcr);
if (complex) {
  // Route to a heavyweight pipeline, like LlamaParse
  ...
} else {
  // Cheap path — skip OCR entirely
  const result = await parser.parse("document.pdf");
}

// Inspect why specific pages were flagged
for (const page of pages.filter((p) => p.needsOcr)) {
  console.log(`Page ${page.pageNumber}: ${page.reasons.join(", ")}`);
}
```

</TabItem>
<TabItem label="Python" value="python">

```python
from liteparse import LiteParse

parser = LiteParse(ocr_enabled=False)
pages = parser.is_complex("document.pdf")

if any(p.needs_ocr for p in pages):
    # Route to a heavyweight pipeline, like LlamaParse
    ...
else:
    # Cheap path — skip OCR entirely
    result = parser.parse("document.pdf")

# Inspect why specific pages were flagged
for page in pages:
    if page.needs_ocr:
        print(f"Page {page.page_number}: {', '.join(page.reasons)}")
```

</TabItem>
<TabItem label="Browser (WASM)" value="wasm">

```javascript
import { LiteParse } from "@llamaindex/liteparse-wasm";

const parser = new LiteParse();
const pages = await parser.isComplex(pdfBytes); // Uint8Array

const complex = pages.some((p) => p.needsOcr);
```

</TabItem>
</Tabs>

Both the library and CLI accept raw bytes as well as file paths, so you can run the check on documents you've already loaded into memory.

## Per-page fields

Every entry includes the raw signals behind the verdict, so you can apply your own thresholds instead of relying solely on `needs_ocr`:

| Field | Description |
|-------|-------------|
| `page_number` | 1-indexed page number. |
| `needs_ocr` | Verdict: the page needs more than the cheap text-only path. Equivalent to `reasons` being non-empty. |
| `reasons` | Every reason the page was flagged (see the table above). Empty exactly when `needs_ocr` is false. |
| `text_length` | Length of usable native text (garbled/unmappable text excluded). |
| `text_coverage` | Fraction of the page area covered by native text (0–1). |
| `has_substantial_images` | Whether any counted inline raster figures are present. |
| `image_block_count` | Number of counted raster image objects (full-page backgrounds excluded). |
| `image_coverage` | Summed image-bbox area over page area, clamped to 1. |
| `largest_image_coverage` | Largest single counted image's area over page area, clamped to 1. |
| `full_page_image` | A single raster covers ≥90% of the page — the signal that tells a scan apart from a blank page. |
| `uncovered_vector_area` | Filled vector-outline area not covered by native text (pt²). `null`/`undefined` when a cheaper signal already decided the page. |
| `is_garbled` | Whether the native text decodes to garbage. |
| `page_area` | Page area in pt². |
| `layout` | Nested [layout signals](#layout-signals) for the page. Omitted when layout was not computed. |

## Next steps

- [OCR configuration](/liteparse/guides/ocr/): Set up the OCR backend you route complex documents to.
- [Library usage](/liteparse/guides/library-usage/): Full programmatic API for TypeScript and Python.
- [CLI reference](/liteparse/cli-reference/): Complete command and option reference.

---

### Src Content Docs Liteparse Guides Extraction (`docs/src/content/docs/liteparse/guides/extraction.mdx`)

---
title: Extraction Options
description: Opt-in flags that add images, vector graphics, annotations, form fields, structure trees, layout blocks, and typography metadata to LiteParse output.
sidebar:
  order: 3
---

By default, LiteParse returns text, geometry, and page metadata. Everything a PDF *can* carry beyond that (embedded images, vector art, annotations, form fields, tagged-structure trees, per-glyph typography) is **opt-in**, behind its own flag.

This keeps the default output shape small, stable, and cheap to produce. It also means that if you're looking for data you know is in the file and don't see it in the output, you're probably missing a flag.

> Each option costs parse time and output size. Turn on only what you consume.

## Quick reference

| Rust / Python / CLI | Node / WASM | Adds |
|---------------------|-------------|------|
| `extract_images` / `--extract-images` | `extractImages` | `images[]`, `image_error_count` |
| `extract_vector_graphics` / `--extract-vector-graphics` | `extractVectorGraphics` | `pages[].vector_graphics` |
| `extract_annotations` / `--extract-annotations` | `extractAnnotations` | `pages[].annotations` |
| `extract_form_fields` / `--extract-form-fields` | `extractFormFields` | `pages[].form_fields` |
| `extract_structure_tree` / `--extract-structure-tree` | `extractStructureTree` | `pages[].structure_tree` |
| `extract_blocks` / `--extract-blocks` | `extractBlocks` | `pages[].blocks` |
| `extract_content_bounds` / `--extract-content-bounds` | `extractContentBounds` | `pages[].content_bounds` |
| `extract_xfa_packets` / `--extract-xfa-packets` | `extractXfaPackets` | `xfa_packets[]` |
| `extract_text_metadata` / `--extract-text-metadata` | `extractTextMetadata` | extra keys on `text_items[]` |
| `include_complexity` / `--complexity` | `includeComplexity` | `pages[].complexity` |
| `emit_word_boxes` *(no CLI flag)* | `emitWordBoxes` | `TextItem.words` (bindings only) |

One toggle is **on** by default: `extract_links` (`--no-links` to disable). See the [Markdown guide](/liteparse/guides/markdown/).

## Enabling options

<Tabs>
<TabItem label="TypeScript" value="typescript">

```typescript
import { LiteParse } from "@llamaindex/liteparse";

const parser = new LiteParse({
  outputFormat: "json",
  extractImages: true,
  extractFormFields: true,
  extractAnnotations: true,
  extractTextMetadata: true,
});

const result = await parser.parse("form.pdf");
```

</TabItem>
<TabItem label="Python" value="python">

```python
from liteparse import LiteParse

parser = LiteParse(
    output_format="json",
    extract_images=True,
    extract_form_fields=True,
    extract_annotations=True,
    extract_text_metadata=True,
)

result = parser.parse("form.pdf")
```

</TabItem>
<TabItem label="Rust" value="rust">

```rust
use liteparse::{LiteParse, LiteParseConfig, OutputFormat};

let config = LiteParseConfig {
    output_format: OutputFormat::Json,
    extract_images: true,
    extract_form_fields: true,
    extract_annotations: true,
    extract_text_metadata: true,
    ..Default::default()
};

let result = LiteParse::new(config).parse("form.pdf").await?;
```

</TabItem>
<TabItem label="CLI" value="cli">

```bash
lit parse form.pdf --format json \
  --extract-images \
  --extract-form-fields \
  --extract-annotations \
  --extract-text-metadata
```

</TabItem>
</Tabs>

## Embedded images

`extract_images` decodes embedded raster images and returns their bytes and metadata in a document-level `images` array. Pair it with `image_output_dir` to write each image to disk instead of holding it in memory.

```bash
lit parse report.pdf --format json --extract-images --image-output-dir ./images
```

```json
{
  "images": [
    {
      "id": "p1_i0",
      "name": "image_p1_0.png",
      "path": "./images/image_p1_0.png",
      "page": 1,
      "bbox": { "x": 72.0, "y": 118.5, "width": 240.0, "height": 160.0 },
      "width": 800,
      "height": 533,
      "rotation": 0.0,
      "format": "png"
    }
  ],
  "image_error_count": 0
}
```

Notes:

- `image_output_dir` **requires** `extract_images` — setting it alone is a config error. `image_mode: "embed"` also implies extraction, for backwards compatibility.
- `duplicate_of` is set when the same image appears more than once in the document; the duplicate points at the canonical entry's `id` rather than re-storing the pixels.
- `image_error_count` counts images that failed to decode. It is omitted when zero.
- This is separate from `image_mode`, which only controls how image *references* are written into markdown. See the [Markdown guide](/liteparse/guides/markdown/).

## Vector graphics

`extract_vector_graphics` reports the vector drawing operations on each page, split into filled/stroked `shapes` and straight `lines`. Useful for finding table rules, underlines, dividers, and chart geometry.

```json
{
  "vector_graphics": {
    "shapes": [
      {
        "bbox": { "x": 72.0, "y": 400.0, "width": 180.0, "height": 90.0 },
        "stroke": true,
        "stroke_color": "#000000",
        "fill": false,
        "has_curve": true
      }
    ],
    "lines": [
      {
        "x1": 72.0, "y1": 512.0, "x2": 540.0, "y2": 512.0,
        "stroke": true,
        "stroke_width": 0.5,
        "stroke_color": "#cccccc",
        "fill": false
      }
    ]
  }
}
```

## Annotations

`extract_annotations` returns PDF annotations per page — comments, highlights, and link targets.

```json
{
  "annotations": [
    {
      "subtype": "Link",
      "rect": { "x": 72.0, "y": 300.0, "width": 120.0, "height": 12.0 },
      "uri": "https://example.com"
    },
    {
      "subtype": "Highlight",
      "contents": "check this figure",
      "title": "Reviewer 2",
      "created": "D:20260714093000Z",
      "quadpoint_rects": [
        { "x": 72.0, "y": 280.0, "width": 200.0, "height": 11.0 }
      ]
    }
  ]
}
```

Only `subtype` is always present; every other key is omitted when the annotation doesn't carry it.

## Form fields

`extract_form_fields` returns AcroForm widgets and their resolved values.

```json
{
  "form_fields": [
    {
      "id": "f0",
      "type": "text",
      "page": 1,
      "annotation_index": 0,
      "widget_index": 0,
      "field_flags": 0,
      "name": "applicant_name",
      "value": "Ada Lovelace",
      "rect": { "x": 150.0, "y": 640.0, "width": 220.0, "height": 18.0 }
    },
    {
      "id": "f1",
      "type": "checkbox",
      "page": 1,
      "annotation_index": 1,
      "widget_index": 0,
      "field_flags": 0,
      "name": "agree_terms",
      "checked": true
    }
  ]
}
```

> The widget type is serialized as `type` in JSON, and is `type` on the Node and Python objects. Radio groups and checkbox sets expose `control_count`/`control_index` to tie sibling widgets together, and choice fields expose `options`/`selected_options`.

For **XFA**-based forms (a different, XML-based form technology), use `extract_xfa_packets` instead, which returns the raw XFA packets at document level.

Related: `render_form_fields` draws filled-in field appearances into rendered rasters. It affects [screenshots](/liteparse/guides/visual-citations/) and OCR input pixels, not JSON keys.

## Structure tree

`extract_structure_tree` returns the tagged-PDF logical structure — the authoring-time document outline of paragraphs, headings, tables, and lists. Only present in PDFs that were actually tagged.

```json
{
  "structure_tree": {
    "roots": [
      {
        "type": "Document",
        "children": [
          {
            "type": "H1",
            "marked_content_ids": [0],
            "actual_text": "Quarterly Report",
            "children": []
          },
          {
            "type": "P",
            "marked_content_ids": [1, 2],
            "children": []
          }
        ]
      }
    ]
  }
}
```

`marked_content_ids` join back to the `mcid` field on text items (available with `extract_text_metadata`), letting you map reconstructed text to its authored role.

## Layout blocks

`extract_blocks` returns the page decomposed into blocks — headings, paragraphs, list items, tables, code, rules, and figures — each with the region of the page it came from. This is the same classification the Markdown renderer runs on.

```bash
lit parse document.pdf --format json --extract-blocks
```

Only the fields that apply to a block's `kind` are present:

```json
{
  "kind": "heading",
  "text": "Quarterly Report",
  "level": 1,
  "bbox": { "x": 72.0, "y": 114.88, "width": 284.0, "height": 21.0 }
}
```

| Field | Kinds | Meaning |
|-------|-------|---------|
| `kind` | all | `heading`, `paragraph`, `list_item`, `code`, `table`, `grid_fallback`, `rule`, `figure` |
| `bbox` | all | Region occupied, in the same coordinate space as `text_items` |
| `text` | `heading`, `paragraph`, `list_item` | Rendered text |
| `level` | `heading`, `list_item` | Heading level 1–6, or list nesting depth |
| `bold`, `italic` | `paragraph`, `list_item` | Set when the whole block is emphasized |
| `ordered`, `marker` | `list_item` | Whether the list is ordered, and the marker as it appeared (`138.`, `iii)`, `•`) |
| `lines`, `lang` | `code`, `grid_fallback` | Verbatim source lines, and a best-effort language hint |
| `header`, `rows` | `table` | Cells, each with its own `text` and `bbox` |
| `id`, `format` | `figure` | Matches the `img_{id}.{format}` target in Markdown output |

`bbox` is the union of every source line that fed the block, so a wrapped heading or multi-line paragraph reports the whole band it occupies — not just its first line.

### Tables

Table cells are objects, not bare strings: each carries its own `bbox`, so a cell maps back to the region it was read from.

```json
{
  "kind": "table",
  "bbox": { "x": 67.9, "y": 511.1, "width": 458.4, "height": 111.0 },
  "header": [
    { "text": "Title of each class", "bbox": { "x": 132.7, "y": 511.1, "width": 64.0, "height": 8.0 } },
    { "text": "Trading symbol(s)",   "bbox": { "x": 288.7, "y": 511.1, "width": 35.0, "height": 8.0 } }
  ],
  "rows": [
    [
      { "text": "Common Stock", "bbox": { "x": 67.9, "y": 522.4, "width": 193.0, "height": 9.5 } },
      { "text": "AAPL",         "bbox": { "x": 293.8, "y": 522.4, "width": 24.0, "height": 9.5 } }
    ]
  ]
}
```

For a ruled table, a cell's box is the drawn grid cell. For a borderless one, it is the extent of the spans the cell was built from. Cells that exist only to square off a ragged grid have no `bbox` — there is no ink behind them.

Getting a table into a dataframe is then a two-line transform:

<Tabs>
<TabItem label="Python" value="python">

```python
import pandas as pd
from liteparse import LiteParse

result = LiteParse(extract_blocks=True).parse("10-k.pdf")

for page in result.pages:
    for block in page.blocks or []:
        if block.kind == "table":
            df = pd.DataFrame(
                [[c.text for c in row] for row in block.rows],
                columns=[c.text for c in block.header or []],
            )
```

</TabItem>
<TabItem label="TypeScript" value="typescript">

```typescript
import { LiteParse } from "@llamaindex/liteparse";

const { pages } = await new LiteParse({ extractBlocks: true }).parse("10-k.pdf");

for (const page of pages) {
  for (const block of page.blocks ?? []) {
    if (block.kind !== "table") continue;
    const grid = [block.header ?? [], ...block.rows].map((r) => r.map((c) => c.text));
  }
}
```

</TabItem>
</Tabs>

## Text metadata

`extract_text_metadata` enriches every entry in `text_items[]` with typography detail. Without it, a text item carries only `text`, `x`, `y`, `width`, `height`, `font_name`, and `font_size`.

```json
{
  "text": "Quarterly Report",
  "x": 72.0, "y": 118.5, "width": 180.4, "height": 14.0,
  "rotation": 0.0,
  "font_name": "Helvetica-Bold",
  "font_size": 14.0,
  "font_height": 14.2,
  "font_ascent": 11.1,
  "font_descent": -3.1,
  "font_weight": 700,
  "text_width": 180.4,
  "font_is_buggy": false,
  "mcid": 0,
  "fill_color": "#111111"
}
```

`font_weight` and `fill_color` are the practical way to detect emphasis and headings yourself when you need more control than markdown output gives you. `font_is_buggy` flags fonts whose metrics LiteParse had to estimate — treat sizes from those items with suspicion.

## Content bounds

`extract_content_bounds` adds a `content_bounds` rect per page: the bounding box of actual content, ignoring the page's declared media box. Use it to crop whitespace or detect pages whose content sits far from the nominal page area.

```json
{ "content_bounds": { "x": 68.0, "y": 96.0, "width": 476.0, "height": 620.0 } }
```

## Word boxes

`emit_word_boxes` splits each text item into per-word sub-boxes (`TextItem.words`), for word-level bounding-box attribution.

This one is **library- and bindings-only**. Word boxes are deliberately excluded from `--format json`, because on a text-heavy document they dwarf the rest of the payload. There is no CLI flag; reach for it from Node, Python, Rust, or WASM.

<Tabs>
<TabItem label="TypeScript" value="typescript">

```typescript
const parser = new LiteParse({ emitWordBoxes: true });
const result = await parser.parse("report.pdf");

for (const item of result.pages[0].textItems) {
  for (const word of item.words ?? []) {
    console.log(word.text, word.x, word.y, word.width, word.height);
  }
}
```

</TabItem>
<TabItem label="Python" value="python">

```python
parser = LiteParse(emit_word_boxes=True)
result = parser.parse("report.pdf")

for item in result.pages[0].text_items:
    for word in item.words:
        print(word.text, word.x, word.y, word.width, word.height)
```

</TabItem>
</Tabs>

## Options with no CLI flag

A few config options are only reachable from the library and bindings:

| Option | Description |
|--------|-------------|
| `emit_word_boxes` | Per-word sub-boxes (see above). |
| `crop_box` | Restrict output to a sub-region of each page. A text item is kept only if it falls entirely inside. |
| `skip_diagonal_text` | Drop text rotated more than 2° off the nearest right angle — watermarks and diagonal stamps. |
| `page_orientation_corrections` | Counter-rotate specific pages before extraction, using angles from your own orientation classifier (see below). |
| `detect_screenshot_rects` | Populate `rects` on screenshot results. |
| `render_form_fields` | Draw form-field appearances into rendered pages. |
| `ocr_failure_fatal` | When `false`, a systemic OCR failure returns partial results instead of erroring. Defaults to `true`. |
| `ocr_hedge_delays_ms` | Request-hedging schedule for HTTP OCR servers. No effect on built-in Tesseract. |

`crop_box` takes the fraction to crop from each side. It is an object in Node and WASM, and a `(top, right, bottom, left)` tuple in Python:

<Tabs>
<TabItem label="TypeScript" value="typescript">

```typescript
// Drop the top 10% and bottom 5% of every page
const parser = new LiteParse({
  cropBox: { top: 0.1, right: 0.0, bottom: 0.05, left: 0.0 },
});
```

</TabItem>
<TabItem label="Python" value="python">

```python
# (top, right, bottom, left)
parser = LiteParse(crop_box=(0.1, 0.0, 0.05, 0.0))
```

</TabItem>
</Tabs>

### Correcting page orientation

LiteParse honours a page's `/Rotate` entry, but a scanned page whose *content* is
upside down or sideways carries no such hint: its text layer (or OCR raster) reads
bottom-up, so reading order and label/value pairs come out inverted. If you run an
orientation classifier on the rendered page, hand its verdict to
`page_orientation_corrections`. Each entry names a 1-based page and the clockwise
angle (`0`, `90`, `180` or `270`) by which that page's content appears rotated;
LiteParse counter-rotates the page before extraction, so text coordinates, reading
order, page dimensions, OCR rasters and screenshots all come out upright. Pages not
listed, or past the end of the document, are left unchanged.

<Tabs>
<TabItem label="TypeScript" value="typescript">

```typescript
// Page 3 was scanned upside down, page 7 sideways (content rotated 90° clockwise)
const parser = new LiteParse({
  pageOrientationCorrections: [
    { page: 3, angle: 180 },
    { page: 7, angle: 90 },
  ],
});
```

</TabItem>
<TabItem label="Python" value="python">

```python
# (page, angle) pairs
parser = LiteParse(page_orientation_corrections=[(3, 180), (7, 90)])
```

</TabItem>
</Tabs>

## Next steps

- [Document complexity](/liteparse/guides/complexity/): Route documents before you parse them.
- [Markdown output](/liteparse/guides/markdown/): Images, links, and header/footer handling in markdown.
- [Visual citations](/liteparse/guides/visual-citations/): Turn block and text-item boxes into highlights on the rendered page.
- [CLI reference](/liteparse/cli-reference/): Every flag, per command.
- [API reference](/liteparse/api/): Full `LiteParseConfig` field list.

---

### Src Content Docs Liteparse Guides Library Usage (`docs/src/content/docs/liteparse/guides/library-usage.mdx`)

---
title: Library Usage
description: Use LiteParse programmatically from TypeScript or Python.
sidebar:
  order: 1
---

LiteParse can be used as a library in your own code, not just from the CLI. There are native packages for both TypeScript and Python.


<Tabs>
<TabItem label="TypeScript" value="typescript">

## TypeScript / Node.js usage

Install as a project dependency:

```bash
npm install @llamaindex/liteparse
# or
pnpm add @llamaindex/liteparse
```

### Parsing a document

```typescript
import { LiteParse } from "@llamaindex/liteparse";

const parser = new LiteParse({ ocrEnabled: true });
const result = await parser.parse("document.pdf");

// Full document text with layout preserved
console.log(result.text);

// Per-page data
for (const page of result.pages) {
  console.log(`Page ${page.pageNum}: ${page.textItems.length} text items`);
}
```

### JSON output with bounding boxes

Text items include spatial coordinates (`x`, `y`, `width`, `height`) in PDF points:

```typescript
const parser = new LiteParse({ outputFormat: "json" });
const result = await parser.parse("document.pdf");

for (const page of result.pages) {
  for (const item of page.textItems) {
    console.log(`[${item.x}, ${item.y}] ${item.width}×${item.height} "${item.text}"`);
  }
}
```

### Markdown output

Render the document to Markdown — headings, tables, lists, images, and links. The rendered Markdown comes back on `result.text`:

```typescript
const parser = new LiteParse({
  outputFormat: "markdown",   // "json" | "text" | "markdown"
  imageMode: "placeholder",   // "placeholder" | "off" | "embed"
  extractLinks: true,         // render [text](url) link syntax (default: true)
});
const result = await parser.parse("document.pdf");
console.log(result.text); // rendered Markdown
```

### Configuration

Pass any config options to the constructor. You only need to specify what you want to override:

```typescript
const parser = new LiteParse({
  ocrEnabled: true,
  ocrServerUrl: "http://localhost:8828/ocr",
  ocrLanguage: "fra",
  dpi: 300,
  outputFormat: "json",
  targetPages: "1-10",
  password: "secret",
});
```

Images, vector graphics, annotations, form fields, structure trees, and per-item typography are all off by default and enabled with their own options. See [Extraction options](/liteparse/guides/extraction/).

### Buffer / Uint8Array input

You can pass raw bytes directly instead of a file path:

```typescript
import { readFile } from "fs/promises";

const parser = new LiteParse();

// From a file read
const pdfBytes = await readFile("document.pdf");
const result = await parser.parse(pdfBytes);

// From an HTTP response
const response = await fetch("https://example.com/document.pdf");
const buffer = Buffer.from(await response.arrayBuffer());
const result2 = await parser.parse(buffer);
```

### Screenshots

Generate page images as buffers — useful for sending to LLMs or saving to disk:

```typescript
const parser = new LiteParse();
const screenshots = await parser.screenshot("document.pdf");

for (const shot of screenshots) {
  console.log(`Page ${shot.pageNum}: ${shot.width}x${shot.height}`);
  // shot.imageBuffer contains the raw PNG data
}

// Screenshot specific pages
const shots = await parser.screenshot("document.pdf", [1, 2, 3]);
```

### Environment variables

| Variable | Description |
|----------|-------------|
| `TESSDATA_PREFIX` | Path to a directory containing Tesseract `.traineddata` files. For offline/air-gapped environments. Also available as the `tessdataPath` config option. |

</TabItem>
<TabItem label="Python" value="python">

## Python usage

Install the package from PyPI:

```bash
pip install liteparse
```

### Parsing a document

```python
from liteparse import LiteParse

parser = LiteParse()
result = parser.parse("document.pdf")

# Full document text
print(result.text)

# Per-page data
for page in result.pages:
    print(f"Page {page.page_num}: {len(page.text_items)} text items")
```

### Configuration

All options are passed to the constructor:

```python
parser = LiteParse(
    ocr_enabled=True,
    ocr_server_url="http://localhost:8828/ocr",
    ocr_language="fra",
    dpi=300,
    target_pages="1-5",
    password="secret",
)
result = parser.parse("document.pdf")
```

All options are keyword-only. Images, vector graphics, annotations, form fields, structure trees, and per-item typography are off by default and enabled with their own options — see [Extraction options](/liteparse/guides/extraction/).

### Markdown output

Render the document to Markdown — headings, tables, lists, images, and links. The rendered Markdown comes back on `result.text`:

```python
parser = LiteParse(
    output_format="markdown",   # "json" | "text" | "markdown"
    image_mode="placeholder",   # "placeholder" | "off" | "embed"
    extract_links=True,         # render [text](url) link syntax (default: True)
)
result = parser.parse("document.pdf")
print(result.text)  # rendered Markdown
```

### Parsing from bytes

If you already have file contents in memory (e.g. from a web upload), pass them directly to `parse()`:

```python
with open("document.pdf", "rb") as f:
    pdf_bytes = f.read()

result = parser.parse(pdf_bytes)
print(result.text)
```

### Screenshots

```python
screenshots = parser.screenshot("document.pdf", page_numbers=[1, 2, 3])
for s in screenshots:
    print(f"Page {s.page_num}: {s.width}x{s.height}")
    # s.image_bytes contains PNG data
```

### Environment variables

| Variable | Description |
|----------|-------------|
| `TESSDATA_PREFIX` | Path to a directory containing Tesseract `.traineddata` files. For offline/air-gapped environments. Also available as the `tessdata_path` constructor option. |
</TabItem>
<TabItem label="Rust" value="rust">

## Rust usage

Add `liteparse` to your `Cargo.toml`:

```toml
[dependencies]
liteparse = "2"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```

### Parsing a document

```rust
use liteparse::{LiteParse, LiteParseConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let parser = LiteParse::new(LiteParseConfig::default());
    let result = parser.parse("document.pdf").await?;

    // Full document text
    println!("{}", result.text);

    // Per-page data
    for page in &result.pages {
        println!("Page {}: {} text items", page.page_number, page.text_items.len());
    }
    Ok(())
}
```

### Parsing from bytes

Use `PdfInput::Bytes` when you have the PDF in memory:

```rust
use liteparse::{LiteParse, LiteParseConfig};
use liteparse::types::PdfInput;

let parser = LiteParse::new(LiteParseConfig::default());
let pdf_bytes = std::fs::read("document.pdf")?;
let result = parser.parse_input(PdfInput::Bytes(pdf_bytes)).await?;
```

### Configuration

Override only the fields you need:

```rust
use liteparse::{LiteParse, LiteParseConfig, OutputFormat};

let config = LiteParseConfig {
    ocr_enabled: true,
    ocr_language: "fra".to_string(),
    dpi: 300.0,
    target_pages: Some("1-10".to_string()),
    output_format: OutputFormat::Json,
    password: Some("secret".to_string()),
    ..Default::default()
};
let parser = LiteParse::new(config);
```

`LiteParseConfig` is a plain struct, so `..Default::default()` is the idiomatic way to set only what you need. Images, vector graphics, annotations, form fields, structure trees, and per-item typography are off by default — see [Extraction options](/liteparse/guides/extraction/).

### Markdown output

Render the document to Markdown — headings, tables, lists, images, and links. The rendered Markdown comes back on `result.text`:

```rust
use liteparse::config::{ImageMode, LiteParseConfig, OutputFormat};
use liteparse::LiteParse;

let config = LiteParseConfig {
    output_format: OutputFormat::Markdown,
    image_mode: ImageMode::Placeholder,
    extract_links: true,
    ..Default::default()
};
let result = LiteParse::new(config).parse("document.pdf").await?;
println!("{}", result.text); // rendered Markdown
```

### Screenshots

Generate page images as PNG bytes:

```rust
let parser = LiteParse::new(LiteParseConfig::default());
let screenshots = parser.screenshot("document.pdf", None).await?;

for shot in &screenshots {
    println!("Page {}: {}x{}", shot.page_num, shot.width, shot.height);
    // shot.image_bytes contains the raw PNG data
}

// Screenshot specific pages
let shots = parser.screenshot("document.pdf", Some(vec![1, 2, 3])).await?;
```

### Custom OCR engine

Implement the `OcrEngine` trait to plug in your own OCR backend:

```rust
use liteparse::LiteParse;
use liteparse::ocr::OcrEngine;
use std::sync::Arc;

let parser = LiteParse::new(Default::default())
    .with_ocr_engine(Arc::new(my_engine));
```

For native ONNX OCR, enable LiteParse's `oar-ocr` feature and pass local model
paths to `OarOcrEngine::from_models`. Enabling `oar-ocr-auto-download` also lets
the same constructor accept registered bare file names; `oar-ocr` downloads
them from ModelScope, verifies their SHA-256 digests, and caches them under
`$OAR_HOME` (default `~/.oar`). The OAR backend is intentionally Rust-only and
is not included in the Node.js, Python, or WASM binding binaries.

### Features

| Feature | Default | Description |
|---------|---------|-------------|
| `tesseract` | Yes | Built-in Tesseract OCR via `tesseract-rs`. Disable with `default-features = false` if you only use HTTP OCR or no OCR. |
| `oar-ocr` | No | Native ONNX OCR via `oar-ocr`, using local or in-memory models. Available through the non-WASM Rust API. |
| `oar-ocr-auto-download` | No | Enables verified download and caching of registered OAR model file names. |
| `oar-ocr-{cuda,tensorrt,directml,coreml,webgpu,openvino}` | No | Forwards the selected ONNX Runtime execution provider to `oar-ocr`. |

</TabItem>
<TabItem label="Browser (WASM)" value="wasm">

## Browser (WASM) usage

Install the WASM package:

```bash
npm install @llamaindex/liteparse-wasm
```

### Parsing a document

```typescript
import init, { LiteParse } from "@llamaindex/liteparse-wasm";

// Initialize the WASM module (required once before use)
await init();

const parser = new LiteParse({ ocrEnabled: false });

// Pass a Uint8Array (e.g. from fetch, File input, or drag-drop)
const bytes = new Uint8Array(await file.arrayBuffer());
const result = await parser.parse(bytes);

console.log(result.text);          // full document text
console.log(result.pages[0]);      // per-page items with bboxes
```

### Configuration

All options are optional and use camelCase:

```typescript
const parser = new LiteParse({
  ocrEnabled: true,
  ocrLanguage: "fra",
  dpi: 300,
  outputFormat: "json",
  targetPages: "1-10",
  maxPages: 100,
  password: "secret",
  quiet: false,
});
```

### Markdown output

The browser build supports Markdown output too — set `outputFormat: "markdown"` and read the rendered Markdown from `result.text`. See the [Markdown output guide](/liteparse/guides/markdown/) for the `imageMode` and `extractLinks` options.

```typescript
const parser = new LiteParse({ outputFormat: "markdown" });
const result = await parser.parse(bytes);
console.log(result.text); // rendered Markdown
```

### OCR in the browser

The native Tesseract and HTTP OCR backends are not available in the browser. To use OCR, provide a JS-side engine with a `recognize` method:

```typescript
const parser = new LiteParse({
  ocrEnabled: true,
  ocrLanguage: "eng",
  ocrEngine: {
    /**
     * @param imageData - PNG-encoded image bytes
     * @param width     - rendered page width in pixels
     * @param height    - rendered page height in pixels
     * @param language  - e.g. "eng"
     * @returns array of recognized text regions
     */
    async recognize(imageData, width, height, language) {
      // e.g. call tesseract.js in a worker, or a remote OCR service
      return [
        { text: "Hello", bbox: [10, 20, 80, 40], confidence: 0.98 },
      ];
    },
  },
});
```

### Notes

- Input must be a `Uint8Array` — file paths are not supported in the browser.
- The WASM module must be initialized with `await init()` before creating a `LiteParse` instance.

</TabItem>
</Tabs>

---

### Src Content Docs Liteparse Guides Markdown (`docs/src/content/docs/liteparse/guides/markdown.mdx`)

---
title: Markdown Output
description: Render documents to clean, structured Markdown for LLMs and RAG pipelines.
sidebar:
  order: 2
---

LiteParse can render documents directly to Markdown, reconstructing headings,
tables, lists, images, and links from the spatial layout. This is ideal for
feeding documents to LLMs and RAG pipelines, where clean, structured text
matters more than exact visual fidelity.

Markdown is a first-class output format alongside `text` and `json`.

## CLI

```bash
# Render a document to Markdown
lit parse document.pdf --format markdown -o output.md

# Print Markdown to stdout
lit parse document.pdf --format markdown
```

### Images

By default, raster images are emitted as Markdown placeholders
(``) in reading order. Control this with `--image-mode`:

| Mode | Behavior |
|------|----------|
| `placeholder` (default) | Emit `` references in reading order |
| `off` | Strip images entirely |
| `embed` | Emit the same references, and extract the image bytes |

`--image-mode` controls only how image *references* appear in the markdown. It does
not, on its own, decode any pixels. To actually get image bytes, use
`--extract-images` (see [Extraction options](/liteparse/guides/extraction/)):

```bash
# Strip images
lit parse document.pdf --format markdown --image-mode off

# Extract embedded images to disk and reference them from the markdown
lit parse document.pdf --format markdown --extract-images --image-output-dir ./images
```

`--image-output-dir` requires `--extract-images`. For backwards compatibility
`--image-mode embed` also turns extraction on, so the older
`--image-mode embed --image-output-dir ./images` form still works.

### Links

Hyperlink annotations are rendered as `[text](url)` by default. Pass
`--no-links` to emit the anchor text as plain text instead:

```bash
lit parse document.pdf --format markdown --no-links
```

### Headers and footers

Running headers and footers (page numbers, document titles repeated on every
page) are stripped from markdown output by default, since they interrupt the
prose when chunked for retrieval. Pass `--keep-headers-footers` to retain them:

```bash
lit parse document.pdf --format markdown --keep-headers-footers
```

## Library

The rendered Markdown is returned on `result.text`.

<Tabs>
<TabItem label="TypeScript" value="typescript">

```typescript
import { LiteParse } from "@llamaindex/liteparse";

const parser = new LiteParse({
  outputFormat: "markdown",   // "json" | "text" | "markdown"
  imageMode: "placeholder",   // "placeholder" | "off" | "embed" (default: "placeholder")
  extractLinks: true,         // render [text](url) link syntax (default: true)
});
const result = await parser.parse("document.pdf");
console.log(result.text); // rendered Markdown
```

</TabItem>
<TabItem label="Python" value="python">

```python
from liteparse import LiteParse

parser = LiteParse(
    output_format="markdown",   # "json" | "text" | "markdown"
    image_mode="placeholder",   # "placeholder" | "off" | "embed"
    extract_links=True,         # render [text](url) link syntax (default: True)
)
result = parser.parse("document.pdf")
print(result.text)  # rendered Markdown
```
</TabItem>
<TabItem label="Rust" value="rust">

```rust
use liteparse::config::{ImageMode, LiteParseConfig, OutputFormat};
use liteparse::LiteParse;

let config = LiteParseConfig {
    output_format: OutputFormat::Markdown,
    image_mode: ImageMode::Placeholder,
    extract_links: true,
    ..Default::default()
};
let result = LiteParse::new(config).parse("document.pdf").await?;
println!("{}", result.text); // rendered Markdown
```

</TabItem>
</Tabs>

## Quality notes

Markdown reconstruction quality varies with document complexity. LiteParse does
a strong job on typical documents, handling prose, headings, simple-to-moderate tables,
and lists. This runs entirely locally with no models using rule-based heuristics. For the hardest documents (dense or
multi-level tables, complex multi-column layouts, charts, and scans),
[LlamaParse](https://developers.llamaindex.ai/python/cloud/llamaparse/?utm_source=github&utm_medium=liteparse)
remains the most accurate option.

---

### Src Content Docs Liteparse Guides Multi Format (`docs/src/content/docs/liteparse/guides/multi-format.mdx`)

---
title: Multi-Format Support
description: Parse Word documents, spreadsheets, presentations, and images with LiteParse.
sidebar:
  order: 6
---

LiteParse automatically converts non-PDF formats to PDF before parsing. This lets you use the same parsing pipeline for Office documents, images, and more.

## Supported formats

### Office documents (via LibreOffice)

| Category | Extensions |
|----------|-----------|
| Word | `.doc`, `.docx`, `.docm`, `.odt`, `.rtf`, `.pages` |
| PowerPoint | `.ppt`, `.pptx`, `.pptm`, `.odp`, `.key` |
| Spreadsheets | `.xls`, `.xlsx`, `.xlsm`, `.ods`, `.csv`, `.tsv`, `.numbers` |

### Images (native support)

<Aside>
    As of [v2.8.0](https://github.com/run-llama/liteparse/releases/tag/crates-v2.8.0), LiteParse no longer requires `imagemagick` for image to PDF conversion,
    which is now natively handled by the rust code.
</Aside>

`.jpg`, `.jpeg`, `.png`, `.gif`, `.bmp`, `.tiff`, `.webp`, `.svg`

Images are converted to PDF and then parsed with OCR to extract text.

## Installing dependencies

Format conversion uses standard system tools. Install the ones you need:

### LibreOffice (for Office documents)

```bash
# macOS
brew install --cask libreoffice

# Ubuntu/Debian
apt-get install libreoffice

# Windows
choco install libreoffice-fresh
```

> On Windows, you may need to add the LibreOffice CLI directory (typically `C:\Program Files\LibreOffice\program`) to your PATH and restart.

## Usage

Once the dependencies are installed, just pass any supported file to `lit parse`:

```bash
lit parse report.docx
lit parse slides.pptx --format json
lit parse spreadsheet.xlsx -o output.txt
lit parse scan.png
```

Batch mode also handles mixed formats:

```bash
lit batch-parse ./documents ./output --recursive
```

## How it works

1. LiteParse detects the file extension
2. If it's not a PDF, it converts to PDF — office documents via LibreOffice, images and SVGs natively in Rust
3. The resulting PDF is parsed normally
4. Temporary conversion files are cleaned up automatically

Only office formats need an external tool (LibreOffice). If it isn't installed, LiteParse returns an error explaining which dependency is needed.

---

### Src Content Docs Liteparse Guides Ocr (`docs/src/content/docs/liteparse/guides/ocr.md`)

---
title: OCR Configuration
description: Configure OCR in LiteParse — built-in Tesseract, or bring your own via HTTP servers.
sidebar:
  order: 5
---

LiteParse uses OCR selectively — only on embedded images or pages where native text extraction didn't find text. This keeps parsing fast while still capturing text from scanned pages and embedded images.

## Built-in Tesseract (default)

Tesseract is bundled with LiteParse and works out of the box. Just run:

```bash
lit parse document.pdf
```

### Language support

Specify the OCR language for better accuracy on non-English documents:

```bash
lit parse document.pdf --ocr-language fra    # French
lit parse document.pdf --ocr-language deu    # German
lit parse document.pdf --ocr-language jpn    # Japanese
```

Tesseract uses [ISO 639-3](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html) language codes (`eng`, `fra`, `deu`, etc.).

### Offline / air-gapped environments

For environments without internet access, point Tesseract at a local directory containing pre-downloaded `.traineddata` files:

```bash
# Via environment variable
export TESSDATA_PREFIX=/path/to/tessdata
lit parse document.pdf --ocr-language eng

# Or via CLI flag
lit parse document.pdf --tessdata-path /path/to/tessdata
```

The `tessdata_path` / `tessdataPath` option is also available in the library APIs.

### Troubleshooting: missing language data

If Tesseract can't find its language data, you'll see an error like:

```
Error opening data file tessdata/eng.traineddata
Please make sure the TESSDATA_PREFIX environment variable is set to your "tessdata" directory.
Failed loading language 'eng'
```

The bundled Tesseract still needs the `.traineddata` file for your language. It is normally downloaded and cached automatically on first use (under `~/.tesseract-rs/tessdata` on Linux, `~/Library/Application Support/tesseract-rs/tessdata` on macOS); if that download didn't happen — e.g. offline, restricted network, or a sandboxed install — OCR cannot run.

To resolve it, do any one of the following:

- Download the language file (e.g. [`eng.traineddata`](https://github.com/tesseract-ocr/tessdata)) and point LiteParse at it with `TESSDATA_PREFIX` or `--tessdata-path` (see [above](#offline--air-gapped-environments)).
- Use an [HTTP OCR server](#http-ocr-servers) instead of the built-in engine.
- Disable OCR with `--no-ocr` if you don't need it.

When language data is missing for **every** page, LiteParse now fails with a clear `OCR failed for all N page(s)` error instead of returning a document with silently-empty OCR text — so an unresolved setup surfaces immediately rather than producing partial results that look complete.

### Disabling OCR

If you don't need OCR (pure native-text PDFs, or you don't care about images), disable it for faster parsing:

```bash
lit parse document.pdf --no-ocr
```

## HTTP OCR servers

For higher accuracy or GPU-accelerated OCR, you can point LiteParse at an HTTP OCR server. LiteParse ships with ready-to-use examples for popular OCR engines.

### EasyOCR

```bash
# Start the EasyOCR server (requires Python)
git clone https://github.com/run-llama/liteparse.git
cd liteparse/ocr/easyocr
pip install -r requirements.txt
python server.py

# Parse with EasyOCR in another terminal
lit parse document.pdf --ocr-server-url http://localhost:8828/ocr
```

### PaddleOCR

```bash
# Start the PaddleOCR server (requires Python)
git clone https://github.com/run-llama/liteparse.git
cd liteparse/ocr/paddleocr
pip install -r requirements.txt
python server.py

# Parse with PaddleOCR in another terminal
lit parse document.pdf --ocr-server-url http://localhost:8828/ocr
```

### Authenticated servers

Pass extra HTTP headers on every OCR request with `--ocr-server-header`, repeating the flag for each header:

```bash
lit parse document.pdf \
  --ocr-server-url https://ocr.internal/ocr \
  --ocr-server-header "Authorization: Bearer $TOKEN" \
  --ocr-server-header "X-Tenant: acme"
```

In the libraries this is `ocr_server_headers` / `ocrServerHeaders`, taking a map of header name to value. It has no effect unless an OCR server URL is set.

### Parallel OCR workers

LiteParse OCRs multiple pages in parallel. By default, it uses one fewer worker than your CPU core count. Override this with:

```bash
lit parse document.pdf --num-workers 8
```

This is useful if you need to slow down OCR requests to an external server or if your OCR engine is GPU-accelerated and can handle more concurrency.

## Custom OCR servers

You can integrate any OCR engine by implementing the LiteParse OCR API. Your server needs a single endpoint:

```
POST /ocr
Content-Type: multipart/form-data
```

**Request fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file` | binary | Yes | Image file (PNG, JPG, etc.) |
| `language` | string | No | ISO 639-1 language code (default: `en`) |

> **Watch the language code.** LiteParse forwards whatever `--ocr-language` is set to, which defaults to the Tesseract-style ISO 639-3 code `eng` — not `en`. Your server should accept both. The bundled EasyOCR and PaddleOCR servers do this by mapping `eng` → `en` before dispatch; do the same in a custom server, or pass `--ocr-language en` explicitly.

**Response format:**

```json
{
  "results": [
    {
      "text": "recognized text",
      "bbox": [12, 40, 220, 62],
      "confidence": 0.95
    }
  ]
}
```

Each result contains:

| Field | Type | Description |
|-------|------|-------------|
| `text` | string | Recognized text |
| `bbox` | `[x1, y1, x2, y2]` | Bounding box in pixels. Origin is top-left, x goes right, y goes down |
| `confidence` | number | Score from 0.0 to 1.0 |

### Testing your server

```bash
# Quick test with curl
curl -X POST http://localhost:8828/ocr \
  -F "file=@test.png" \
  -F "language=en" | jq .

# Use with LiteParse
lit parse document.pdf --ocr-server-url http://localhost:8828/ocr
```

### Common Gotchas

- Return `{"results": []}` if no text is detected
- Bounding boxes must be axis-aligned (`[x1, y1, x2, y2]` where top-left to bottom-right)
- If your engine returns rotated boxes, convert to axis-aligned by taking min/max coordinates
- If your engine doesn't provide confidence scores, return `1.0`
- Results should be in reading order (top-to-bottom, left-to-right)
- Cache OCR models in memory rather than reloading per request

## OCR in the browser (WASM)

The built-in Tesseract and HTTP OCR backends are not available in the WASM build. Instead, you can pass a custom `ocrEngine` object with a `recognize` method. See the [browser usage guide](/liteparse/guides/browser-usage/) for details.

### A note on OCR approaches

These days, its common to apply the term "OCR" to both traditional approaches and newer LLM-based document understanding models.

The LiteParse OCR API is designed specifically for approaches that return text with bounding boxes.

If you are trying to integrate a method that doesn't return bounding boxes, you will have to generate dummy bounding boxes.

---

### Src Content Docs Liteparse Guides Parsing Urls (`docs/src/content/docs/liteparse/guides/parsing-urls.md`)

---
title: Parsing URLs
description: Parse remote documents by reading URLs.
sidebar:
  order: 8
---

To parse remote files, LiteParse supports both CLI and library usage for reading bytes and streams. The CLI can download them with any tool you like and pipe the bytes to `lit parse` using `-` as the file argument, while the libraries can fetch the bytes directly and pass them to the parser.

## CLI usage

```bash
# Parse a remote PDF
curl -sL https://example.com/report.pdf | lit parse -

# With options
curl -sL https://example.com/report.pdf | lit parse --no-ocr --format json -

# Save to a file
curl -sL https://example.com/report.pdf | lit parse -o report.txt -
```

The `-` argument tells LiteParse to read from stdin instead of a file path. Any tool that writes to stdout works — `curl`, `wget`, `aws s3 cp - -`, etc.

## Library usage

The TypeScript library accepts `Buffer`/`Uint8Array` directly, so you can handle the download however you like.

For example, using `fetch` in a Node.js environment:

```typescript
import { LiteParse } from "@llamaindex/liteparse";

const response = await fetch("https://example.com/report.pdf");
const buffer = Buffer.from(await response.arrayBuffer());

const parser = new LiteParse({ ocrEnabled: false });
const result = await parser.parse(buffer);
console.log(result.text);
```

---

### Src Content Docs Liteparse Guides Server Usage (`docs/src/content/docs/liteparse/guides/server-usage.md`)

---
title: Server Usage
description: Run LiteParse as an HTTP service with liteparse-server.
sidebar:
  order: 10
---

[`liteparse-server`](https://github.com/run-llama/liteparse-server) is an Express server that exposes [`@llamaindex/liteparse`](https://www.npmjs.com/package/@llamaindex/liteparse) as an HTTP parsing backend. It lets you run LiteParse as a standalone service, which is useful when your application isn't Node.js, when you want to share one parsing instance across multiple clients, or when you need a parsing endpoint behind your own infrastructure.

The server ships in two flavours:

- **Slim** — a minimal build with no external dependencies, ideal for getting started or embedding in your own stack.
- **Full** — an all-in-one setup with built-in **Redis caching and rate limiting**, **OpenTelemetry tracing** (Jaeger), and **metrics** (Prometheus + Grafana), wired together with Docker Compose.

Both expose the same two endpoints: `POST /parse` and `POST /screenshots`.

## Requirements

- [Bun](https://bun.sh) ≥ 1.0 (or Node.js)
- [Docker](https://docs.docker.com/get-docker/) and Docker Compose (only required for the full server setup)

## Slim server

[`src/slim.ts`](https://github.com/run-llama/liteparse-server/tree/main/src/slim.ts) is a minimal version of the server that removes caching and observability while keeping rate limiting and logging. It needs no Redis, no OpenTelemetry collector, and no supporting services.

### Running locally

After cloning the GitHub repository:

```bash
git clone https://github.com/run-llama/liteparse-server
cd liteparse-server
```

You can run the API server with either Bun or Node:

```bash
bun run start-slim:bun
# or with Node
npm run start-slim:node
```

The server listens on **port 5000**.

### Running with Docker

**Pre-built image**

You can pull the pre-built LiteParse (slim) server image from the GitHub Container Registry:

```bash
docker pull ghcr.io/run-llama/liteparse-server:main
```

You can then run it exposing port 5000:

```bash
docker run -p 5000:5000 ghcr.io/run-llama/liteparse-server:main
```

**Build locally**

If you clone the repository, the provided [`slim.Dockerfile`](https://github.com/run-llama/liteparse-server/tree/main/slim.Dockerfile) produces a self-contained image:

```bash
# Build the image
docker build -f slim.Dockerfile -t liteparse-server-slim .

# Run exposing port 5000
docker run -p 5000:5000 liteparse-server-slim
```

The API is then available at **http://localhost:5000**.

## Full server

For a production-style deployment with caching, rate limiting, distributed tracing, and metrics, follow the Docker Compose example in [`examples/docker-compose`](https://github.com/run-llama/liteparse-server/tree/main/examples/docker-compose). It brings up the server alongside Redis, the OpenTelemetry Collector, Jaeger, Prometheus, and Grafana.

## API specification

Base URL: `http://localhost:5000`

### `POST /parse` — parse a single file

Parses a single document and returns either structured page data or plain text.

**Form fields:**

| Field    | Type   | Required | Description                               |
| -------- | ------ | -------- | ----------------------------------------- |
| `file`   | file   | Yes      | The document to parse                     |
| `config` | string | No       | JSON-serialized `LiteParseConfig` options |

**Query parameters:**

| Parameter | Type    | Default | Description                                                                        |
| --------- | ------- | ------- | ---------------------------------------------------------------------------------- |
| `text`    | boolean | `false` | If `true`, returns `text/plain`; otherwise `application/json` with a `pages` array |

**Responses:**

- `200 text/plain` — extracted text (when `text=true`)
- `200 application/json` — `{ "pages": [...] }` (when `text=false`)
- `400` — missing `file`
- `429` — rate limit exceeded (only if rate limiting is configured)

### `POST /screenshots` — screenshot pages of a document

Renders document pages as PNG images and streams them back as newline-delimited JSON (NDJSON).

**Form fields:**

| Field    | Type   | Required | Description                               |
| -------- | ------ | -------- | ----------------------------------------- |
| `file`   | file   | Yes      | The document to screenshot                |
| `config` | string | No       | JSON-serialized `LiteParseConfig` options |

**Query parameters:**

| Parameter | Type   | Default | Description                                                       |
| --------- | ------ | ------- | ----------------------------------------------------------------- |
| `pages`   | string | all     | Comma-separated 1-based page numbers to screenshot (e.g. `1,2,3`) |

**Response `200 application/x-ndjson`** — one JSON object per line:

```json
{
  "index": 0,
  "mimetype": "image/png",
  "data": "<base64>",
  "page_number": 1,
  "height": 1056,
  "width": 816
}
```

## Example usage with `curl`

Parse a file and get JSON pages:

```bash
curl -X POST http://localhost:5000/parse \
  -F "file=@path/to/document.pdf"
```

Parse a file and get plain text:

```bash
curl -X POST "http://localhost:5000/parse?text=true" \
  -F "file=@path/to/document.pdf"
```

Pass a `LiteParseConfig` (e.g. enable OCR):

```bash
curl -X POST http://localhost:5000/parse \
  -F "file=@path/to/document.pdf" \
  -F 'config={"ocrEnabled":true}'
```

Screenshot specific pages and stream NDJSON:

```bash
curl -X POST "http://localhost:5000/screenshots?pages=1,2,3" \
  -F "file=@path/to/document.pdf"
```

---

### Src Content Docs Liteparse Guides Visual Citations (`docs/src/content/docs/liteparse/guides/visual-citations.mdx`)

---
title: Visual Citations with Bounding Boxes
description: Use bounding boxes and screenshots to show exactly where information was found in a document.
sidebar:
  order: 7
---

When building agents or RAG workflows, it is often not enough to parse text and call it done. Frequently, users and applications will require you to show _where_ that text came from.

LiteParse gives you spatial coordinates for every text item, plus page screenshots, so you can highlight exact regions on the rendered page.

## How bounding boxes work

When you parse a document with JSON output, each page includes a list of text items — every extracted text element with its position (`x`, `y`, `width`, `height`) and content.

```bash
lit parse document.pdf --format json
```

```json
{
  "pages": [
    {
      "page": 1,
      "width": 612,
      "height": 792,
      "text": "Revenue grew 15% ...",
      "text_items": [
        {
          "text": "Revenue grew 15%",
          "x": 72,
          "y": 200,
          "width": 150,
          "height": 12
        }
      ]
    }
  ]
}
```

Coordinates are in **PDF points** (1 point = 1/72 inch). Origin is the top-left corner of the page, with X increasing right and Y increasing down.

> JSON output is snake_case (`text_items`, `page`). The Node bindings use camelCase (`textItems`, `pageNum`) and Python uses snake_case (`text_items`, `page_num`). The examples below use each language's own naming.

## Library usage

The library lets you parse for bboxes and generate screenshots in a single script. For example, searching for "0°C to 70°C" and showing where it appears:

<Tabs>
<TabItem label="TypeScript" value="typescript">
### TypeScript

```typescript
import { LiteParse, searchItems } from "@llamaindex/liteparse";

const parser = new LiteParse({ outputFormat: "json" });
const result = await parser.parse("report.pdf");

for (const page of result.pages) {
  const matches = searchItems(page.textItems, { phrase: "0°C to 70°C" });
  for (const match of matches) {
    console.log(`Found "${match.text}" at (${match.x}, ${match.y}) ${match.width}×${match.height}`);
  }
}
```
</TabItem>

<TabItem label="Python" value="python">

### Python

```python
from liteparse import LiteParse, search_items

parser = LiteParse()
result = parser.parse("report.pdf")

for page in result.pages:
    matches = search_items(page.text_items, "0°C to 70°C")
    for match in matches:
        print(f'Found "{match.text}" at ({match.x}, {match.y}) {match.width}×{match.height}')
```
</TabItem>
</Tabs>

## Converting coordinates to image pixels

Text item coordinates are in PDF points, but screenshots are in pixels. To draw highlights on a screenshot, you need to scale the coordinates:

```typescript
const scaleFactor = dpi / 72; // PDF points -> pixels at your chosen DPI

function itemToPixels(item, dpi = 150) {
  const scale = dpi / 72;
  return {
    x: item.x * scale,
    y: item.y * scale,
    width: item.width * scale,
    height: item.height * scale,
  };
}
```

For example, at the default 150 DPI the scale factor is `150 / 72 ~ 2.08`, so a text item at `(72, 200)` maps to pixel `(150, 416)`.

## Full example: highlighting citations with sharp

Here's a complete workflow that parses a PDF, searches for a phrase, and draws yellow highlight boxes on the page screenshot:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Running this script on a PDF will produce new images with the search phrase highlighted, showing exactly where the information was found on the page.



## CLI usage

Parse to JSON to get bounding boxes:

```bash
lit parse document.pdf --format json -o result.json
```

Generate page screenshots alongside:

```bash
lit screenshot document.pdf -o ./screenshots
```

From there, you (or an agent) can process the resulting JSON and screenshots as needed using any tools available.

## Tips

- Use the same `dpi` value for both `parse()` and `screenshot()`. The default is `150` for both.
- Page `width` and `height` in the JSON are in PDF points, matching the coordinate space. Use these if you need to normalize coordinates to percentages.
- For multi-word phrase search, iterate `textItems` and check adjacent items — a phrase may span multiple text items.
- To highlight a whole heading, paragraph, or table rather than individual words, parse with [`extract_blocks`](/liteparse/guides/extraction/#layout-blocks). Each block carries one `bbox` covering every line that fed it, so you can outline a region without stitching text items together yourself — and table cells carry their own boxes for cell-level citations.

---

--- METRICS ---
- Files Extracted: 24
- Estimated Token Budget: ~40460 tokens
- Canonical Reference: https://codewiki.google/github.com/run-llama/liteparse
