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 classification — text_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 OCR — auto 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. x/y are PDF points relative to the
# visible page box (CropBox ∩ MediaBox), origin at its lower-left corner, y up.
# extract_text_in_regions uses the same box from its top-left corner (y down).
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}")

# Pages whose text is predominantly rotated are re-based so it reads
# left-to-right; ask for the frame of each such page alongside the items
positioned = pdf_inspector.extract_text_with_positions_and_rotations("document.pdf")
for frame in positioned.page_rotations:
    print(f"page {frame.page} turned {frame.rotation}")

# 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, bold_from_weight=False, bold_weight_threshold=600) Text with X/Y coords (visible-page-box frame) and font info; bold_from_weight=True also reads is_bold from a weight class of bold_weight_threshold (100..900) or more and merges runs by that verdict
extract_text_with_positions_bytes(data, pages=None, bold_from_weight=False, bold_weight_threshold=600) Text with positions from bytes
extract_text_with_positions_and_rotations(path, bold_from_weight=False, bold_weight_threshold=600) Positioned text plus the frame of every page whose text was turned (PositionedText)
extract_text_with_positions_and_rotations_bytes(data, bold_from_weight=False, bold_weight_threshold=600) The same from bytes
extract_text_in_regions(path, page_regions, bold_from_weight=False, bold_weight_threshold=600) Extract text in bounding-box regions (top-left PDF points in the visible page box)
extract_text_in_regions_bytes(data, page_regions, bold_from_weight=False, bold_weight_threshold=600) 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
    cmap_gaps: list[FontCMapGaps]    # fonts whose ToUnicode CMap (or, without one, the program's cmap table)
                                     # lacked an entry for a code shown through it; always [] for detect_pdf

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

class FontCMapGaps:                  # what became of a font's codes its ToUnicode CMap (or cmap table) lacks
    font: str                        # /BaseFont name, or the resource name without one
    codes: int                       # codes shown through the CMap (two-byte, or the bytes of a single-byte CMap)
    interpolated: int                # read from the mapped codes around them
    unmapped: int                    # could not be read; each is a U+FFFD in the text

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                         # PDF points from the visible page box's lower-left corner
    y: float                         # (CropBox ∩ MediaBox, else MediaBox); y grows upward
    width: float
    height: float                    # axis-aligned box; y is the baseline for horizontal text
    rotation: float                  # baseline angle in degrees CCW: 0 horizontal, 90 bottom-to-top, 270 top-to-bottom
    advance_known: bool              # False when the font has no width metrics or an ActualText advance could not be recovered: the extent along the baseline is then estimated
    font: str
    font_size: float
    page: int
    is_bold: bool
    is_italic: bool
    font_weight: int | None          # weight class 100..900 (400 regular, 700 bold) from the embedded font's OS/2 table, /FontWeight or a weight word in the name; None when unknown
    bold_source: str | None          # where is_bold came from: "font_name", "font_flags", "weight_class" (with bold_from_weight) or "painted"; None when not bold
    fixed_pitch: bool | None         # True when the FixedPitch flag or the embedded program says so, else measured from the width table (a dozen glyphs sharing one advance: True; two differing: False; neither: None)
    is_underline: bool
    is_strikeout: bool
    baseline_shift: float            # super/subscript offset from the body baseline (0.0 = normal text; >0 raised, <0 lowered)
    item_type: str
    mcid: int | None                 # marked-content ID for tagged PDFs (None otherwise)

class PageRotation:                  # extract_text_with_positions_and_rotations
    page: int                        # 1-indexed (matches TextItem.page)
    rotation: Literal["ccw", "cw"]   # how the page's frame was turned so its text reads left-to-right

class PositionedText:                # extract_text_with_positions_and_rotations
    items: list[TextItem]            # on a page listed below, in that page's turned frame
    page_rotations: list[PageRotation]

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

Release files for pdf-inspector 1.23.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pdf-inspector 1.23.0
File Size Uploaded
pdf_inspector-1.23.0.tar.gz 2.1 MB Details

Built distributions (wheels)

Table of built distributions (wheels) for pdf-inspector 1.23.0
File
pdf_inspector-1.23.0-cp38-abi3-win_amd64.whl CPython 3.8 abi3 Windows x86-64 Details
pdf_inspector-1.23.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.8 abi3 Linux glibc 2.17+ x86-64 Details
pdf_inspector-1.23.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.8 abi3 Linux glibc 2.17+ ARM64 Details
pdf_inspector-1.23.0-cp38-abi3-macosx_11_0_arm64.whl CPython 3.8 abi3 macOS 11.0+ ARM64 Details
pdf_inspector-1.23.0-cp38-abi3-macosx_10_12_x86_64.whl CPython 3.8 abi3 macOS 10.12+ x86-64 Details

Total release size: 34.0 MB

Release files / pdf_inspector-1.23.0.tar.gz

Download URL pdf_inspector-1.23.0.tar.gz
Size 2.1 MB
Tags Source
SHA-256 checksum
How to use checksums
40892c0c5141947947a2ce217bb88fd7b7ad6f9f7038909289243277640e5014
BLAKE2b-256 checksum
How to use checksums
5708979e1d8d5f99e967389af7a7d33c98dadee57a09f8e6f6eb6a5a5f64f1bc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / pdf_inspector-1.23.0-cp38-abi3-win_amd64.whl

Download URL pdf_inspector-1.23.0-cp38-abi3-win_amd64.whl
Size 6.1 MB
Tags CPython 3.8 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
04da6d45603091cfd970f42c464ee5dd3b5756a00eb19a0c718b2ade8ee2503e
BLAKE2b-256 checksum
How to use checksums
a84b0dc2197bca6dc1739c5c8ea2f593d5214ecc6504cf545295a95269a1d092
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / pdf_inspector-1.23.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL pdf_inspector-1.23.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 6.8 MB
Tags CPython 3.8 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
d60ccd0c059fd5295887101c467c78630d0afbc77be76f0470be37db28ea332c
BLAKE2b-256 checksum
How to use checksums
96f30b05aeeff019ea6c891180bbb40edc8fe15d3041278e9dbfb408242eba13
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / pdf_inspector-1.23.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL pdf_inspector-1.23.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 6.6 MB
Tags CPython 3.8 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
30de7d2e2ffd8bedde4e5755306ed92342444e245d08d3d354c4355ae8f8e30c
BLAKE2b-256 checksum
How to use checksums
057fdf58ed175582033c4b8b81a0f442f18d3beeac66760c6d87207b9b3a2ce8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / pdf_inspector-1.23.0-cp38-abi3-macosx_11_0_arm64.whl

Download URL pdf_inspector-1.23.0-cp38-abi3-macosx_11_0_arm64.whl
Size 6.0 MB
Tags CPython 3.8 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
142e6aa376f7d8f4a8ff7484f94949ae170a48ae664a5e8232bc0bec082908aa
BLAKE2b-256 checksum
How to use checksums
b63274a6d11a163198fd99d9260fc1d9e997d311474029e03d03378302f0e26d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / pdf_inspector-1.23.0-cp38-abi3-macosx_10_12_x86_64.whl

Download URL pdf_inspector-1.23.0-cp38-abi3-macosx_10_12_x86_64.whl
Size 6.3 MB
Tags CPython 3.8 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
afc607413993872987723cc1a6c86266d0a90513b30c6e109defd8cb0063c63e
BLAKE2b-256 checksum
How to use checksums
0afeb7ca0e3f2a198fe497fd4e3e10e54ae5da316ad26ea746ea316beb83c048
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

1.25.0

6 release files

1.24.0

6 release files

This release

1.23.0 This release

6 release files

1.22.1

6 release files

1.22.0

6 release files

1.21.0

6 release files

1.20.0

6 release files

1.17.0

6 release files

1.16.0

6 release files

1.15.0

6 release files

1.14.2

6 release files

1.14.1

6 release files

1.14.0

6 release files

0.2.7

6 release files

0.2.6

6 release files

0.2.5

6 release files

0.2.4

6 release files

0.2.3

6 release files

0.2.2

6 release files

0.2.1

6 release files

0.2.0

6 release files

0.1.1

6 release files

0.1.0

6 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page