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
+6
-6
@@ -33,7 +33,7 @@ pub(crate) fn try_build_rect_guided_table(
|
||||
|
||||
// 1. Derive column boundaries from rect X positions (snapped to 2pt tolerance)
|
||||
let mut x_lefts: Vec<f32> = cluster_rects.iter().map(|&(x, _, _, _)| x).collect();
|
||||
x_lefts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
x_lefts.sort_by(|a, b| a.total_cmp(b));
|
||||
// Snap: deduplicate within 2pt tolerance
|
||||
let mut col_boundaries: Vec<f32> = Vec::new();
|
||||
for x in &x_lefts {
|
||||
@@ -54,7 +54,7 @@ pub(crate) fn try_build_rect_guided_table(
|
||||
// boundaries so every day gets a column.
|
||||
if col_boundaries.len() >= 2 {
|
||||
let mut spacings: Vec<f32> = col_boundaries.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
spacings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
spacings.sort_by(|a, b| a.total_cmp(b));
|
||||
let median_spacing = spacings[spacings.len() / 2];
|
||||
let threshold = median_spacing * 1.5;
|
||||
|
||||
@@ -87,7 +87,7 @@ pub(crate) fn try_build_rect_guided_table(
|
||||
|
||||
// 3. Derive row boundaries from item Y positions (5pt tolerance)
|
||||
let mut y_values: Vec<f32> = expanded_items.iter().map(|(item, _)| item.y).collect();
|
||||
y_values.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); // descending
|
||||
y_values.sort_by(|a, b| b.total_cmp(a)); // descending
|
||||
let mut row_boundaries: Vec<f32> = Vec::new();
|
||||
for y in &y_values {
|
||||
if row_boundaries
|
||||
@@ -296,7 +296,7 @@ pub(crate) fn try_build_table_from_columns(items: &[TextItem], page: u32) -> Opt
|
||||
|
||||
// Find the top-most row with items in multiple columns (likely the header)
|
||||
let mut ys: Vec<f32> = page_items.iter().map(|i| i.y).collect();
|
||||
ys.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
|
||||
ys.sort_by(|a, b| b.total_cmp(a));
|
||||
ys.dedup_by(|a, b| (*a - *b).abs() < y_tol);
|
||||
|
||||
for &header_y in ys.iter().take(5) {
|
||||
@@ -318,7 +318,7 @@ pub(crate) fn try_build_table_from_columns(items: &[TextItem], page: u32) -> Opt
|
||||
if col_items.len() >= 2 {
|
||||
// Sort by X and find the split point
|
||||
let mut sorted: Vec<f32> = col_items.iter().map(|i| i.x).collect();
|
||||
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
sorted.sort_by(|a, b| a.total_cmp(b));
|
||||
// Split at the midpoint between the two items
|
||||
let split_x = (sorted[0]
|
||||
+ col_items.iter().find(|i| i.x == sorted[0]).unwrap().width
|
||||
@@ -411,7 +411,7 @@ pub(crate) fn try_build_table_from_columns(items: &[TextItem], page: u32) -> Opt
|
||||
}
|
||||
}
|
||||
}
|
||||
row_ys.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
|
||||
row_ys.sort_by(|a, b| b.total_cmp(a));
|
||||
|
||||
if row_ys.len() < 3 || row_ys.len() > 40 {
|
||||
return None;
|
||||
|
||||
Reference in New Issue
Block a user