Skip to main content

pdf-inspector

Fast PDF classification, text extraction, and selective OCR. Detects whether a PDF is text-based or scanned, extracts text with position awareness, and converts clean native and OCR results to Markdown. Python bindings via PyO3 for the pdf-inspector Rust library.

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.

Features

  • Smart classificationtext_based / scanned / image_based / mixed in ~10–50ms, with a confidence score and per-page OCR routing.
  • Markdown conversion — headings, lists, code blocks, bold/italic, URL linking, and dual-mode table detection (PDF drawing ops + text-alignment heuristics).
  • Layout-aware extraction — multi-column reading order, position and font info per text item, RTL support.
  • Robust text decoding — CID/Type0 fonts via ToUnicode CMaps, plus automatic flagging of broken encodings so callers can fall back to OCR.
  • Selective OCRauto routes only pages rejected by native extraction; force OCRs every selected page; off keeps the result/provenance contract without external runtime work.
  • External artifacts — the wheel embeds no OCR models, PDFium, or ONNX Runtime; clean auto requests never load or download them.

Benchmark

opendataloader-bench corpus (200 PDFs), local engines without model-based PDF parsing; OCR disabled. Scores 0–1, higher is better:

Engine Overall Reading order Tables (TEDS) Headings Speed
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

Refreshed July 31, 2026, on Apple M4 Pro; speed is the median of five complete corpus runs after an excluded warm-up. Full methodology and versions are in the repo README, with raw timings and artifacts in the results branch.

Install

pip install pdf-inspector

Prebuilt wheels cover CPython ≥3.8 on Linux (x86_64, aarch64), macOS (Intel, Apple Silicon), and Windows (x64). Other platforms build from source, which requires a Rust toolchain. For local development in a repo checkout:

pip install maturin
maturin develop --release

OCR calls that route work require compatible PDFium and ONNX Runtime shared libraries. Set PDFIUM_LIB_PATH and ORT_DYLIB_PATH when they are not on the platform library search path. The pinned OCR model set is downloaded and checksum-verified on the first routed page; use offline=True with a warm cache or model_directory to prohibit network access. See the OCR runtime setup guide for pinned downloads, supported platforms, and hosted-fallback behavior.

Usage

import pdf_inspector

# Full processing: detect + extract + convert to Markdown
result = pdf_inspector.process_pdf("document.pdf")
print(result.pdf_type)      # "text_based", "scanned", "image_based", "mixed"
print(result.confidence)     # 0.0 - 1.0
print(result.page_count)     # number of pages
print(result.markdown)       # Markdown string or None

# Process specific pages only
result = pdf_inspector.process_pdf("document.pdf", pages=[1, 3, 5])

# Process from bytes (no filesystem needed)
with open("document.pdf", "rb") as f:
    result = pdf_inspector.process_pdf_bytes(f.read())

# Fast detection only (no text extraction)
result = pdf_inspector.detect_pdf("document.pdf")
if result.pdf_type == "text_based":
    print("Can extract locally!")
else:
    print(f"Pages needing OCR: {result.pages_needing_ocr}")

# Plain text extraction
text = pdf_inspector.extract_text("document.pdf")

# Positioned text items with font info
items = pdf_inspector.extract_text_with_positions("document.pdf")
for item in items[:5]:
    print(f"'{item.text}' at ({item.x:.0f}, {item.y:.0f}) size={item.font_size}")

# Per-page markdown (one Markdown string per page, plus layout metadata)
result = pdf_inspector.extract_pages_markdown("document.pdf")
for page in result.pages:
    print(f"Page {page.page}: {len(page.markdown)} chars, needs_ocr={page.needs_ocr}")

# Restrict to specific 0-indexed pages (preserves caller order)
result = pdf_inspector.extract_pages_markdown("document.pdf", pages=[0, 2])

# One-call selective OCR. This releases the GIL while processing.
ocr = pdf_inspector.process_pdf_with_ocr("document.pdf")
for page in ocr.pages:
    print(page.page_number, page.provenance.source)

# Restrict OCR processing to 1-indexed PDF pages and prohibit downloads.
ocr = pdf_inspector.process_pdf_with_ocr(
    "document.pdf",
    page_numbers=[1, 3],
    model_directory="/opt/models/pp-ocrv6-small",
    offline=True,
)

# Structure-tree elements from tagged PDFs (empty list when untagged).
# Pages are 1-indexed to match TextItem.page, so (page, mcid) joins directly
# against extract_text_with_positions — e.g. to recover real heading levels:
elements = pdf_inspector.extract_structure_elements("tagged.pdf")
roles = {(e.page, e.mcid): e.role for e in elements}
headings = [
    item.text
    for item in pdf_inspector.extract_text_with_positions("tagged.pdf")
    if item.mcid is not None and roles.get((item.page, item.mcid), "").startswith("H")
]

API reference

Function Description
process_pdf(path, pages=None) Full processing (detect + extract + markdown)
process_pdf_bytes(data, pages=None) Full processing from bytes
process_pdf_with_ocr(path, **options) Native extraction + selective OCR with provenance
process_pdf_with_ocr_bytes(data, **options) Native extraction + selective OCR from bytes
detect_pdf(path) Fast detection only (returns PdfResult)
detect_pdf_bytes(data) Fast detection from bytes
classify_pdf(path) Lightweight classification (returns PdfClassification)
classify_pdf_bytes(data) Lightweight classification from bytes
extract_text(path) Plain text extraction
extract_text_bytes(data) Plain text extraction from bytes
extract_text_with_positions(path, pages=None) Text with X/Y coords and font info
extract_text_with_positions_bytes(data, pages=None) Text with positions from bytes
extract_text_in_regions(path, page_regions) Extract text in bounding-box regions
extract_text_in_regions_bytes(data, page_regions) Region extraction from bytes
extract_pages_markdown(path, pages=None) Per-page Markdown + layout metadata (all pages by default)
extract_pages_markdown_bytes(data, pages=None) Per-page Markdown from bytes
extract_structure_elements(path, pages=None) Structure-tree elements from tagged PDFs (page, mcid, role)
extract_structure_elements_bytes(data, pages=None) Structure-tree elements from bytes

Types

Type stubs (pdf_inspector.pyi) ship with the package. Result types at a glance:

class PdfResult:                     # process_pdf / detect_pdf
    pdf_type: str                    # "text_based" | "scanned" | "image_based" | "mixed"
    markdown: str | None             # extracted Markdown (None for detect_pdf)
    page_count: int
    processing_time_ms: int
    pages_needing_ocr: list[int]     # 1-indexed
    ocr_reasons_by_page: list[PageOcrReasons]
    title: str | None
    confidence: float                # 0.0 - 1.0
    is_complex_layout: bool
    pages_with_tables: list[int]
    pages_with_columns: list[int]
    has_encoding_issues: bool        # broken font encodings — consider OCR fallback

class PageOcrReasons:                # per-page OCR diagnostics
    page: int                        # 1-indexed
    reasons: list[str]               # machine-readable reason identifiers

class OcrModelIdentity:
    name: str                        # model family/name
    revision: str                    # immutable artifact-set revision

class OcrTimings:                    # per-page processing stages
    render_ms: int
    ocr_ms: int
    assembly_ms: int

class OcrPageProvenance:
    page_number: int                 # 1-indexed
    source: Literal["native", "ocr", "fused"]
    ocr_model: OcrModelIdentity | None
    render_dpi: float | None
    ocr_confidence: float | None
    timings: OcrTimings
    warnings: list[str]
    hosted_recommended: bool

class OcrPageResult:
    page_number: int                 # 1-indexed
    markdown: str
    provenance: OcrPageProvenance

class OcrPdfResult:                  # process_pdf_with_ocr / bytes
    markdown: str
    pages: list[OcrPageResult]
    page_count: int
    pages_recommended_for_ocr: list[int]
    pages_routed_to_ocr: list[int]
    pages_recommending_hosted: list[int]
    ocr_reasons_by_page: list[PageOcrReasons]
    pages_with_tables: list[int]
    pages_with_columns: list[int]
    is_complex: bool
    processing_time_ms: int
    render_time_ms: int
    ocr_time_ms: int

class PdfClassification:             # classify_pdf
    pdf_type: str
    page_count: int
    pages_needing_ocr: list[int]     # 0-indexed
    confidence: float

class TextItem:                      # extract_text_with_positions
    text: str
    x: float
    y: float
    width: float
    height: float
    font: str
    font_size: float
    page: int
    is_bold: bool
    is_italic: bool
    is_underline: bool
    is_strikeout: bool
    item_type: str
    mcid: int | None                 # marked-content ID for tagged PDFs (None otherwise)

class StructureElement:              # extract_structure_elements
    page: int                        # 1-indexed (matches TextItem.page)
    mcid: int
    role: str                        # "H1".."H6", "P", "Table", ... (resolved via /RoleMap)

class RegionText:                    # extract_text_in_regions
    text: str
    needs_ocr: bool
    ocr_reason: str | None           # machine-readable OCR reason

class PageRegionTexts:               # extract_text_in_regions
    page: int                        # 0-indexed
    regions: list[RegionText]

class PagesExtractionResult:         # extract_pages_markdown
    pages: list[PageMarkdown]        # PageMarkdown: page (0-indexed), markdown, needs_ocr, ocr_reason
    pages_with_tables: list[int]     # 1-indexed
    pages_with_columns: list[int]    # 1-indexed
    pages_needing_ocr: list[int]     # 1-indexed
    ocr_reasons_by_page: list[PageOcrReasons]
    is_complex: bool                 # any page has tables or multi-column layout

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pdf_inspector-1.15.0.tar.gz (1.7 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pdf_inspector-1.15.0-cp38-abi3-win_amd64.whl (4.7 MB view details)

Uploaded CPython 3.8+Windows x86-64

pdf_inspector-1.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (5.3 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ x86-64

pdf_inspector-1.15.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (5.1 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.17+ ARM64

pdf_inspector-1.15.0-cp38-abi3-macosx_11_0_arm64.whl (4.7 MB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

pdf_inspector-1.15.0-cp38-abi3-macosx_10_12_x86_64.whl (4.8 MB view details)

Uploaded CPython 3.8+macOS 10.12+ x86-64

File details

Details for the file pdf_inspector-1.15.0.tar.gz.

File metadata

  • Download URL: pdf_inspector-1.15.0.tar.gz
  • Upload date:
  • Size: 1.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pdf_inspector-1.15.0.tar.gz
Algorithm Hash digest
SHA256 25e66cb313f8523660c63b738089a5dbf953864ec4ac5411f73fe11b3339a05c
MD5 e2cfb1c43a83a9569d135f51cf1c03bc
BLAKE2b-256 50cbd97a2c26b180976da5662db68120f3e5941ac1b9f6985440c02a0021a5b5

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_inspector-1.15.0.tar.gz:

Publisher: publish-pypi.yml on firecrawl/pdf-inspector

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pdf_inspector-1.15.0-cp38-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for pdf_inspector-1.15.0-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 518c94a7ea5ffa47154611150a70aaa9e3afe902d84c3c3b64619a62d2d26ee9
MD5 6580946c062799991ee9616cb13609a3
BLAKE2b-256 2ef8d07a932ccd8921ca65c53de6385117feec5efc1874f4e9d160b3db114f6b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_inspector-1.15.0-cp38-abi3-win_amd64.whl:

Publisher: publish-pypi.yml on firecrawl/pdf-inspector

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pdf_inspector-1.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for pdf_inspector-1.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 652170d3c2edc9ddacc4ff078100c56d7cbcdef731468f4afc65e134b1277c4c
MD5 759026b72ebd51aab3312e986827f0a0
BLAKE2b-256 62a4948a596715b81c9dc5d2614ebb097f7eb938963c5c9944cda6b86322a956

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_inspector-1.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish-pypi.yml on firecrawl/pdf-inspector

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pdf_inspector-1.15.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for pdf_inspector-1.15.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 bea9003d9be2315377697869010480c89f0754b485e6eb91fe77ff697beb4049
MD5 a132de748df8b78f7f495644440e8714
BLAKE2b-256 eab8bcdba2c42533887b7a5af61e8e7d2ab2895ae6c384ece4d8a54deee8b9a9

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_inspector-1.15.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: publish-pypi.yml on firecrawl/pdf-inspector

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pdf_inspector-1.15.0-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pdf_inspector-1.15.0-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 b5cc0f8f482200bb1d0bae8809fc72a36bb2113b8ff39b8903b9cf9ef9086e93
MD5 410675684e667b4b47969b59c6cdbe0c
BLAKE2b-256 79866abb83408bba99c36c34481f17fd04767160449a342b5fcde0bd0e8a4d34

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_inspector-1.15.0-cp38-abi3-macosx_11_0_arm64.whl:

Publisher: publish-pypi.yml on firecrawl/pdf-inspector

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pdf_inspector-1.15.0-cp38-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pdf_inspector-1.15.0-cp38-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 b282814c5a86b0a4bfaec898f0d2bc914cf58e36bef93d7700e0ba52d698d8e2
MD5 f21956ed98255b426e69a435132d5b81
BLAKE2b-256 c723168814115d8a32edec4963ef288697694f10f2eacf1f013ad77a7baab240

See more details on using hashes here.

Provenance

The following attestation bundles were made for pdf_inspector-1.15.0-cp38-abi3-macosx_10_12_x86_64.whl:

Publisher: publish-pypi.yml on firecrawl/pdf-inspector

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page