OCR 功能需单独安装 PDFium 和 ONNX Runtime 库及模型文件,默认构建不含;Rust 与 CLI 需用 --features ocr 编译才启用 OCR;Python 和 Node 包虽内置 OCR 集成,但外部运行时仍需自行准备。
pdf-inspector
firecrawlMIT活跃维护用 Rust 写的高性能 PDF 解析库,能快速判断 PDF 是扫描件还是文本型并抽取文字,方便后续决定走 OCR 还是直接解析。
Fast Rust library for PDF inspection, classification, and text extraction. Intelligently detects scanned vs text-based PDFs to enable smart routing decisions.
pip install pdf-inspector
cargo add pdf-inspectorREADME 未说明显存或 GPU 要求;基准测试在 Apple M4 Pro 上完成。OCR 需另行安装 PDFium 与 ONNX Runtime 库,仅在页面被路由到 OCR 时加载。
Rust 写的 PDF 检测与文本提取库,快速判断扫描件还是文本件,省掉不必要的 OCR
用 Rust 写的高性能 PDF 解析库,能快速判断 PDF 是扫描件还是文本型并抽取文字,方便后续决定走 OCR 还是直接解析。
核心能力
- 采样内容流,10-50ms 内分类 TextBased/Scanned/ImageBased/Mixed 并给出置信度
- 按位置提取文本,保留字体、坐标、旋转角度与多栏阅读顺序
- 转 Markdown 支持标题分级、列表、代码块、表格、粗斜体与链接
- 双模式表格检测:绘图矩形加文本对齐启发式
- 可按页路由选择性 OCR,仅渲染缺文本的页面
容易踩的坑
- v1.25.21.25.2 — Script sizes and ToUnicode repair checks最新正式版2026-09-28 · 2026-09-28 07:14 UTC
Changes since 1.25.1:
- Size a super- or subscript run by its letters and digits: an exponent whose minus sign a symbol font sets a design size above its digits (as TeX does) keeps its
<sup>markup; a run of signs alone, or with signs more than a quarter larger than its letters and digits, is still sized by its largest glyph (#603). - Skip a subset font's ToUnicode repairs where the font itself shows the CMap is right: the repair through a
/CIDToGIDMapfor a CMap keyed by code, and the renumbering of a subset that kept its glyph indexes (#605).
No API changes.
Release preparation: #606.
- Size a super- or subscript run by its letters and digits: an exponent whose minus sign a symbol font sets a design size above its digits (as TeX does) keeps its
- v1.25.11.25.1 — Font decoding fixes2026-09-28 · 2026-09-28 07:14 UTC
查看更新说明
Changes since 1.25.0:
- Read simple fonts (Type1, TrueType, Type3) one byte per code when their ToUnicode CMap declares a two-byte codespace: text shown as a kerned run of short strings no longer loses every two-byte string (
Income Statementread asIometatent). The stale-CMap check for Type1 fonts judges such a CMap too, and the page detector counts such a font's text byte by byte (#595). - Read a Type1 font whose
/Encodingnames no base encoding through the built-in encoding of its embedded program (PDF 32000-1:2008, Table 114): TeX's fonts read their ligatures (efficiency, waseciency), curly quotes, dashes and math symbols. A code the program leaves at.notdefreads as nothing, the word space excepted, and the base-14 width fallback measures such fonts as they read (#596). - Take a repaired ToUnicode CMap for a string of one or two glyphs only on evidence beyond short common words:
engagementno longer reads asengageaent,ANDasANaorThreeasThaTe(#597).
No API changes.
Release preparation: #604.
- Read simple fonts (Type1, TrueType, Type3) one byte per code when their ToUnicode CMap declares a two-byte codespace: text shown as a kerned run of short strings no longer loses every two-byte string (
- v1.25.01.25.0 — Async positions and underline detection speed2026-09-28 · 2026-09-28 07:14 UTC
查看更新说明
Changes since 1.24.0:
- Node:
extractTextWithPositionsAsync, the async variant ofextractTextWithPositions: the same arguments and items, run on the libuv thread pool so a slow page no longer blocks the caller's event loop (#592). - Underline and strikeout detection no longer takes quadratic time on pages drawn from many thin filled rects or short strokes: a page of about 200,000 thin rects went from about 40 s to under 0.5 s, with the same marks (#592).
- The
linux-x64-gnuNode binary is cross-built for a GLIBC_2.17 floor again, so it loads on Amazon Linux 2023 (the managed AWS Lambda Node runtimes), RHEL 9 and Debian 11 (#586).
API: Node
extractTextWithPositionsAsync.Release preparation: #593.
- Node:
- v1.24.01.24.0 — Text paint and document information2026-09-23 · 2026-09-23 04:42 UTC
查看更新说明
Changes since 1.23.0:
- Report the paint each text run was shown with:
TextItemgainsfill_colorandstroke_color(8-bit sRGB, read from DeviceRGB, DeviceGray, DeviceCMYK, ICCBased by component count and Indexed palettes;Nonefor Separation, DeviceN, Pattern and CIE-based spaces) andrender_mode(theTrmode, so invisible text and clipping-only text can be told apart). Extraction and markdown are unchanged (#579). - Read the document information dictionary's
/Author,/Subject,/Keywords,/Creator,/Producer,/CreationDateand/ModDatebeside/Title, each decoded as a PDF text string (#579). - Decode the document
/Titleas a PDF text string: titles in PDFDocEncoding no longer read with U+FFFD, titles in UTF-16LE no longer read as mojibake, and titles given by reference are read (#579). - Extract text a page shows with the
"operator, which the page parser skipped together with the spacing and line move it makes (#579). - Take an ActualText span's weight from the paint of its first painted glyph rather than from the paint in force at the span's end (#579).
- Keep two lines of small type apart when their baselines lie less than 5 pt apart: a stacked table header at 4.7 pt on a 4.5 pt pitch no longer comes out as words zipped from both lines, and type of 8⅓ pt and above groups exactly as before (#580).
API:
TextItem::fill_color,stroke_colorandrender_mode(NodefillColor,strokeColor,renderMode; Pythonfill_color,stroke_color,render_mode; thepdf2md --items-jsonfields), and thePdfProcessResult/PdfTypeResultfieldsauthor,subject,keywords,creator,producer,creation_dateandmod_date(Node and WebAssembly in camelCase, Python as attributes, and thepdf2md --json/detect-pdf --jsonmembers). Rust literals of these types must include the new fields.Release preparation: #582.
- Report the paint each text run was shown with:
- v1.23.01.23.0 — Text-layer repairs2026-09-22 · 2026-09-22 01:26 UTC
查看更新说明
Changes since 1.22.1:
-
Read a Form XObject whose
/BBoxnumerals are too large for any parser: such numerals are saturated in place before the document is read, so the form's text is extracted and the OCR pipeline renders it instead of an empty page (#560). -
Read right-to-left runs shown in reading order forwards: a visible run of right-to-left letters painted forwards now votes for visual storage, so pages that came out word-mirrored read correctly; the logical-order reading is kept for invisible text layers (#561).
-
Read a re-encoded simple font by its
/Differencesnames when it kept a stale ToUnicode CMap, and keep one glyph's characters together through the visual-order read-back (#562). -
Compose a detached spacing accent with the letter it is painted over instead of leaving a stray accent a word on (#563).
-
Treat a return from a zero-advance dependent sign placed behind the pen as no word gap, so scripts that attach signs by stepping the pen back keep their words whole (#564).
-
Classify a page whose text layer is drawn invisibly under a covering image as a scan with the OCR reason
invisible_text_layer, following the text rendering mode, the transformation, the graphics state stack and image draws through form content within a per-page budget (#566). -
Interpolate single-glyph gaps in a ToUnicode CMap when the neighbouring codes make the reading unambiguous, mark the rest as U+FFFD instead of dropping them, and report each font's coverage (#568).
-
A ToUnicode entry whose destination is a control character is a miss, not text: the marker replaces it and the font program, the declared encoding or a blank glyph's advance recovers the letter where it can (#567).
-
pdf2mdanddetect-pdftake the PDF path as the first non-option argument, so flags may come before or after it, and a missing input names the path that could not be opened; the built-in CMaps are compiled into the binary instead of being read from the crate checkout at run time, socargo installbinaries and published wheels find them (PDF_INSPECTOR_BCMAPS_DIRstill overrides) (#570).
API:
PdfProcessResult::cmap_gaps(NodecmapGaps, Pythoncmap_gaps, WebAssemblycmapGaps, and thepdf2md --json/detect-pdf --analyze --jsonfield) lists, per font with gaps among the two-byte codes shown, the codes shown, interpolated and left unmapped (#568). The detector'sinvisible_text_layerOCR reason is new (#566).Release preparation: #571.
-
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
pdf-inspector
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.
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.