pdf-inspector
Library New
Fast PDF inspection, classification, and text extraction with smart scanned vs text-based detection
- Downloads
- 622,399
- Dependents
- 16
- Stars
- 19,959
- Forks
- 1,357
- Contributors
- 12
- Commits
- 554
- Latest release
- 1.25.2 ·
About
Fast Rust library for PDF classification and text extraction. By default it detects whether a PDF is text-based or scanned, extracts text with position awareness, and converts to clean Markdown without OCR. Native Rust and CLI consumers can opt into selective OCR. Includes bindings for Python, Node.js, and browser WebAssembly.
Built by Firecrawl to handle text-based PDFs locally in under 200ms, skipping expensive OCR services for the ~54% of PDFs that don't need them.
Read the full README
Features
- Smart classification — Detect TextBased, Scanned, ImageBased, or Mixed PDFs in ~10-50ms by sampling content streams. Returns a confidence score (0.0-1.0) and per-page OCR routing.
- Text extraction — Position-aware extraction with font info, X/Y coordinates, and automatic multi-column reading order. Rotated runs (margin stamps, chart axis titles) keep a true axis-aligned box and report their
rotationangle instead of collapsing to zero width. - Markdown conversion — Headings (H1-H4 via font size ratios), bullet/numbered/letter lists, code blocks (monospace font detection), tables (rectangle-based and heuristic), bold/italic formatting, URL linking, and page breaks.
- Table detection — Dual-mode: rectangle-based detection from PDF drawing ops, plus heuristic detection from text alignment. Handles financial tables, footnotes, and continuation tables across pages.
- CID font support — ToUnicode CMap decoding for Type0/Identity-H fonts, UTF-16BE, UTF-8, and Latin-1 encodings.
- Multi-column layout — Automatic detection of newspaper-style columns, sequential reading order, and RTL text support.
- Encoding issue detection — Automatically flags broken font encodings so callers can fall back to OCR.
- Selective OCR — Rust, CLI, Python, and Node can render only pages that need OCR, run PP-OCRv6 Small locally, and preserve per-page provenance and hosted-fallback recommendations.
- Single document load — The document is parsed once and shared between detection and extraction, avoiding redundant I/O.
- Browser WebAssembly — Run the same Rust parser locally in browsers and Web Workers, with embedded CMaps and no server round trip.
- Lightweight by default — The default Rust and browser builds remain pure extraction. Native Python and Node packages include the OCR integration, but PDFium, ONNX Runtime, and model files remain external and are touched only when a page is routed to OCR.
Benchmark
Evaluated on the opendataloader-bench corpus (200 PDFs). Only local engines without model-based PDF parsing are shown; OCR was disabled. Scores are 0-1, higher is better.
| Engine | Overall | Reading Order (NID) | Tables (TEDS) | Headings (MHS) | Speed (200 docs) |
|---|---|---|---|---|---|
| pdf-inspector | 0.875 | 0.915 | 0.814 | 0.788 | 0.470s |
| liteparse | 0.873 | 0.913 | 0.693 | 0.811 | 0.750s |
| opendataloader | 0.831 | 0.902 | 0.489 | 0.739 | 2.569s |
| pymupdf4llm | 0.735 | 0.886 | 0.401 | 0.424 | 17.117s |
| markitdown | 0.589 | 0.844 | 0.273 | 0.000 | 16.165s |
Results were refreshed on July 31, 2026, on an Apple M4 Pro. Engine versions were pdf-inspector 0.2.6, LiteParse 2.10.1, OpenDataLoader 2.2.1, PyMuPDF4LLM 0.2.0, and MarkItDown 0.1.5. Speed is the median of five alternating or rotating complete corpus runs after an excluded warm-up run, with each parser processing documents sequentially in a single process.
The complete parser configuration, per-document predictions, evaluator output, and generated charts are available in the reproducible results branch.
Best fit: Native-text PDFs where speed, reading order, and table structure matter. In this comparison, pdf-inspector delivered the higher overall, reading-order, and table scores, along with the fastest complete run. That makes it a strong local default for reports, research papers, financial documents, invoices, and legal PDFs that need clean, structured Markdown without adding OCR latency or infrastructure.
Use the paired benchmark harness to compare two local builds against the exact same corpus and evaluator revision.
Quick start
Python
pip install pdf-inspector
import pdf_inspector
result = pdf_inspector.process_pdf("document.pdf")
print(result.pdf_type) # "text_based", "scanned", "image_based", "mixed"
print(result.markdown) # Markdown string or None
# Selective OCR; clean text PDFs do not load the external OCR runtime.
ocr = pdf_inspector.process_pdf_with_ocr("document.pdf")
print(ocr.pages_routed_to_ocr)
Full API reference: docs/python.md
Node.js
npm install @firecrawl/pdf-inspector
import { readFileSync } from 'fs';
import { processPdf, processPdfWithOcr } from '@firecrawl/pdf-inspector';
const pdf = readFileSync('document.pdf');
const result = processPdf(pdf);
console.log(result.pdfType); // "TextBased", "Scanned", "ImageBased", "Mixed"
console.log(result.markdown); // Markdown string or null
const ocr = await processPdfWithOcr(pdf); // selective OCR, off the event loop
console.log(ocr.pagesRoutedToOcr);
Full API reference: napi/README.md
Browser WebAssembly
npm install @firecrawl/pdf-inspector-wasm
import init, { processPdf } from '@firecrawl/pdf-inspector-wasm';
await init();
const response = await fetch('/document.pdf');
const pdf = new Uint8Array(await response.arrayBuffer());
const result = processPdf(pdf);
console.log(result.pdfType);
console.log(result.markdown);
Full API reference: wasm/README.md
Rust
Install from crates.io:
cargo add pdf-inspector
Or add it manually:
[dependencies]
pdf-inspector = "1"
use pdf_inspector::process_pdf;
let result = process_pdf("document.pdf")?;
println!("Type: {:?}", result.pdf_type);
if let Some(markdown) = &result.markdown {
println!("{}", markdown);
}
Full API reference: docs/rust-api.md
CLI
# Install the CLI tools
cargo install pdf-inspector
# Convert PDF to Markdown
pdf2md document.pdf
# JSON output (for piping), with the document information (title, author, producer, dates, ...)
pdf2md document.pdf --json
# Positioned TextItem JSON (coordinates relative to the visible page box): axis-aligned box, rotation, font, paint (fill and stroke colour, render mode), underline metadata
pdf2md document.pdf --items-json
# Raw markdown only (no headers)
pdf2md document.pdf --raw
# Token-efficient output (collapses long dot leaders and similar source padding)
pdf2md document.pdf --compact
# Insert page break markers (<!-- Page N -->)
pdf2md document.pdf --pages
# Process only specific pages
pdf2md document.pdf --select-pages 1,3,5-10
# Detection only (no extraction)
detect-pdf document.pdf
detect-pdf document.pdf --json
# Detection + layout analysis (tables, columns)
detect-pdf document.pdf --analyze --json
Rust and CLI consumers opt into OCR at build time:
cargo install pdf-inspector --features ocr --bin pdf2md
PDFIUM_LIB_PATH=/path/to/libpdfium ORT_DYLIB_PATH=/path/to/libonnxruntime \
pdf2md scan.pdf --ocr auto --json
The OCR JSON envelope is versioned and reports routed pages, per-page source and confidence, warnings, and pages recommended for the hosted document pipeline. Native Python and Node packages expose the same pipeline without a source-build feature. All native entry points still require separately installed PDFium and ONNX Runtime libraries only when OCR is routed. See the OCR runtime setup guide for pinned downloads, platform support, model-cache behavior, and hosted-fallback integration. See the Rust API guide for lower-level controls.
From a source checkout, use cargo run --bin pdf2md -- document.pdf or cargo run --bin detect-pdf -- document.pdf instead.
Architecture
PDF bytes
│
├─► detector → PdfType (TextBased / Scanned / ImageBased / Mixed)
│
└─► extractor
├─ fonts → font widths, encodings
├─ content_stream → walk PDF operators → TextItems + PdfRects
├─ xobjects → Form XObject text, image placeholders
├─ links → hyperlinks, AcroForm fields
└─ layout → column detection → line grouping → reading order
│
├─► tables
│ ├─ detect_rects → rectangle-based tables (union-find)
│ ├─ detect_heuristic → alignment-based tables
│ ├─ grid → column/row assignment → cells
│ └─ format → cells → Markdown table
│
└─► markdown
├─ analysis → font stats, heading tiers
├─ preprocess → merge headings, drop caps
├─ convert → line loop + table/image insertion
├─ classify → captions, lists, code
└─ postprocess → cleanup → final Markdown
The document is loaded once via load_document_from_path / load_document_from_mem and shared between the detection and extraction stages, so there's no redundant parsing.
Project structure
src/
lib.rs — Public API, PdfOptions builder, convenience functions
python.rs — PyO3 Python bindings
types.rs — Shared types: TextItem, TextLine, PdfRect, ItemType
text_utils.rs — Character/text helpers (CJK, RTL, ligatures, bold/italic)
process_mode.rs — ProcessMode enum (DetectOnly, Analyze, Full)
detector.rs — Fast PDF type detection without full document load
glyph_names.rs — Adobe Glyph List → Unicode mapping
tounicode.rs — ToUnicode CMap parsing for CID-encoded text
extractor/ — Text extraction pipeline
tables/ — Table detection and formatting
markdown/ — Markdown conversion and structure detection
bin/ — CLI tools (pdf2md, detect_pdf)
napi/ — Node.js/Bun bindings (napi-rs)
wasm/ — Browser bindings (wasm-bindgen)
How classification works
- Parse the xref table and page tree (no full object load)
- Select pages based on
ScanStrategy(default: a sample of 8 pages) - Look for
Tj/TJ(text operators) andDo(image operators) in content streams - Classify based on text operator presence across sampled pages
This detects 300+ page PDFs in milliseconds. The result includes pages_needing_ocr — a list of specific page numbers that lack text, enabling per-page OCR routing instead of all-or-nothing.
Scan strategies
| Strategy | Behavior | Best for |
|---|---|---|
EarlyExit | Scan all pages, stop on first non-text page | Pipelines routing TextBased PDFs to fast extraction |
Full | Scan all pages, no early exit | Accurate Mixed vs Scanned classification |
Sample(n) (default: Sample(8)) | Sample n evenly distributed pages (first, last, middle) | Very large PDFs where speed matters more than precision |
Pages(vec) | Only scan specific 1-indexed page numbers | When the caller knows which pages to check |
Markdown output
The converter handles:
| Element | How it's detected |
|---|---|
| Headings (H1-H4) | Font size tiers relative to body text, with 0.5pt clustering |
| Bold/italic | Font name patterns (Bold, Italic, Oblique) |
| Bullet lists | •, -, *, ○, ●, ◦ prefixes |
| Numbered lists | 1., 1), (1) patterns |
| Letter lists | a., a), (a) patterns |
| Code blocks | Monospace fonts (Courier, Consolas, Monaco, Menlo, Fira Code, JetBrains Mono) and keyword detection |
| Tables | Rectangle-based detection from PDF drawing ops + heuristic detection from text alignment |
| Financial tables | Token splitting for consolidated numeric values |
| Captions | "Figure", "Table", "Source:" prefix detection |
| Sub/superscript | Font size and Y-offset relative to baseline |
| URLs | Converted to Markdown links |
| Hyphenation | Rejoins words broken across lines |
| Page numbers | Filtered from output |
| Drop caps | Large initial letters merged with following text |
| Dot leaders | TOC-style dots collapsed to " ... " |
Use case: smart PDF routing
pdf-inspector was built for pipelines that process PDFs at scale. Instead of sending every PDF through OCR:
PDF arrives
→ pdf-inspector classifies it (~20ms)
→ TextBased + high confidence?
YES → extract locally (~150ms), done
NO → send to OCR service (2-10s)
This saves cost and latency for the majority of PDFs that are already text-based (reports, papers, invoices, legal docs).
Debugging
See docs/debugging.md for RUST_LOG environment variable usage.
License
Compatibility
- Runs on
- GPU support unknown
What shows it
- no dependency or feature matches a backend row
- Native code
- Optional
- Binds native code: ort-sys (feature ocr-oar)
- Python bindings
- Has Python bindings
- pdf-inspector needs Python only with feature python
- License
- MIT
Compare with similar projects
Order: strongest tier first, then most downloads, then name, with unmeasured values last. A library counts downloads over 30 days. An app counts binary downloads per release. An app with none counts its crate downloads over 30 days.
| Project | Downloads | Dependents | Stars | Latest release | Runs on (GPU) | Native code | License |
|---|---|---|---|---|---|---|---|
|
Fast PDF inspection, classification, and text extraction with smart scanned vs text-based detection Python bindings |
622,399 downloads over 30 days | 16 | 19,959 | 1.25.2 | GPU support unknown | Binds native code: ort-sys (feature ocr-oar) | MIT |
|
pdf_oxide Featured
The fastest Rust PDF library — 0.8ms mean, 5× faster than the industry leaders, 100% pass rate on 3,830 real-world PDFs. Text extraction, Markdown/HTML conversion, PDF creation and editing. Python bindings |
316,729 downloads over 30 days | 30 | 1,180 | 0.3.78 | GPU support unknown | Binds native code: aws-lc-rs (feature fips), lcms2-sys (feature icc-lcms2), ort-sys (feature gpu) | Apache-2.0 OR MIT |
|
kreuzberg-lts Listed
High-performance document intelligence library for Rust. Extract text, metadata, and structured data from PDFs, Office documents, images, and 91+ formats and 248 programming languages via tree-sitter code intelligence with async/sync APIs. |
54,366 downloads over 30 days | 7 | 21 | 4.10.4 | GPU support unknown | Binds native code: kreuzberg_tesseract, clipper-sys, ort-sys, tree-sitter (feature tree-sitter), tree-sitter-language (feature tree-sitter). Build uses a native toolchain: pkg-config, cc (feature build-tesseract), cmake (feature build-tesseract) | MIT |
|
edgeparse Listed
EdgeParse core library — PDF parsing and structured data extraction |
6,764 downloads over 30 days | 2 | 155 | 0.3.2 | GPU support unknown | No native code found | Apache-2.0 |
|
Extractous provides a fast and efficient way to extract content from all kind of file formats including PDF, Word, Excel CSV, Email etc... Internally it uses a natively compiled Apache Tika for formats are not supported natively by the Rust core |
1,515 downloads over 30 days | 3 | 1,787 | 0.3.0 | GPU support unknown | No native code found | Apache-2.0 |
|
xberg New
High-performance document intelligence library for Rust. Extract text, metadata, and structured data from PDFs, Office documents, images, and 107 formats and 371 programming languages via tree-sitter code intelligence with async/sync APIs. |
26,095 downloads over 30 days | 2 | 9,565 | 1.3.10 | CUDA, Metal | Binds native code: libheif-sys, libz-sys, xberg_libwpd, xberg_tesseract, clipper-sys and 4 more. Build uses a native toolchain: cc, bindgen (feature bindings), cmake (feature build-tesseract), pkg-config (feature dynamic-linking), vcpkg (feature dynamic-linking) | MIT |
Why NewIts first release on 5 Jun 2026 is under 180 days old.
The check tests the Featured gates once the first release passes this age limit.
What else the check found
- The last activity is on 7 Oct 2026, 33 days ago. The rules expect an update within 1200 days. Dormancy rule
- The latest release requires the deprecated dependency ttf-parser. RUSTSEC-2026-0192 Rot signals rule
Exact rules: New. The check on applied thresholds version 9.
Every rule outcome, as the check records it
expected-interval: version 1.25.2 base 300, doubled for 622399 downloads per month, doubled for 16 active users: E = 1200 daysdormancy: last activity 2026-10-07, 33 days ago, E = 1200: activerot: deprecated requirement `ttf-parser (RUSTSEC-2026-0192, https://rustsec.org/advisories/RUSTSEC-2026-0192.html)`new: new: first release 2026-06-05 is under 180 days old
Measurements and sources
Each signal row shows the raw value, its source and the fetch date. The Sources list gathers every distinct source on this page. Movement needs more history.
Sources6 sources
- Description: the crates.io description
- crates.io database dump, fetched
- RustSec advisory database, fetched
- GitHub repository, fetched
- crates.io package, fetched
- GitHub contributors, fetched
Adoption16 active dependents, 16 new dependents in 365 days1 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Active dependents | 16 | crates.io database dump | Yes | |
| New dependents (365 days) | 16 | crates.io database dump | Yes | |
| Peak active dependents | 16 | crates.io database dump | Yes | |
| Active dependents as a share of the peak | 1 | crates.io database dump | Yes | |
| Binary downloads per release | Not measured | Yes |
MaintenanceLatest human commit: 33 days ago3 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Latest human commit | 2026-10-07 | GitHub repository | Yes | |
| Latest release | 2026-09-28 | crates.io database dump | Yes | |
| First release | 2026-06-05 | crates.io database dump | Yes | |
| Latest version | 1.25.2 | crates.io database dump | Yes | |
| Days the oldest outside issue waits for a reply | 70 | GitHub repository | Yes | |
| Maintainer replies over 365 days | 30 | GitHub repository | Yes | |
| Repository archived | false | GitHub repository | Yes | |
| Repository gone | false | GitHub repository | Yes | |
| Release commit or manifest in the repo | true | crates.io package | Yes | |
| Published docs | true | crates.io database dump | Yes | |
| Latest release fails cargo check | Not measured | Yes | ||
| Build check date | Not measured | Yes | ||
| Build check toolchain | Not measured | Yes |
SecurityUnpatched vulnerabilities: none
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Unpatched vulnerabilities past the grace window | none | RustSec advisory database | Yes | |
| RustSec unmaintained advisory | none | RustSec advisory database | Yes | |
| Deprecation notice | none | crates.io database dump | Yes | |
| Deprecated requirement | ttf-parser (RUSTSEC-2026-0192, https://rustsec.org/advisories/RUSTSEC-2026-0192.html) | crates.io database dump | Yes |
Distribution26 releases, 622,399 downloads over 30 days5 not measured
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| Releases | 26 | crates.io database dump | Yes | |
| Downloads over 30 days | 622399 | crates.io database dump | Yes | |
| Binary downloads over the last 3 releases | Not measured | Yes | ||
| Homebrew installs on request over 365 days | Not measured | Yes | ||
| Repology repository families | Not measured | Yes | ||
| Docker pull growth over 90 days | Not measured | Yes | ||
| Docker pulls at the growth baseline | Not measured | Yes | ||
| Every version a prerelease | false | crates.io database dump | Yes | |
| Every version 0.0.x | false | crates.io database dump | Yes | |
| Every version yanked | false | crates.io database dump | Yes | |
| Crate deleted from crates.io | false | crates.io database dump | Yes |
Repository19,959 stars, 1,357 forks, 12 contributors, 554 commits. Stars alone never qualify a project.
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| GitHub stars | 19959 | GitHub repository | Yes — one part of the repository route | |
| GitHub forks | 1357 | GitHub repository | Yes — one part of the repository route | |
| Contributors with a GitHub account | 12 | GitHub contributors | Yes — one part of the repository route | |
| Commits on the default branch | 554 | GitHub repository | No | |
| Repository's main crate left out of the entry | none | crates.io database dump | Yes |
Facts that never count
| Signal | Value | Source | Fetched | Counts |
|---|---|---|---|---|
| License | MIT | crates.io database dump | No | |
| Runs on, from listed crates and their direct dependencies | GPU support unknown | crates.io database dump | No | |
| What shows each backend | no dependency or feature matches a backend row | crates.io database dump | No | |
| Native code, from listed crates and their dependencies | Binds native code: ort-sys (feature ocr-oar) | crates.io database dump | No | |
| Python bindings, from direct dependencies of listed crates | Has Python bindings: pdf-inspector needs Python only with feature python | crates.io database dump | No | |
| Crates that depend on a listed crate directly | 16 | crates.io database dump | No |
Corrections and review history
No correction applies to this entry.
Judgments
A judgment enters only through an approved issue and a merged pull request.
No judgment applies to this entry.