Technical Documentation: run-llama/liteparse
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
LiteParse
|
|
|
|
|
|
Docs
English | 简体中文
Looking for LiteParse V1? Follow this link to the old code
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,
our cloud-based document parser built for production document pipelines. LlamaParse handles the
hard stuff so your models see clean, structured data and markdown.
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), 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 (2,049 docs) | Overall (mean of 5 categories) | 0.364 | 0.380 | 0.389 | pdf-inspector 0.283 |
| opendataloader-bench (200 docs) | Overall (NID + TEDS + MHS) | 0.886 | 0.896 | 0.901 | opendataloader 0.842 |
| olmOCR-bench (1,403 pages) | % tests passed | 39.6 | 41.1 | 42.2 | pdf-inspector 33.7 |
ParseBench
Rule-based scoring, no LLM judge. Each column is ParseBench's 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.
opendataloader-bench
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.
olmOCR-bench
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.
How the runs are configured
- 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 defaultlit 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 byocr/rapidocr, which runs the PaddleOCR
models on ONNX Runtime — the same models asocr/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).
Reproducing the benchmarks
./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 # everythingResults 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 |
| Python | pip install liteparse |
Python README |
| Rust | cargo install liteparse (CLI) / cargo add liteparse (lib) |
Rust README (crates.io) |
| Browser (WASM) | npm i @llamaindex/liteparse-wasm |
WASM README |
Agent Skill
You can use liteparse as an agent skill, downloading it with the skills CLI tool:
npx skills add run-llama/llamaparse-agent-skills --skill liteparseOr copy-pasting the SKILL.md file to your own skills setup.
See the Agent Skill guide for requirements and usage patterns.
CLI Usage
The CLI is the same across all installations (npm, pip, cargo install).
Parse Files
# 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.
# 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-blocksImage 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
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/Pythonextract_vector_graphics = true, or JavaScript/WASMextractVectorGraphics: 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 containroots: [].
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/WASMextractBlocks) 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 ofheading,paragraph,list_item,code,table,grid_fallback,rule,figure.bbox: the region it occupies, in the same top-left 72-DPI viewport space astext_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:
textandlevelfor
headings;ordered/markerfor list items;linesandlangfor code;headerandrowsfor tables;id/formatfor 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.
{
"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/WASMextractDocumentMetadata) 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/Pythonextract_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/WASMextractXfaPackets) 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.
# 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:
lit batch-parse ./input-directory ./output-directoryGenerate Screenshots
Screenshots are essential for LLM agents to extract visual information that text alone cannot capture.
# 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 ./screenshotsCLI 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 helpScreenshot 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 helpIs-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 helpPrints 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:
lit parse document.pdf # OCR enabled by default
lit parse document.pdf --ocr-language fra # Specify language
lit parse document.pdf --no-ocr # Disable OCRFor offline or air-gapped environments, set TESSDATA_PREFIX to a directory containing pre-downloaded .traineddata files:
export TESSDATA_PREFIX=/path/to/tessdata
lit parse document.pdf --ocr-language engOr pass the path directly:
lit parse document.pdf --tessdata-path /path/to/tessdataOptional: 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:
You can integrate any OCR service by implementing the simple LiteParse OCR API specification (see OCR_API_SPEC.md).
The API requires:
- POST
/ocrendpoint - Accepts
fileandlanguageparameters - 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:
# macOS
brew install --cask libreoffice
# Ubuntu/Debian
apt-get install libreoffice
# Windows
choco install libreoffice-freshOn 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,
imagemagickis 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 packageBuilding
# 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 buildWe 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 - PDF rendering and text extraction
- Tesseract - OCR engine (via tesseract-rs)
- EasyOCR - HTTP OCR server (optional)
- PaddleOCR - HTTP OCR server (optional)
- napi-rs - Node.js native bindings
- PyO3 - Python native bindings
- 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+.
# 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:
hf download run-llama/liteparse-eval-dataset --repo-type dataset --local-dir ./liteparse-eval-datasetCLI Tools
`lp-process` — Generate Ground Truth Datasets
Processes PDF and image files using Claude's vision capabilities to generate structured QA ground truth data.
lp-process /path/to/documents --output-dir ./ground_truthOptions:
--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 setANTHROPIC_API_KEYenv 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.
lp-evaluate \
--data-dir ./documents \
--ground-truth-dir ./ground_truth \
--parse-provider liteparse \
--output ./results/run1Options:
--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.
lp-benchmark ./documents --providers pymupdf liteparse --warmup-runs 0Takes 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; use0for 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 | Spatial text extraction with OCR support |
pymupdf |
PyMuPDF | Fast, mature PDF library |
pypdf |
pypdf | Pure-Python PDF library |
markitdown |
MarkItDown | Microsoft's document-to-markdown converter |
Evaluation Pipeline
- Extract text from PDF using the selected parser provider
- Answer questions — LLM reads the extracted text and answers ground truth questions
- Judge answers — A separate LLM judge evaluates whether predicted answers are semantically equivalent to expected answers
- 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
# 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.pyThen use with LiteParse:
# 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 filelanguage- Language code (e.g., 'en', 'zh', 'ja')
Response:
{
"results": [
{
"text": "recognized text",
"bbox": [x1, y1, x2, y2],
"confidence": 0.95
}
]
}Creating a Custom OCR Server
To implement your own OCR server:
Create a Flask/FastAPI/Express server
Accept
POST /ocrwith multipart form dataReturn JSON with
resultsarray containing:text- Recognized text stringbbox- Bounding box as[x1, y1, x2, y2]confidence- Confidence score (0-1)
(Optional) Implement
GET /healthendpoint
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
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, Python, and the browser (WASM). See the project README for all options.
Installation
Add to your Cargo.toml:
[dependencies]
liteparse = "2"Or install the CLI:
cargo install liteparseQuick Start
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
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 isNone 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. Setoutput_format: OutputFormat::Markdown; the rendered Markdown is returned onresult.text. Two related knobs control Markdown rendering:
image_mode(ImageMode::Placeholderdefault |Off|Embed) — how raster
images are surfaced in the output.extract_images(defaultfalse) — 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;
requiresextract_images: true. Duplicate image resources reuse the same file.extract_links(defaulttrue) — render hyperlink annotations as[text](url); setfalsefor plain anchor text.
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 MarkdownReconstruction quality varies with document complexity.
Parsing from Bytes
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 aPageComplexityStats 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.
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:
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:
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):
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 useppocrv6_dict.txt; a mismatched dictionary silently produces garbled text:
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 returnliteparse::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 viatesseract-rs. Disable withdefault-features = falseif you don't need OCR or want to use an HTTP OCR server instead.oar-ocr— Optional native ONNX backend viaoar-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 throughoar-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 tooar-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:
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.pdfSee 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
# install and run (in one command)
uv run server.pyUsage
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
curl -X POST -F "[email protected]" -F "language=en" http://localhost:8828/ocrResponse Format
{
"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- Englishfr- Frenchde- Germanes- Spanishzh- Chineseja- Japaneseko- Koreanar- Arabic
Full list: https://www.jaided.ai/easyocr/
Use with LiteParse
Once the server is running, use it with LiteParse OSS:
# 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 zhOr in code:
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:
uv run pytest test_server.pyOverview (`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
# install and run (in one command)
uv run server.pyUsage
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
curl -X POST -F "[email protected]" -F "language=zh" http://localhost:8829/ocrResponse Format
{
"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- Englishzh/zh-cn- Chinese (Simplified)zh-tw/zh-hant- Chinese (Traditional)ja- Japaneseko- Koreanfr- Frenchde- Germanes- Spanishpt- Portugueseru- Russianar- Arabichi- 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:
# 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 zhOr in code:
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:
uv run pytest test_server.pyGPU 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 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
# install and run (in one command)
uv run server.pyThe 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-serverbinary so Surya'sllamacppbackend 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 putllama-serveron
yourPATH(or setLLAMA_CPP_BINARY=/path/to/llama-server).
- macOS:
- GPU (vllm): set
SURYA_INFERENCE_BACKEND=vllm(requires a CUDA GPU). - External server: point
SURYA_INFERENCE_URLat 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-capablellama-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.
# 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 allexposes the host GPUs; all model layers are offloaded
(LLAMA_CPP_NGL=99).- The
-v …:/modelsmount caches weights (HF_HOME=/models). The firstPOST /ocrdownloads the GGUF weights from Hugging Face and spawnsllama-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, setSURYA_INFERENCE_BACKEND=vllm (see the backend section above).
Verify it is up:
curl http://localhost:8830/health
curl -X POST -F "[email protected]" http://localhost:8830/ocrUsage
The service exposes:
POST /ocr— Perform OCR on an uploaded imageGET /health— Health check
Parameters
file— Image file (multipart/form-data)language— Language code (accepted for API compatibility; ignored, since
Surya 2 is multilingual)
Example
curl -X POST -F "[email protected]" http://localhost:8830/ocrResponse Format
{
"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_NGLcontrols how many layers of the VLM (the model that reads
text) thellamacppbackend offloads to the GPU.99offloads everything;0keeps it on CPU. The Docker image sets99.TORCH_DEVICEcontrols the device for Surya's helper torch models
(e.g. layout). Surya auto-detects, or you can force it:bashTORCH_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
lit parse document.pdf --ocr-server-url http://localhost:8830/ocrOr in code:
import { LiteParse } from 'liteparse';
const parser = new LiteParse({
ocrServerUrl: 'http://localhost:8830/ocr',
});
const result = await parser.parse('document.pdf');Testing
uv run pytest test_server.pyTests 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 — fast, lightweight PDF and document parsing with spatial text extraction.
Installation
npm i @llamaindex/liteparseThis also installs the lit CLI command (use npm i -g for global access).
Quick Start
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:
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:
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 throughbatch.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:
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 MarkdownReconstruction 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. Eachresult.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. WithextractImages: false, lightweight Markdown placement refs are still collected andresult.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 whendetectScreenshotRects is enabled.
Parsing from Bytes
Pass a Buffer or Uint8Array directly — useful for HTTP responses or in-memory data:
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:
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 exitScreenshots
Generate PNG screenshots of document pages:
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.
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:
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.pdfOverview (`packages/python/README.md`)
LiteParse Python
Python bindings for LiteParse — fast, lightweight PDF and document parsing with spatial text extraction.
Installation
pip install liteparseThis also installs the lit CLI command.
Quick Start
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:
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 MarkdownReconstruction 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. Eachresult.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:
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:
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:
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.
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:
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.pdfOverview (`packages/wasm/README.md`)
@llamaindex/liteparse-wasm
Browser/WebAssembly build of LiteParse — a fast, lightweight PDF parser with spatial text extraction.
This package runs entirely in the browser. No server, no cloud calls.
Install
npm install @llamaindex/liteparse-wasmQuick start
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 bboxesDocument 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.
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 arecognize method. Supplying ocrEngine alone does not enable OCR.
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:
# from packages/wasm
npm run build # web target (default)
npm run build:bundler # for webpack/rollup/vite
npm run build:nodejs # for node.jsOutput goes to pkg/.
License
Apache-2.0
Src Content Docs Liteparse Index (`docs/src/content/docs/liteparse/index.md`)
title: What is LiteParse? 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.
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: Install LiteParse and parse your first document.
- Markdown output: Render documents to clean, structured Markdown.
- Extraction options: Opt in to images, form fields, annotations, and more.
- Document complexity: Detect scanned, multi-column, and table-heavy pages up front.
- Library usage: Use LiteParse from TypeScript or Python code.
- Browser usage (WASM): Run LiteParse in the browser with zero server dependencies.
- Agent skill: Give Claude Code, Cursor, or Codex the ability to parse documents locally.
- CLI reference: Complete command and option reference.
- API reference: 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 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 |
HTTP OCR server URL | — (uses Tesseract) |
--ocr-server-header <header> |
Extra HTTP header for OCR server requests, as "Name: Value". Repeatable |
— |
--tessdata-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 to parse (e.g., "1-5,10") |
— (all pages) |
--dpi <dpi> |
Rendering DPI | 150 |
--preserve-small-text |
Keep very small text | — |
--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 |
Examples
# 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 |
HTTP OCR server URL | — (uses Tesseract) |
--ocr-server-header <header> |
Extra HTTP header for OCR server requests, as "Name: Value". Repeatable |
— |
--tessdata-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 for encrypted/protected documents (applied to all files) | — |
-q, --quiet |
Suppress progress output | — |
batch-parse accepts the same 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
# 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 to screenshot (e.g., "1,3,5" or "1-5") |
— (all pages) |
--dpi <dpi> |
Rendering DPI | 150 |
--password |
Password for encrypted/protected documents | — |
-q, --quiet |
Suppress progress output | — |
Examples
# 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 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 to check (e.g., "1-5,10,15-20") |
— (all pages) |
--password |
Password for encrypted/protected documents | — |
-q, --quiet |
Suppress the stderr verdict | — |
Examples
# 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 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
npm i @llamaindex/liteparsepip install liteparsecargo install liteparsenpm i @llamaindex/liteparse-wasmSince 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.
Quick start
Once installed, parse from the command line:
# 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:
lit batch-parse ./pdfs ./outputsScreenshots
Generate page images from a PDF for LLM agents or visual workflows:
lit screenshot document.pdf -o ./screenshotsNext steps
- Markdown output: Render documents to clean, structured Markdown.
- Extraction options: Opt in to embedded images, vector graphics, annotations, form fields, and structure trees.
- Library usage: Use LiteParse programmatically from TypeScript or Python.
- OCR configuration: Configure Tesseract, use an external OCR server, or bring your own.
- Multi-format support: Parse DOCX, XLSX, PPTX, images, and more.
- Document complexity: Check whether a document needs OCR / heavy processing before parsing, and route accordingly.
- Browser usage (WASM): Run LiteParse entirely in the browser.
- Agent skill: Add LiteParse as a skill for coding agents.
- 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 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
CLI:
npx skills add run-llama/llamaparse-agent-skills --skill liteparseThis downloads a skill file that compatible coding agents pick up automatically.
You can also copy SKILL.md
into your own skills setup by hand, or install the liteparse plugin from the
agent plugins marketplace 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 | Parsing Office files (DOCX, XLSX, PPTX) |
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 parsere-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
./contractsfolder" - "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.
Src Content Docs Liteparse Guides Browser Usage (`docs/src/content/docs/liteparse/guides/browser-usage.md`)
title: Browser Usage (WASM) 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
npm install @llamaindex/liteparse-wasmQuick start
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
Uint8Arrayinput (usefile.arrayBuffer()to get bytes from a file picker for example) - Custom OCR via the
ocrEnginecallback interface (see below) - Text, JSON, and markdown output formats
- Document complexity via
parser.isComplex(bytes)— see the complexity guide - The extraction options — annotations, form fields, structure trees, vector graphics, and the rest. See Extraction options
What doesn't work
- File path input — pass
Uint8Arrayinstead - DOCX/XLSX/PPTX conversion — requires LibreOffice, which isn't available in the browser
- Built-in Tesseract or HTTP OCR — use the custom
ocrEngineinterface instead - Screenshots — not available in the WASM build
imageOutputDir— there is no filesystem to write to. UseextractImagesand 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:
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 — 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 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
reasonsas 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
lit is-complex document.pdfThe 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.
// 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 OCRThe exit code is non-zero when any page needs OCR, so the command works 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-ocrPipe the JSON into jq to act on individual pages. The example below will list the page numbers that need OCR:
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 |
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.
lit parse document.pdf --format json --complexityThis is off by default: computing layout signals requires the full spatial projection, so it only runs when you ask for it.
lit is-complexalways populateslayout. Withinlit parse, thecomplexityobject (and itslayout) appears only when--complexityis set.
Library
The same check is available programmatically. It returns one entry per page.
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(", ")}`);
}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)}")import { LiteParse } from "@llamaindex/liteparse-wasm";
const parser = new LiteParse();
const pages = await parser.isComplex(pdfBytes); // Uint8Array
const complex = pages.some((p) => p.needsOcr);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 for the page. Omitted when layout was not computed. |
Next steps
- OCR configuration: Set up the OCR backend you route complex documents to.
- Library usage: Full programmatic API for TypeScript and Python.
- CLI reference: Complete command and option reference.
Src Content Docs Liteparse Guides Extraction (`docs/src/content/docs/liteparse/guides/extraction.mdx`)
title: Extraction Options 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.
Enabling options
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");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")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?;lit parse form.pdf --format json \
--extract-images \
--extract-form-fields \
--extract-annotations \
--extract-text-metadataEmbedded 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.
lit parse report.pdf --format json --extract-images --image-output-dir ./images{
"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_dirrequiresextract_images— setting it alone is a config error.image_mode: "embed"also implies extraction, for backwards compatibility.duplicate_ofis set when the same image appears more than once in the document; the duplicate points at the canonical entry'sidrather than re-storing the pixels.image_error_countcounts 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.
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.
{
"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.
{
"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.
{
"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
typein JSON, and istypeon the Node and Python objects. Radio groups and checkbox sets exposecontrol_count/control_indexto tie sibling widgets together, and choice fields exposeoptions/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 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.
{
"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.
lit parse document.pdf --format json --extract-blocksOnly the fields that apply to a block's kind are present:
{
"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.
{
"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:
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 []],
)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));
}
}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.
{
"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.
{ "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.
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);
}
}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)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:
// 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 },
});# (top, right, bottom, left)
parser = LiteParse(crop_box=(0.1, 0.0, 0.05, 0.0))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 topage_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.
// 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 },
],
});# (page, angle) pairs
parser = LiteParse(page_orientation_corrections=[(3, 180), (7, 90)])Next steps
- Document complexity: Route documents before you parse them.
- Markdown output: Images, links, and header/footer handling in markdown.
- Visual citations: Turn block and text-item boxes into highlights on the rendered page.
- CLI reference: Every flag, per command.
- API reference: Full
LiteParseConfigfield list.
Src Content Docs Liteparse Guides Library Usage (`docs/src/content/docs/liteparse/guides/library-usage.mdx`)
title: Library Usage 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.
TypeScript / Node.js usage
Install as a project dependency:
npm install @llamaindex/liteparse
# or
pnpm add @llamaindex/liteparseParsing a document
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:
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:
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 MarkdownConfiguration
Pass any config options to the constructor. You only need to specify what you want to override:
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.
Buffer / Uint8Array input
You can pass raw bytes directly instead of a file path:
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:
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. |
Python usage
Install the package from PyPI:
pip install liteparseParsing a document
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:
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.
Markdown output
Render the document to Markdown — headings, tables, lists, images, and links. The rendered Markdown comes back on result.text:
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 MarkdownParsing from bytes
If you already have file contents in memory (e.g. from a web upload), pass them directly to parse():
with open("document.pdf", "rb") as f:
pdf_bytes = f.read()
result = parser.parse(pdf_bytes)
print(result.text)Screenshots
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 dataEnvironment 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. |
Rust usage
Add liteparse to your Cargo.toml:
[dependencies]
liteparse = "2"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }Parsing a document
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:
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:
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.
Markdown output
Render the document to Markdown — headings, tables, lists, images, and links. The rendered Markdown comes back on result.text:
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 MarkdownScreenshots
Generate page images as PNG bytes:
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:
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. |
Browser (WASM) usage
Install the WASM package:
npm install @llamaindex/liteparse-wasmParsing a document
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 bboxesConfiguration
All options are optional and use camelCase:
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 for the imageMode and extractLinks options.
const parser = new LiteParse({ outputFormat: "markdown" });
const result = await parser.parse(bytes);
console.log(result.text); // rendered MarkdownOCR 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:
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 aLiteParseinstance.
Src Content Docs Liteparse Guides Markdown (`docs/src/content/docs/liteparse/guides/markdown.mdx`)
title: Markdown Output 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
# Render a document to Markdown
lit parse document.pdf --format markdown -o output.md
# Print Markdown to stdout
lit parse document.pdf --format markdownImages
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):
# 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:
lit parse document.pdf --format markdown --no-linksHeaders 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:
lit parse document.pdf --format markdown --keep-headers-footersLibrary
The rendered Markdown is returned on result.text.
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 Markdownfrom 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 Markdownuse 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 MarkdownQuality 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
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 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)
.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)
# macOS
brew install --cask libreoffice
# Ubuntu/Debian
apt-get install libreoffice
# Windows
choco install libreoffice-freshOn 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:
lit parse report.docx
lit parse slides.pptx --format json
lit parse spreadsheet.xlsx -o output.txt
lit parse scan.pngBatch mode also handles mixed formats:
lit batch-parse ./documents ./output --recursiveHow it works
- LiteParse detects the file extension
- If it's not a PDF, it converts to PDF — office documents via LibreOffice, images and SVGs natively in Rust
- The resulting PDF is parsed normally
- 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 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:
lit parse document.pdfLanguage support
Specify the OCR language for better accuracy on non-English documents:
lit parse document.pdf --ocr-language fra # French
lit parse document.pdf --ocr-language deu # German
lit parse document.pdf --ocr-language jpn # JapaneseTesseract uses ISO 639-3 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:
# 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/tessdataThe 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) and point LiteParse at it withTESSDATA_PREFIXor--tessdata-path(see above). - Use an HTTP OCR server instead of the built-in engine.
- Disable OCR with
--no-ocrif 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:
lit parse document.pdf --no-ocrHTTP 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
# 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/ocrPaddleOCR
# 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/ocrAuthenticated servers
Pass extra HTTP headers on every OCR request with --ocr-server-header, repeating the flag for each header:
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:
lit parse document.pdf --num-workers 8This 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-dataRequest 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-languageis set to, which defaults to the Tesseract-style ISO 639-3 codeeng— noten. Your server should accept both. The bundled EasyOCR and PaddleOCR servers do this by mappingeng→enbefore dispatch; do the same in a custom server, or pass--ocr-language enexplicitly.
Response format:
{
"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
# Quick test with curl
curl -X POST http://localhost:8828/ocr \
-F "[email protected]" \
-F "language=en" | jq .
# Use with LiteParse
lit parse document.pdf --ocr-server-url http://localhost:8828/ocrCommon 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 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 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
# 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:
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 sidebar: order: 10
liteparse-server is an Express server that exposes @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
Slim server
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:
git clone https://github.com/run-llama/liteparse-server
cd liteparse-serverYou can run the API server with either Bun or Node:
bun run start-slim:bun
# or with Node
npm run start-slim:nodeThe 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:
docker pull ghcr.io/run-llama/liteparse-server:mainYou can then run it exposing port 5000:
docker run -p 5000:5000 ghcr.io/run-llama/liteparse-server:mainBuild locally
If you clone the repository, the provided slim.Dockerfile produces a self-contained image:
# Build the image
docker build -f slim.Dockerfile -t liteparse-server-slim .
# Run exposing port 5000
docker run -p 5000:5000 liteparse-server-slimThe 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. 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 (whentext=true)200 application/json—{ "pages": [...] }(whentext=false)400— missingfile429— 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:
{
"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:
curl -X POST http://localhost:5000/parse \
-F "file=@path/to/document.pdf"Parse a file and get plain text:
curl -X POST "http://localhost:5000/parse?text=true" \
-F "file=@path/to/document.pdf"Pass a LiteParseConfig (e.g. enable OCR):
curl -X POST http://localhost:5000/parse \
-F "file=@path/to/document.pdf" \
-F 'config={"ocrEnabled":true}'Screenshot specific pages and stream NDJSON:
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 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.
lit parse document.pdf --format 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:
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}`);
}
}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}')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:
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:
lit parse document.pdf --format json -o result.jsonGenerate page screenshots alongside:
lit screenshot document.pdf -o ./screenshotsFrom there, you (or an agent) can process the resulting JSON and screenshots as needed using any tools available.
Tips
- Use the same
dpivalue for bothparse()andscreenshot(). The default is150for both. - Page
widthandheightin 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
textItemsand 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. Each block carries onebboxcovering 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