fix: replace partial_cmp with total_cmp to prevent NaN sort panics (#20)
* fix sort panics on NaN values from bogus PDF font metrics Replace all `partial_cmp(...).unwrap_or(Ordering::Equal)` and bare `partial_cmp(...).unwrap()` with `total_cmp()` across the codebase. `partial_cmp` returns `None` for NaN, and mapping that to `Equal` violates total ordering: `a == NaN` and `NaN == b` but `a != b`. Rust 1.81+ detects this and panics in sort_by. `total_cmp` handles NaN deterministically (sorts to end) and guarantees total ordering. The critical crash was in `extract_text_in_regions` (lib.rs:478) where PDFs with bogus font ascent/descent values produced NaN in text item coordinates, causing process abort via NAPI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix missed partial_cmp in layout.rs and restore napi exports - Convert two remaining b.y.partial_cmp(&a.y) calls to total_cmp in group_single_column and column layout sorting - Restore missing napi exports: detectPdf, extractText, extractTextWithPositions, processPdf Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- 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
732b1b1359
commit
fba0a644ef
+14
-22
@@ -218,7 +218,7 @@ fn columns_have_prose(columns: &[ColumnRegion], items: &[&TextItem]) -> bool {
|
||||
|
||||
// Sort by Y descending (top of page = higher Y in PDF coords)
|
||||
let mut sorted: Vec<&TextItem> = col_items;
|
||||
sorted.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal));
|
||||
sorted.sort_by(|a, b| b.y.total_cmp(&a.y));
|
||||
|
||||
// Group into lines by Y-proximity and measure fill + item count
|
||||
let mut full_lines = 0usize;
|
||||
@@ -616,7 +616,7 @@ fn identify_spanning_lines(items: &[TextItem], columns: &[ColumnRegion]) -> Vec<
|
||||
// Build (original_index, y) pairs sorted by Y descending for grouping
|
||||
let mut indexed: Vec<(usize, f32)> =
|
||||
items.iter().enumerate().map(|(i, it)| (i, it.y)).collect();
|
||||
indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
indexed.sort_by(|a, b| b.1.total_cmp(&a.1));
|
||||
|
||||
// Group by Y-proximity into rough lines (as index sets)
|
||||
let mut groups: Vec<Vec<usize>> = Vec::new();
|
||||
@@ -778,7 +778,7 @@ pub(crate) fn is_newspaper_layout(
|
||||
return 0.0;
|
||||
}
|
||||
let mut ys: Vec<f32> = lines.iter().map(|l| l.y).collect();
|
||||
ys.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
ys.sort_by(|a, b| a.total_cmp(b));
|
||||
let span = ys.last().unwrap() - ys.first().unwrap();
|
||||
span / (lines.len() as f32 - 1.0)
|
||||
};
|
||||
@@ -845,7 +845,7 @@ fn split_column_stragglers(lines: Vec<TextLine>) -> (Vec<TextLine>, Vec<TextLine
|
||||
|
||||
// Median gap = typical line spacing
|
||||
let mut sorted_gaps = gaps.clone();
|
||||
sorted_gaps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
sorted_gaps.sort_by(|a, b| a.total_cmp(b));
|
||||
let median_gap = sorted_gaps[sorted_gaps.len() / 2];
|
||||
|
||||
// A gap > 3× median (min 30pt) indicates a break between content clusters
|
||||
@@ -1078,9 +1078,8 @@ pub(crate) fn group_into_lines_with_thresholds(
|
||||
}
|
||||
}
|
||||
|
||||
above.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal));
|
||||
below_spanning
|
||||
.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal));
|
||||
above.sort_by(|a, b| b.y.total_cmp(&a.y));
|
||||
below_spanning.sort_by(|a, b| b.y.total_cmp(&a.y));
|
||||
|
||||
all_lines.extend(above);
|
||||
for col in core_columns {
|
||||
@@ -1101,16 +1100,13 @@ pub(crate) fn group_into_lines_with_thresholds(
|
||||
|
||||
// Sort by Y descending (top-first), then by X for same-Y lines
|
||||
all_page_lines.sort_by(|a, b| {
|
||||
b.y.partial_cmp(&a.y)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then(
|
||||
a.items
|
||||
.first()
|
||||
.map(|i| i.x)
|
||||
.unwrap_or(0.0)
|
||||
.partial_cmp(&b.items.first().map(|i| i.x).unwrap_or(0.0))
|
||||
.unwrap_or(std::cmp::Ordering::Equal),
|
||||
)
|
||||
b.y.total_cmp(&a.y).then(
|
||||
a.items
|
||||
.first()
|
||||
.map(|i| i.x)
|
||||
.unwrap_or(0.0)
|
||||
.total_cmp(&b.items.first().map(|i| i.x).unwrap_or(0.0)),
|
||||
)
|
||||
});
|
||||
|
||||
// Merge lines at the same Y (within tolerance) into single lines
|
||||
@@ -1185,11 +1181,7 @@ fn group_single_column(items: Vec<TextItem>, adaptive_threshold: f32) -> Vec<Tex
|
||||
let items = if use_y_sorting {
|
||||
// Sort by Y descending (top to bottom in PDF coords)
|
||||
let mut sorted = items;
|
||||
sorted.sort_by(|a, b| {
|
||||
b.y.partial_cmp(&a.y)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then(a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal))
|
||||
});
|
||||
sorted.sort_by(|a, b| b.y.total_cmp(&a.y).then(a.x.total_cmp(&b.x)));
|
||||
sorted
|
||||
} else {
|
||||
items
|
||||
|
||||
@@ -334,17 +334,14 @@ pub(crate) fn merge_text_items(items: Vec<TextItem>) -> Vec<TextItem> {
|
||||
for (_, _, group) in &mut line_groups {
|
||||
let rtl = is_rtl_text(group.iter().map(|i| &i.text));
|
||||
if rtl {
|
||||
group.sort_by(|a, b| b.x.partial_cmp(&a.x).unwrap_or(std::cmp::Ordering::Equal));
|
||||
group.sort_by(|a, b| b.x.total_cmp(&a.x));
|
||||
} else {
|
||||
group.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
|
||||
group.sort_by(|a, b| a.x.total_cmp(&b.x));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort groups by page then Y descending (top of page first)
|
||||
line_groups.sort_by(|a, b| {
|
||||
a.0.cmp(&b.0)
|
||||
.then_with(|| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
|
||||
});
|
||||
line_groups.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| b.1.total_cmp(&a.1)));
|
||||
|
||||
let mut merged = Vec::new();
|
||||
|
||||
@@ -452,7 +449,7 @@ pub(crate) fn merge_subscript_items(items: Vec<TextItem>) -> Vec<TextItem> {
|
||||
|
||||
for (_, _, mut group) in line_groups {
|
||||
// Sort by X position
|
||||
group.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
|
||||
group.sort_by(|a, b| a.x.total_cmp(&b.x));
|
||||
|
||||
// Find the dominant (most common) font size in this group
|
||||
let max_fs = group.iter().map(|i| i.font_size).fold(0.0_f32, f32::max);
|
||||
|
||||
Reference in New Issue
Block a user