pdf-inspector

Library New

Fast PDF inspection, classification, and text extraction with smart scanned vs text-based detection

Checked · Sources

Downloads
622,399
Dependents
16
Stars
19,959
Forks
1,357
Contributors
12
Commits
554
Latest release
1.25.2 ·

About

From the project README ·

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 rotation angle 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.

EngineOverallReading Order (NID)Tables (TEDS)Headings (MHS)Speed (200 docs)
pdf-inspector0.8750.9150.8140.7880.470s
liteparse0.8730.9130.6930.8110.750s
opendataloader0.8310.9020.4890.7392.569s
pymupdf4llm0.7350.8860.4010.42417.117s
markitdown0.5890.8440.2730.00016.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

  1. Parse the xref table and page tree (no full object load)
  2. Select pages based on ScanStrategy (default: a sample of 8 pages)
  3. Look for Tj/TJ (text operators) and Do (image operators) in content streams
  4. 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
StrategyBehaviorBest for
EarlyExitScan all pages, stop on first non-text pagePipelines routing TextBased PDFs to fast extraction
FullScan all pages, no early exitAccurate 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 numbersWhen the caller knows which pages to check

Markdown output

The converter handles:

ElementHow it's detected
Headings (H1-H4)Font size tiers relative to body text, with 0.5pt clustering
Bold/italicFont name patterns (Bold, Italic, Oblique)
Bullet lists•, -, *, ○, ●, ◦ prefixes
Numbered lists1., 1), (1) patterns
Letter listsa., a), (a) patterns
Code blocksMonospace fonts (Courier, Consolas, Monaco, Menlo, Fira Code, JetBrains Mono) and keyword detection
TablesRectangle-based detection from PDF drawing ops + heuristic detection from text alignment
Financial tablesToken splitting for consolidated numeric values
Captions"Figure", "Table", "Source:" prefix detection
Sub/superscriptFont size and Y-offset relative to baseline
URLsConverted to Markdown links
HyphenationRejoins words broken across lines
Page numbersFiltered from output
Drop capsLarge initial letters merged with following text
Dot leadersTOC-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

MIT

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.

pdf-inspector beside the projects that share a topic with it
ProjectDownloadsDependentsStarsLatest releaseRuns on (GPU)Native codeLicense
pdf-inspector This page New

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

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 Listed Dormant

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

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 days
  • dormancy: last activity 2026-10-07, 33 days ago, E = 1200: active
  • rot: 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
Adoption16 active dependents, 16 new dependents in 365 days1 not measured
Adoption measurements
SignalValueSourceFetchedCounts
Active dependents16crates.io database dumpYes
New dependents (365 days)16crates.io database dumpYes
Peak active dependents16crates.io database dumpYes
Active dependents as a share of the peak1crates.io database dumpYes
Binary downloads per releaseNot measuredYes
MaintenanceLatest human commit: 33 days ago3 not measured
Maintenance measurements
SignalValueSourceFetchedCounts
Latest human commit2026-10-07GitHub repositoryYes
Latest release2026-09-28crates.io database dumpYes
First release2026-06-05crates.io database dumpYes
Latest version1.25.2crates.io database dumpYes
Days the oldest outside issue waits for a reply70GitHub repositoryYes
Maintainer replies over 365 days30GitHub repositoryYes
Repository archivedfalseGitHub repositoryYes
Repository gonefalseGitHub repositoryYes
Release commit or manifest in the repotruecrates.io packageYes
Published docstruecrates.io database dumpYes
Latest release fails cargo checkNot measuredYes
Build check dateNot measuredYes
Build check toolchainNot measuredYes
SecurityUnpatched vulnerabilities: none
Security measurements
SignalValueSourceFetchedCounts
Unpatched vulnerabilities past the grace windownoneRustSec advisory databaseYes
RustSec unmaintained advisorynoneRustSec advisory databaseYes
Deprecation noticenonecrates.io database dumpYes
Deprecated requirementttf-parser (RUSTSEC-2026-0192, https://rustsec.org/advisories/RUSTSEC-2026-0192.html)crates.io database dumpYes
Distribution26 releases, 622,399 downloads over 30 days5 not measured
Distribution measurements
SignalValueSourceFetchedCounts
Releases26crates.io database dumpYes
Downloads over 30 days622399crates.io database dumpYes
Binary downloads over the last 3 releasesNot measuredYes
Homebrew installs on request over 365 daysNot measuredYes
Repology repository familiesNot measuredYes
Docker pull growth over 90 daysNot measuredYes
Docker pulls at the growth baselineNot measuredYes
Every version a prereleasefalsecrates.io database dumpYes
Every version 0.0.xfalsecrates.io database dumpYes
Every version yankedfalsecrates.io database dumpYes
Crate deleted from crates.iofalsecrates.io database dumpYes
Repository19,959 stars, 1,357 forks, 12 contributors, 554 commits. Stars alone never qualify a project.
Repository measurements
SignalValueSourceFetchedCounts
GitHub stars19959GitHub repositoryYes — one part of the repository route
GitHub forks1357GitHub repositoryYes — one part of the repository route
Contributors with a GitHub account12GitHub contributorsYes — one part of the repository route
Commits on the default branch554GitHub repositoryNo
Repository's main crate left out of the entrynonecrates.io database dumpYes
Facts that never count
Facts that never count measurements
SignalValueSourceFetchedCounts
LicenseMITcrates.io database dumpNo
Runs on, from listed crates and their direct dependenciesGPU support unknowncrates.io database dumpNo
What shows each backendno dependency or feature matches a backend rowcrates.io database dumpNo
Native code, from listed crates and their dependenciesBinds native code: ort-sys (feature ocr-oar)crates.io database dumpNo
Python bindings, from direct dependencies of listed cratesHas Python bindings: pdf-inspector needs Python only with feature pythoncrates.io database dumpNo
Crates that depend on a listed crate directly16crates.io database dumpNo

Corrections and review history

Report a correction

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.