fix: simplify region text extraction to trust layout model ordering
Publish npm package / Build aarch64-apple-darwin (push) Has been cancelled
Publish npm package / Build x86_64-unknown-linux-gnu (push) Has been cancelled
Publish npm package / Publish to npm (push) Has been cancelled

When fire-pdf sends pre-segmented bboxes from the layout model,
pdf-inspector no longer runs column detection, stream-order heuristics,
or newspaper/tabular mode detection within the region. These heuristics
conflict with the layout model's decisions and cause wrong reading order.

Region extraction now simply: Y-sorts items, groups into lines, and
sorts within each line by X position. The heavy heuristics remain
available for standalone full-page extraction.

Eval showed pure OCR (0.2875 NED) beating native+heuristics (0.2916)
across all categories, especially multi-column (-0.08) and newspaper
(-0.16). This change should close that gap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-04-04 21:58:05 -07:00
co-authored by Claude Opus 4.6
parent 0db9863919
commit 6a9ff170dc
3 changed files with 34 additions and 27 deletions
+1 -19
View File
@@ -129,12 +129,6 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytecount"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]] [[package]]
name = "cbc" name = "cbc"
version = "0.1.2" version = "0.1.2"
@@ -679,7 +673,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "lopdf" name = "lopdf"
version = "0.40.0" version = "0.40.0"
source = "git+https://github.com/J-F-Liu/lopdf?rev=052674053814a9f4897af94f0b8e46a545c9b329#052674053814a9f4897af94f0b8e46a545c9b329" source = "git+https://github.com/J-F-Liu/lopdf?rev=7a05512d831415b1f2b1ce522391d6beab8a1284#7a05512d831415b1f2b1ce522391d6beab8a1284"
dependencies = [ dependencies = [
"aes", "aes",
"bitflags", "bitflags",
@@ -695,7 +689,6 @@ dependencies = [
"log", "log",
"md-5", "md-5",
"nom", "nom",
"nom_locate",
"rand", "rand",
"rangemap", "rangemap",
"rayon", "rayon",
@@ -807,17 +800,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "nom_locate"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
dependencies = [
"bytecount",
"memchr",
"nom",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.1" version = "0.2.1"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "firecrawl-pdf-inspector", "name": "firecrawl-pdf-inspector",
"version": "0.3.3", "version": "0.3.4",
"description": "Fast PDF classification and text extraction. Detect text-based vs scanned PDFs, extract text by region with quality checks. Native Rust performance via napi-rs.", "description": "Fast PDF classification and text extraction. Detect text-based vs scanned PDFs, extract text by region with quality checks. Native Rust performance via napi-rs.",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
+32 -7
View File
@@ -525,9 +525,6 @@ fn collect_text_in_region_with_options(
adaptive_threshold: f32, adaptive_threshold: f32,
) -> String { ) -> String {
let bounds = region_bounds(rx1, ry1, rx2, ry2, page_height, coord_space); let bounds = region_bounds(rx1, ry1, rx2, ry2, page_height, coord_space);
let Some(page) = items.first().map(|item| item.page) else {
return String::new();
};
let matched: Vec<TextItem> = items let matched: Vec<TextItem> = items
.iter() .iter()
.filter(|item| region_overlaps_item(item, bounds)) .filter(|item| region_overlaps_item(item, bounds))
@@ -536,11 +533,39 @@ fn collect_text_in_region_with_options(
if matched.is_empty() { if matched.is_empty() {
return String::new(); return String::new();
} }
let mut thresholds = HashMap::new();
if adaptive_threshold > 0.10 { // Simple extraction: the caller (fire-pdf) already handles reading order
thresholds.insert(page, adaptive_threshold); // and column splitting via the layout model. We just need to sort items
// top-to-bottom, left-to-right and group into lines.
let mut sorted = matched;
sorted.sort_by(|a, b| b.y.total_cmp(&a.y).then(a.x.total_cmp(&b.x)));
let y_tolerance = 3.0;
let mut lines: Vec<extractor::TextLine> = Vec::new();
for item in sorted {
let should_merge = lines.last().is_some_and(|last_line: &extractor::TextLine| {
last_line.page == item.page && (last_line.y - item.y).abs() < y_tolerance
});
if should_merge {
lines.last_mut().unwrap().items.push(item);
} else {
let y = item.y;
let page = item.page;
lines.push(extractor::TextLine {
items: vec![item],
y,
page,
adaptive_threshold,
});
}
} }
let lines = extractor::group_into_lines_with_thresholds(matched, &thresholds, &HashSet::new());
// Sort items within each line by X position
for line in &mut lines {
text_utils::sort_line_items(&mut line.items);
}
lines lines
.into_iter() .into_iter()
.map(|line| line.text()) .map(|line| line.text())