Files
pdf-inspector/site/index.html
T
Abimael Martell a910b7df1d docs(benchmark): refresh parser comparison (#178)
* docs: refresh benchmark comparison

* docs(site): refresh benchmark section

* docs: reframe benchmark positioning

* docs: focus benchmark positioning on best fit
2026-07-16 17:43:53 -07:00

428 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>pdf-inspector — PDF classification &amp; text extraction, no OCR</title>
<meta name="description" content="Fast Rust library that classifies PDFs (text-based vs scanned) and extracts clean Markdown — no OCR, no ML models. Bindings for Rust, Python, and Node.js.">
<meta property="og:title" content="pdf-inspector">
<meta property="og:description" content="Classify PDFs and extract clean Markdown in milliseconds. No OCR. No ML. Pure Rust.">
<meta property="og:type" content="website">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%F0%9F%93%84%3C/text%3E%3C/svg%3E">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,800&family=Hanken+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--paper: #f4efe4;
--paper-2: #eee7d8;
--ink: #1b1712;
--ink-soft: #4a433a;
--muted: #8b8375;
--line: #d9cfbb;
--accent: #dd3f22;
--accent-deep: #b32d15;
--card: #faf6ec;
--display: "Bricolage Grotesque", serif;
--body: "Hanken Grotesk", sans-serif;
--mono: "JetBrains Mono", monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--paper);
color: var(--ink);
font-family: var(--body);
font-size: 17px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
background-image:
radial-gradient(circle at 1px 1px, rgba(27,23,18,0.05) 1px, transparent 0);
background-size: 22px 22px;
}
::selection { background: var(--accent); color: var(--paper); }
a { color: inherit; text-decoration: none; }
.wrap { max-width: 1120px; margin: 0 auto; padding: 0 28px; }
/* ── nav ── */
nav {
position: sticky; top: 0; z-index: 50;
background: rgba(244,239,228,0.82);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.nav-in { display: flex; align-items: center; gap: 22px; height: 60px; }
.brand { font-family: var(--mono); font-weight: 700; font-size: 15px; letter-spacing: -0.02em; display: flex; align-items: center; gap: 9px; }
.brand .dot { width: 9px; height: 9px; background: var(--accent); border-radius: 50%; box-shadow: 0 0 0 3px rgba(221,63,34,0.18); }
.nav-links { margin-left: auto; display: flex; gap: 24px; align-items: center; font-size: 14.5px; font-weight: 500; }
.nav-links a { color: var(--ink-soft); transition: color .15s; }
.nav-links a:hover { color: var(--accent); }
.nav-gh { border: 1px solid var(--ink); border-radius: 999px; padding: 6px 15px; color: var(--ink) !important; transition: all .15s; }
.nav-gh:hover { background: var(--ink); color: var(--paper) !important; }
@media (max-width: 680px) { .nav-links .hide-sm { display: none; } }
/* ── hero ── */
header { padding: 74px 0 40px; position: relative; }
.eyebrow { font-family: var(--mono); font-size: 12.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent-deep); margin-bottom: 22px; }
h1 {
font-family: var(--display);
font-weight: 800;
font-size: clamp(2.9rem, 8vw, 6.1rem);
line-height: 0.96;
letter-spacing: -0.035em;
max-width: 15ch;
}
h1 .em { color: var(--accent); font-style: normal; position: relative; }
h1 .strike { position: relative; white-space: nowrap; }
h1 .strike::after { content: ""; position: absolute; left: -2%; right: -2%; top: 54%; height: 0.09em; background: var(--accent); transform: rotate(-3deg); }
.lede { margin-top: 28px; font-size: clamp(1.05rem, 2.2vw, 1.32rem); color: var(--ink-soft); max-width: 46ch; line-height: 1.5; }
.lede b { color: var(--ink); font-weight: 600; }
.hero-grid { display: grid; grid-template-columns: 1.15fr 0.85fr; gap: 48px; align-items: end; }
@media (max-width: 880px) { .hero-grid { grid-template-columns: 1fr; gap: 40px; } }
/* readout card */
.readout {
background: var(--ink); color: var(--paper);
border-radius: 14px; padding: 22px 22px 20px;
font-family: var(--mono); font-size: 13px;
box-shadow: 14px 14px 0 rgba(27,23,18,0.09);
position: relative;
}
.readout .rlabel { color: #b8ad98; font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; margin-bottom: 16px; display: flex; justify-content: space-between; }
.readout .rrow { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-top: 1px solid rgba(255,255,255,0.09); }
.readout .rrow:first-of-type { border-top: none; }
.readout .k { color: #cfc6b3; }
.readout .v { font-weight: 700; }
.readout .v.hot { color: var(--accent); }
.bar { height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin-top: 3px; width: 96px; }
.bar > i { display: block; height: 100%; background: var(--accent); border-radius: 3px; }
/* ── install row ── */
.installs { display: grid; grid-template-columns: repeat(3,1fr); gap: 14px; margin-top: 54px; }
@media (max-width: 720px) { .installs { grid-template-columns: 1fr; } }
.inst {
background: var(--card); border: 1px solid var(--line); border-radius: 11px;
padding: 15px 17px; transition: transform .16s, border-color .16s, box-shadow .16s;
cursor: pointer;
}
.inst:hover { transform: translateY(-3px); border-color: var(--accent); box-shadow: 0 8px 22px rgba(27,23,18,0.07); }
.inst .reg { font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; display: flex; justify-content: space-between; }
.inst code { font-family: var(--mono); font-size: 14px; color: var(--ink); font-weight: 500; }
.inst .arrow { color: var(--accent); opacity: 0; transition: opacity .16s; }
.inst:hover .arrow { opacity: 1; }
/* ── section scaffold ── */
section { padding: 66px 0; border-top: 1px solid var(--line); }
.sec-head { display: flex; align-items: baseline; gap: 16px; margin-bottom: 40px; flex-wrap: wrap; }
.sec-num { font-family: var(--mono); font-size: 13px; color: var(--accent); font-weight: 700; }
.sec-title { font-family: var(--display); font-weight: 600; font-size: clamp(1.7rem, 4vw, 2.6rem); letter-spacing: -0.025em; }
.sec-sub { color: var(--ink-soft); max-width: 52ch; font-size: 1.02rem; }
/* ── features ── */
.feat-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 14px; overflow: hidden; }
@media (max-width: 900px) { .feat-grid { grid-template-columns: repeat(2,1fr); } }
@media (max-width: 560px) { .feat-grid { grid-template-columns: 1fr; } }
.feat { background: var(--card); padding: 24px 22px; transition: background .16s; }
.feat:hover { background: #fff; }
.feat .fn { font-family: var(--mono); font-size: 12px; color: var(--accent); font-weight: 700; }
.feat h3 { font-family: var(--display); font-weight: 600; font-size: 1.16rem; margin: 12px 0 8px; letter-spacing: -0.01em; }
.feat p { font-size: 14.5px; color: var(--ink-soft); line-height: 1.5; }
/* ── benchmark ── */
.bench {
border: 1px solid var(--line); border-radius: 14px; overflow: hidden;
background: var(--card);
}
table { width: 100%; border-collapse: collapse; font-size: 15px; }
thead th { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); text-align: right; padding: 15px 18px; background: var(--paper-2); border-bottom: 1px solid var(--line); font-weight: 500; }
thead th:first-child { text-align: left; }
tbody td { padding: 14px 18px; text-align: right; font-family: var(--mono); border-bottom: 1px solid var(--line); }
tbody td:first-child { text-align: left; font-family: var(--body); font-weight: 500; }
tbody tr:last-child td { border-bottom: none; }
tbody tr.us { background: rgba(221,63,34,0.06); }
tbody tr.us td:first-child { color: var(--accent-deep); font-weight: 700; }
tbody tr.us td:first-child::before { content: "▸ "; color: var(--accent); }
.bench-foot { padding: 15px 18px; font-size: 13.5px; color: var(--ink-soft); background: var(--paper-2); border-top: 1px solid var(--line); }
.bench-wrap { overflow-x: auto; }
.callouts { display: grid; grid-template-columns: 1fr; gap: 16px; margin-top: 22px; }
.callout { border-left: 3px solid var(--accent); padding: 4px 0 4px 16px; }
.callout .ct { font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-deep); margin-bottom: 5px; }
.callout p { font-size: 14.5px; color: var(--ink-soft); }
/* ── quickstart tabs ── */
.tabs input { position: absolute; opacity: 0; pointer-events: none; }
.tablist { display: flex; gap: 6px; margin-bottom: 0; }
.tablist label {
font-family: var(--mono); font-size: 13px; font-weight: 500;
padding: 10px 18px; cursor: pointer; color: var(--muted);
border: 1px solid var(--line); border-bottom: none;
border-radius: 9px 9px 0 0; background: var(--paper-2); transition: all .15s;
}
.tablist label:hover { color: var(--ink); }
.panel { display: none; }
.code {
background: var(--ink); border-radius: 0 12px 12px 12px;
padding: 22px 24px; overflow-x: auto;
font-family: var(--mono); font-size: 13.5px; line-height: 1.7;
color: #e9e2d3;
box-shadow: 12px 12px 0 rgba(27,23,18,0.07);
}
.code .cm { color: #8a8069; }
.code .kw { color: #ff9f7a; }
.code .st { color: #cbb78a; }
.code .fn { color: #f4efe4; font-weight: 700; }
#t-rust:checked ~ .tablist label[for=t-rust],
#t-py:checked ~ .tablist label[for=t-py],
#t-node:checked ~ .tablist label[for=t-node],
#t-cli:checked ~ .tablist label[for=t-cli] {
background: var(--ink); color: var(--paper); border-color: var(--ink);
}
#t-rust:checked ~ .panels #p-rust,
#t-py:checked ~ .panels #p-py,
#t-node:checked ~ .panels #p-node,
#t-cli:checked ~ .panels #p-cli { display: block; }
.code a.ref { color: #ff9f7a; border-bottom: 1px dotted #ff9f7a; }
/* ── closing split (OSS vs hosted) ── */
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
@media (max-width: 780px) { .split { grid-template-columns: 1fr; } }
.path { border: 1px solid var(--line); border-radius: 16px; padding: 32px 30px; background: var(--card); display: flex; flex-direction: column; }
.path .ptag { font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); margin-bottom: 15px; }
.path h3 { font-family: var(--display); font-weight: 600; font-size: 1.5rem; letter-spacing: -0.02em; line-height: 1.05; margin-bottom: 12px; }
.path p { color: var(--ink-soft); font-size: 15px; line-height: 1.5; flex: 1; margin-bottom: 24px; }
.pbtns { display: flex; gap: 12px; flex-wrap: wrap; }
.path-pro { background: var(--ink); border-color: var(--ink); box-shadow: 14px 14px 0 rgba(27,23,18,0.09); }
.path-pro .ptag { color: #b8ad98; }
.path-pro .ptag b { color: var(--accent); font-weight: 700; }
.path-pro h3 { color: var(--paper); }
.path-pro p { color: #cfc6b3; }
.btn { font-family: var(--mono); font-size: 14px; font-weight: 500; padding: 13px 24px; border-radius: 999px; transition: all .15s; border: 1px solid var(--ink); }
.btn-p { background: var(--accent); border-color: var(--accent); color: var(--paper); }
.btn-p:hover { background: var(--accent-deep); border-color: var(--accent-deep); }
.btn-s:hover { background: var(--ink); color: var(--paper); }
.btn-pro { background: var(--accent); border-color: var(--accent); color: var(--paper); }
.btn-pro:hover { background: var(--accent-deep); border-color: var(--accent-deep); }
.btn-ghost { color: var(--paper); border-color: rgba(255,255,255,0.3); }
.btn-ghost:hover { border-color: var(--paper); background: rgba(255,255,255,0.08); }
.fc-mark { height: 34px; width: auto; display: block; margin-bottom: 20px; }
.fc-wordmark { height: 15px; width: auto; vertical-align: -2px; transition: opacity .15s; }
.fc-wordmark:hover { opacity: 0.65; }
footer { border-top: 1px solid var(--line); padding: 34px 0; font-size: 14px; color: var(--muted); }
.foot-in { display: flex; justify-content: space-between; align-items: center; gap: 18px; flex-wrap: wrap; }
.foot-in a { color: var(--ink-soft); }
.foot-in a:hover { color: var(--accent); }
.foot-links { display: flex; gap: 20px; font-family: var(--mono); font-size: 13px; }
/* ── load animation ── */
.reveal { opacity: 0; transform: translateY(14px); animation: rise .7s cubic-bezier(.2,.7,.3,1) forwards; }
@keyframes rise { to { opacity: 1; transform: none; } }
.d1 { animation-delay: .05s; } .d2 { animation-delay: .15s; } .d3 { animation-delay: .25s; }
.d4 { animation-delay: .35s; } .d5 { animation-delay: .45s; } .d6 { animation-delay: .55s; }
@media (prefers-reduced-motion: reduce) { .reveal { animation: none; opacity: 1; transform: none; } }
</style>
</head>
<body>
<nav>
<div class="wrap nav-in">
<a href="#top" class="brand"><span class="dot"></span>pdf-inspector</a>
<div class="nav-links">
<a href="#features" class="hide-sm">Features</a>
<a href="#benchmark" class="hide-sm">Benchmark</a>
<a href="#start">Quick start</a>
<a class="nav-gh" href="https://github.com/firecrawl/pdf-inspector">GitHub ↗</a>
</div>
</div>
</nav>
<header id="top">
<div class="wrap hero-grid">
<div>
<div class="eyebrow reveal d1">Rust · Python · Node · CLI</div>
<h1 class="reveal d2">Classify PDFs. Extract Markdown. <span class="strike">No OCR.</span></h1>
<p class="lede reveal d3">A fast Rust library that tells text-based PDFs from scanned ones, then extracts position-aware text and clean Markdown — <b>locally, in milliseconds</b>. Skip the OCR bill for the ~54% of PDFs that never needed it.</p>
</div>
<div class="readout reveal d4" aria-hidden="true">
<div class="rlabel"><span>classify_pdf()</span><span>~12ms</span></div>
<div class="rrow"><span class="k">type</span><span class="v hot">TextBased</span></div>
<div class="rrow"><span class="k">confidence</span><span class="v">0.98</span></div>
<div class="rrow"><span class="k">needs_ocr</span><span class="v">false</span></div>
<div class="rrow"><span class="k">route</span><span class="v">local&nbsp;&nbsp;md</span></div>
<div class="rrow" style="border-top:1px solid rgba(255,255,255,.09);padding-top:13px">
<span class="k">signal</span>
<span class="bar"><i style="width:98%"></i></span>
</div>
</div>
</div>
<div class="wrap">
<div class="installs">
<a class="inst reveal d4" href="https://crates.io/crates/pdf-inspector">
<div class="reg"><span>crates.io</span><span class="arrow"></span></div>
<code>cargo add pdf-inspector</code>
</a>
<a class="inst reveal d5" href="https://pypi.org/project/pdf-inspector/">
<div class="reg"><span>PyPI</span><span class="arrow"></span></div>
<code>pip install pdf-inspector</code>
</a>
<a class="inst reveal d6" href="https://www.npmjs.com/package/@firecrawl/pdf-inspector">
<div class="reg"><span>npm</span><span class="arrow"></span></div>
<code>npm i @firecrawl/pdf-inspector</code>
</a>
</div>
</div>
</header>
<section id="features">
<div class="wrap">
<div class="sec-head">
<span class="sec-num">01</span>
<h2 class="sec-title">Built for routing, not just reading</h2>
</div>
<div class="feat-grid">
<div class="feat"><div class="fn">01</div><h3>Smart classification</h3><p>TextBased, Scanned, ImageBased, or Mixed in ~1050ms by sampling content streams. Returns a confidence score and per-page OCR routing.</p></div>
<div class="feat"><div class="fn">02</div><h3>Position-aware text</h3><p>Extraction with font info, X/Y coordinates, and automatic multi-column reading order.</p></div>
<div class="feat"><div class="fn">03</div><h3>Markdown conversion</h3><p>Headings, bullet/numbered lists, code blocks, tables, bold/italic, URL linking, and page breaks.</p></div>
<div class="feat"><div class="fn">04</div><h3>Table detection</h3><p>Rectangle-based detection from drawing ops plus heuristic alignment detection. Financial tables, footnotes, and cross-page continuations.</p></div>
<div class="feat"><div class="fn">05</div><h3>CID font support</h3><p>ToUnicode CMap decoding for Type0/Identity-H fonts, with UTF-16BE, UTF-8, and Latin-1 encodings.</p></div>
<div class="feat"><div class="fn">06</div><h3>Multi-column layout</h3><p>Newspaper-style column detection, sequential reading order, and right-to-left text support.</p></div>
<div class="feat"><div class="fn">07</div><h3>Encoding checks</h3><p>Flags broken font encodings automatically so callers can fall back to OCR only when it's actually needed.</p></div>
<div class="feat"><div class="fn">08</div><h3>Lightweight</h3><p>Pure Rust. No ML models, no external services. A single parse shared between detection and extraction.</p></div>
</div>
</div>
</section>
<section id="benchmark">
<div class="wrap">
<div class="sec-head">
<span class="sec-num">02</span>
<h2 class="sec-title">Fastest of the direct-text engines</h2>
<p class="sec-sub">Evaluated on the <a href="https://github.com/opendataloader-project/opendataloader-bench" style="color:var(--accent-deep);border-bottom:1px solid var(--line)">opendataloader-bench</a> corpus (200 PDFs). Local engines without model-based PDF parsing; OCR disabled. Higher is better.</p>
</div>
<div class="bench">
<div class="bench-wrap">
<table>
<thead>
<tr><th>Engine</th><th>Overall</th><th>Reading order</th><th>Tables</th><th>Headings</th><th>200 docs</th></tr>
</thead>
<tbody>
<tr class="us"><td>pdf-inspector</td><td>0.875</td><td>0.915</td><td>0.814</td><td>0.788</td><td>2.8s</td></tr>
<tr><td>liteparse</td><td>0.870</td><td>0.908</td><td>0.693</td><td>0.811</td><td>13.9s</td></tr>
<tr><td>opendataloader</td><td>0.843</td><td>0.912</td><td>0.489</td><td>0.760</td><td>9.8s</td></tr>
<tr><td>pymupdf4llm</td><td>0.735</td><td>0.886</td><td>0.401</td><td>0.424</td><td>15.5s</td></tr>
<tr><td>markitdown</td><td>0.583</td><td>0.879</td><td>0.000</td><td>0.000</td><td>6.7s</td></tr>
</tbody>
</table>
</div>
<div class="bench-foot">Refreshed July 16, 2026, on Apple M4 Pro. Speed is the median of three complete corpus runs.</div>
</div>
<div class="callouts">
<div class="callout"><div class="ct">Best fit</div><p>Native-text PDFs where speed, reading order, and table structure matter. pdf-inspector delivered the highest overall, reading-order, and table scores, along with the fastest complete run in this benchmark. 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.</p></div>
</div>
</div>
</section>
<section id="start">
<div class="wrap">
<div class="sec-head">
<span class="sec-num">03</span>
<h2 class="sec-title">Three lines to Markdown</h2>
</div>
<div class="tabs">
<input type="radio" name="tab" id="t-rust" checked>
<input type="radio" name="tab" id="t-py">
<input type="radio" name="tab" id="t-node">
<input type="radio" name="tab" id="t-cli">
<div class="tablist">
<label for="t-rust">Rust</label>
<label for="t-py">Python</label>
<label for="t-node">Node.js</label>
<label for="t-cli">CLI</label>
</div>
<div class="panels">
<div class="panel" id="p-rust"><pre class="code"><span class="kw">use</span> pdf_inspector::process_pdf;
<span class="kw">let</span> result = <span class="fn">process_pdf</span>(<span class="st">"document.pdf"</span>)?;
<span class="fn">println!</span>(<span class="st">"Type: {:?}"</span>, result.pdf_type);
<span class="kw">if let</span> <span class="kw">Some</span>(markdown) = &amp;result.markdown {
<span class="fn">println!</span>(<span class="st">"{}"</span>, markdown);
}
<span class="cm">// full reference → </span><a class="ref" href="https://github.com/firecrawl/pdf-inspector/blob/main/docs/rust-api.md">docs/rust-api.md</a></pre></div>
<div class="panel" id="p-py"><pre class="code"><span class="kw">import</span> pdf_inspector
result = pdf_inspector.<span class="fn">process_pdf</span>(<span class="st">"document.pdf"</span>)
<span class="fn">print</span>(result.pdf_type) <span class="cm"># "text_based" | "scanned" | "image_based" | "mixed"</span>
<span class="fn">print</span>(result.markdown) <span class="cm"># Markdown string or None</span>
<span class="cm"># full reference → </span><a class="ref" href="https://github.com/firecrawl/pdf-inspector/blob/main/docs/python.md">docs/python.md</a></pre></div>
<div class="panel" id="p-node"><pre class="code"><span class="kw">import</span> { readFileSync } <span class="kw">from</span> <span class="st">'fs'</span>;
<span class="kw">import</span> { processPdf } <span class="kw">from</span> <span class="st">'@firecrawl/pdf-inspector'</span>;
<span class="kw">const</span> result = <span class="fn">processPdf</span>(<span class="fn">readFileSync</span>(<span class="st">'document.pdf'</span>));
console.<span class="fn">log</span>(result.pdfType); <span class="cm">// "TextBased" | "Scanned" | ...</span>
console.<span class="fn">log</span>(result.markdown); <span class="cm">// Markdown string or null</span>
<span class="cm">// full reference → </span><a class="ref" href="https://github.com/firecrawl/pdf-inspector/blob/main/napi/README.md">napi/README.md</a></pre></div>
<div class="panel" id="p-cli"><pre class="code"><span class="cm"># install the CLI tools</span>
cargo <span class="fn">install</span> pdf-inspector
<span class="cm"># convert a PDF to Markdown</span>
<span class="fn">pdf2md</span> document.pdf
<span class="cm"># classify only — is it scanned?</span>
<span class="fn">detect-pdf</span> document.pdf --analyze --json
<span class="cm"># structured output for pipelines</span>
<span class="fn">pdf2md</span> document.pdf --json</pre></div>
</div>
</div>
</div>
</section>
<section class="close">
<div class="wrap">
<div class="sec-head">
<span class="sec-num">04</span>
<h2 class="sec-title">Two ways to parse</h2>
<p class="sec-sub">Run the classifier locally for text-based PDFs; hand the scanned, OCR, and at-scale work to Firecrawl.</p>
</div>
<div class="split">
<div class="path">
<div class="ptag">Open source · runs local</div>
<h3>Use pdf-inspector yourself</h3>
<p>Pure-Rust library and CLI. Classify and extract text-based PDFs on your own machine in milliseconds — no external calls, no OCR bill, MIT licensed.</p>
<div class="pbtns">
<a class="btn btn-p" href="https://github.com/firecrawl/pdf-inspector">Get started</a>
<a class="btn btn-s" href="https://crates.io/crates/pdf-inspector">crates.io</a>
</div>
</div>
<div class="path path-pro">
<img class="fc-mark" src="assets/firecrawl-mark.svg" alt="Firecrawl" width="24" height="34">
<div class="ptag"><b>Firecrawl Parse</b> · hosted API</div>
<h3>Or let Firecrawl handle the hard ones</h3>
<p>Scanned documents, OCR, DOCX / XLSX / HTML, and parsing at scale — clean, LLM-ready Markdown from one API call. The downstream route for everything local parsing can't reach.</p>
<div class="pbtns">
<a class="btn btn-pro" href="https://docs.firecrawl.dev/api-reference/endpoint/parse">Firecrawl Parse ↗</a>
<a class="btn btn-ghost" href="https://firecrawl.dev">firecrawl.dev</a>
</div>
</div>
</div>
</div>
</section>
<footer>
<div class="wrap foot-in">
<div style="display:flex;align-items:center;gap:7px">Built by <a href="https://firecrawl.dev"><img class="fc-wordmark" src="assets/firecrawl-wordmark.svg" alt="Firecrawl"></a> · MIT licensed</div>
<div class="foot-links">
<a href="https://github.com/firecrawl/pdf-inspector">GitHub</a>
<a href="https://crates.io/crates/pdf-inspector">crates.io</a>
<a href="https://pypi.org/project/pdf-inspector/">PyPI</a>
<a href="https://www.npmjs.com/package/@firecrawl/pdf-inspector">npm</a>
</div>
</div>
</footer>
</body>
</html>