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"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytecount"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
[[package]]
name = "cbc"
version = "0.1.2"
@@ -679,7 +673,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lopdf"
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 = [
"aes",
"bitflags",
@@ -695,7 +689,6 @@ dependencies = [
"log",
"md-5",
"nom",
"nom_locate",
"rand",
"rangemap",
"rayon",
@@ -807,17 +800,6 @@ dependencies = [
"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]]
name = "num-conv"
version = "0.2.1"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"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.",
"main": "index.js",
"types": "index.d.ts",
+32 -7
View File
@@ -525,9 +525,6 @@ fn collect_text_in_region_with_options(
adaptive_threshold: f32,
) -> String {
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
.iter()
.filter(|item| region_overlaps_item(item, bounds))
@@ -536,11 +533,39 @@ fn collect_text_in_region_with_options(
if matched.is_empty() {
return String::new();
}
let mut thresholds = HashMap::new();
if adaptive_threshold > 0.10 {
thresholds.insert(page, adaptive_threshold);
// Simple extraction: the caller (fire-pdf) already handles reading order
// 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
.into_iter()
.map(|line| line.text())