fix: simplify region text extraction to trust layout model ordering
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:
co-authored by
Claude Opus 4.6
parent
0db9863919
commit
6a9ff170dc
Generated
+1
-19
@@ -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
@@ -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
@@ -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())
|
||||||
|
|||||||
Reference in New Issue
Block a user