firecrawl / firecrawl/pdf-inspector

Scanned/ImageBased early return skips extraction entirely — dense Tr 3 OCR layers behind full-page rasters are never read (measured: +66/96 on 4,114 PDFs, 0 regressions)

Open
#467 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
19.2k
Forks
1.3k
Avg merge
9h 21m
Merged PRs (30d)
51

Description

## Summary

`process_document` returns early for `PdfType::Scanned | PdfType::ImageBased` ("nothing to extract", currently `src/lib.rs` ~4144) *before* the extraction step and its invisible-text retry. But the classifier regularly assigns `ImageBased` (and sometimes `Scanned`) to re-OCR'ed scans whose full text is present as a dense invisible (Tr 3) layer — full-page rasters dominate the classification even when hundreds of text operators are present. For those documents the text layer is never read at all: the retry that exists specifically to recover Tr 3 OCR layers is unreachable.

This is the classification-side sibling of #385: there the layer is missed because a `TextBased` document skips the Mixed-only retry; here it is missed because extraction never runs.

## Proposal (measured)

1. Drop the early return; let extraction (with the invisible retry) run for `Scanned | ImageBased` too.
2. Make extraction failure non-fatal for `Scanned | ImageBased` (same treatment as `Mixed`), so malformed genuine scans still produce the current `markdown: None` result instead of an error.
3. Add a result-quality gate so genuine scans keep today's behavior: for `Scanned | ImageBased`, suppress the markdown when it carries fewer than `OCR_LAYER_MIN_ALNUM` (40) alphanumeric characters per page — reusing the existing crate norm for "a real OCR layer carries far more". `pages_needing_ocr` / `ocr_reasons_by_page` keep their detection values, so consumers still route those documents to OCR.

We evaluated a threshold-on-result gate against an operator pre-scan ("only extract when Tr 3 operators were seen") and found the result gate strictly simpler: one code path, no second detection mechanism that can disagree with the extractor, and it also catches layers that exist but are effectively empty.

## Measurements (A/B on 4,114 real-world PDFs, v0.2.0 baseline vs patched)

| | baseline | patched |
|---|---|---|
| successful extractions | 3,991 | 4,057 (+66) |
| "no extractable text" | 96 | 30 |
| broken regressions | — | 0 |
| output diffs among both-ok | — | 8, all strictly richer (e.g. a book cover page previously missed) |
| latency median / p95 | 6 ms / 69 ms | 5 ms / 69 ms |

The remaining 30 refusals are genuine: near-empty OCR layers (<50 alphanumerics per document), one double-text-layer document (broken PUA layer from a pre-OCR watermark — an upstream-of-us pipeline problem), and CJK-noise layers correctly rejected by `is_garbage_text`.

The false-positive guard held over the whole corpus: no genuine scan without a usable layer produced markdown (the gate at 40 alnum/page suppressed every junk recovery; without it there was exactly 1 false positive in 4,114).

We run this as a vendored patch in production; happy to turn it into a PR if the direction is acceptable.

Related: #385 (dense Tr 3 layers classified TextBased), and #466 — the three combine to the +66 figure above.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in src/lib.rs around process_document (~4144), reading the early return for Scanned and ImageBased, the extraction path, and its invisible-text retry. Trace the existing OCR_LAYER_MIN_ALNUM norm and the pages_needing_ocr/ocr_reasons_by_page values before changing behavior. Done means dense OCR layers are recovered, extraction failures remain non-fatal for these types, genuine scans still return markdown: None, and the reported measurements and zero-regression behavior are preserved.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
64/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.