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:
Abimael Martell
2026-04-02 14:39:42 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 732b1b1359
commit fba0a644ef
13 changed files with 137 additions and 137 deletions
+52 -52
View File
@@ -77,8 +77,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-android-arm64') const binding = require('firecrawl-pdf-inspector-android-arm64')
const bindingPackageVersion = require('firecrawl-pdf-inspector-android-arm64/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-android-arm64/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -93,8 +93,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-android-arm-eabi') const binding = require('firecrawl-pdf-inspector-android-arm-eabi')
const bindingPackageVersion = require('firecrawl-pdf-inspector-android-arm-eabi/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-android-arm-eabi/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -114,8 +114,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-win32-x64-gnu') const binding = require('firecrawl-pdf-inspector-win32-x64-gnu')
const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-x64-gnu/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -130,8 +130,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-win32-x64-msvc') const binding = require('firecrawl-pdf-inspector-win32-x64-msvc')
const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-x64-msvc/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-x64-msvc/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -147,8 +147,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-win32-ia32-msvc') const binding = require('firecrawl-pdf-inspector-win32-ia32-msvc')
const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-ia32-msvc/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-ia32-msvc/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -163,8 +163,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-win32-arm64-msvc') const binding = require('firecrawl-pdf-inspector-win32-arm64-msvc')
const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-arm64-msvc/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-win32-arm64-msvc/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -182,8 +182,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-darwin-universal') const binding = require('firecrawl-pdf-inspector-darwin-universal')
const bindingPackageVersion = require('firecrawl-pdf-inspector-darwin-universal/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-darwin-universal/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -198,8 +198,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-darwin-x64') const binding = require('firecrawl-pdf-inspector-darwin-x64')
const bindingPackageVersion = require('firecrawl-pdf-inspector-darwin-x64/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-darwin-x64/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -214,8 +214,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-darwin-arm64') const binding = require('firecrawl-pdf-inspector-darwin-arm64')
const bindingPackageVersion = require('firecrawl-pdf-inspector-darwin-arm64/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-darwin-arm64/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -234,8 +234,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-freebsd-x64') const binding = require('firecrawl-pdf-inspector-freebsd-x64')
const bindingPackageVersion = require('firecrawl-pdf-inspector-freebsd-x64/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-freebsd-x64/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -250,8 +250,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-freebsd-arm64') const binding = require('firecrawl-pdf-inspector-freebsd-arm64')
const bindingPackageVersion = require('firecrawl-pdf-inspector-freebsd-arm64/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-freebsd-arm64/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -271,8 +271,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-x64-musl') const binding = require('firecrawl-pdf-inspector-linux-x64-musl')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-x64-musl/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-x64-musl/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -287,8 +287,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-x64-gnu') const binding = require('firecrawl-pdf-inspector-linux-x64-gnu')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-x64-gnu/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -305,8 +305,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-arm64-musl') const binding = require('firecrawl-pdf-inspector-linux-arm64-musl')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm64-musl/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm64-musl/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -321,8 +321,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-arm64-gnu') const binding = require('firecrawl-pdf-inspector-linux-arm64-gnu')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm64-gnu/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm64-gnu/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -339,8 +339,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-arm-musleabihf') const binding = require('firecrawl-pdf-inspector-linux-arm-musleabihf')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm-musleabihf/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm-musleabihf/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -355,8 +355,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-arm-gnueabihf') const binding = require('firecrawl-pdf-inspector-linux-arm-gnueabihf')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm-gnueabihf/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-arm-gnueabihf/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -373,8 +373,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-loong64-musl') const binding = require('firecrawl-pdf-inspector-linux-loong64-musl')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-loong64-musl/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-loong64-musl/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -389,8 +389,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-loong64-gnu') const binding = require('firecrawl-pdf-inspector-linux-loong64-gnu')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-loong64-gnu/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-loong64-gnu/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -407,8 +407,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-riscv64-musl') const binding = require('firecrawl-pdf-inspector-linux-riscv64-musl')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-riscv64-musl/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-riscv64-musl/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -423,8 +423,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-riscv64-gnu') const binding = require('firecrawl-pdf-inspector-linux-riscv64-gnu')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-riscv64-gnu/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-riscv64-gnu/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -440,8 +440,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-ppc64-gnu') const binding = require('firecrawl-pdf-inspector-linux-ppc64-gnu')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-ppc64-gnu/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-ppc64-gnu/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -456,8 +456,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-linux-s390x-gnu') const binding = require('firecrawl-pdf-inspector-linux-s390x-gnu')
const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-s390x-gnu/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-linux-s390x-gnu/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -476,8 +476,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-openharmony-arm64') const binding = require('firecrawl-pdf-inspector-openharmony-arm64')
const bindingPackageVersion = require('firecrawl-pdf-inspector-openharmony-arm64/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-openharmony-arm64/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -492,8 +492,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-openharmony-x64') const binding = require('firecrawl-pdf-inspector-openharmony-x64')
const bindingPackageVersion = require('firecrawl-pdf-inspector-openharmony-x64/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-openharmony-x64/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
@@ -508,8 +508,8 @@ function requireNative() {
try { try {
const binding = require('firecrawl-pdf-inspector-openharmony-arm') const binding = require('firecrawl-pdf-inspector-openharmony-arm')
const bindingPackageVersion = require('firecrawl-pdf-inspector-openharmony-arm/package.json').version const bindingPackageVersion = require('firecrawl-pdf-inspector-openharmony-arm/package.json').version
if (bindingPackageVersion !== '0.2.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { if (bindingPackageVersion !== '0.2.3' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.2.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) throw new Error(`Native binding package version mismatch, expected 0.2.3 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
} }
return binding return binding
} catch (e) { } catch (e) {
+9 -17
View File
@@ -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) // Sort by Y descending (top of page = higher Y in PDF coords)
let mut sorted: Vec<&TextItem> = col_items; 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 // Group into lines by Y-proximity and measure fill + item count
let mut full_lines = 0usize; 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 // Build (original_index, y) pairs sorted by Y descending for grouping
let mut indexed: Vec<(usize, f32)> = let mut indexed: Vec<(usize, f32)> =
items.iter().enumerate().map(|(i, it)| (i, it.y)).collect(); 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) // Group by Y-proximity into rough lines (as index sets)
let mut groups: Vec<Vec<usize>> = Vec::new(); let mut groups: Vec<Vec<usize>> = Vec::new();
@@ -778,7 +778,7 @@ pub(crate) fn is_newspaper_layout(
return 0.0; return 0.0;
} }
let mut ys: Vec<f32> = lines.iter().map(|l| l.y).collect(); 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(); let span = ys.last().unwrap() - ys.first().unwrap();
span / (lines.len() as f32 - 1.0) 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 // Median gap = typical line spacing
let mut sorted_gaps = gaps.clone(); 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]; let median_gap = sorted_gaps[sorted_gaps.len() / 2];
// A gap > 3× median (min 30pt) indicates a break between content clusters // 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)); above.sort_by(|a, b| b.y.total_cmp(&a.y));
below_spanning below_spanning.sort_by(|a, b| b.y.total_cmp(&a.y));
.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal));
all_lines.extend(above); all_lines.extend(above);
for col in core_columns { for col in core_columns {
@@ -1101,15 +1100,12 @@ pub(crate) fn group_into_lines_with_thresholds(
// Sort by Y descending (top-first), then by X for same-Y lines // Sort by Y descending (top-first), then by X for same-Y lines
all_page_lines.sort_by(|a, b| { all_page_lines.sort_by(|a, b| {
b.y.partial_cmp(&a.y) b.y.total_cmp(&a.y).then(
.unwrap_or(std::cmp::Ordering::Equal)
.then(
a.items a.items
.first() .first()
.map(|i| i.x) .map(|i| i.x)
.unwrap_or(0.0) .unwrap_or(0.0)
.partial_cmp(&b.items.first().map(|i| i.x).unwrap_or(0.0)) .total_cmp(&b.items.first().map(|i| i.x).unwrap_or(0.0)),
.unwrap_or(std::cmp::Ordering::Equal),
) )
}); });
@@ -1185,11 +1181,7 @@ fn group_single_column(items: Vec<TextItem>, adaptive_threshold: f32) -> Vec<Tex
let items = if use_y_sorting { let items = if use_y_sorting {
// Sort by Y descending (top to bottom in PDF coords) // Sort by Y descending (top to bottom in PDF coords)
let mut sorted = items; let mut sorted = items;
sorted.sort_by(|a, b| { sorted.sort_by(|a, b| b.y.total_cmp(&a.y).then(a.x.total_cmp(&b.x)));
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 sorted
} else { } else {
items items
+4 -7
View File
@@ -334,17 +334,14 @@ pub(crate) fn merge_text_items(items: Vec<TextItem>) -> Vec<TextItem> {
for (_, _, group) in &mut line_groups { for (_, _, group) in &mut line_groups {
let rtl = is_rtl_text(group.iter().map(|i| &i.text)); let rtl = is_rtl_text(group.iter().map(|i| &i.text));
if rtl { 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 { } 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) // Sort groups by page then Y descending (top of page first)
line_groups.sort_by(|a, b| { line_groups.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| b.1.total_cmp(&a.1)));
a.0.cmp(&b.0)
.then_with(|| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
});
let mut merged = Vec::new(); 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 { for (_, _, mut group) in line_groups {
// Sort by X position // 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 // 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); let max_fs = group.iter().map(|i| i.font_size).fold(0.0_f32, f32::max);
+21 -8
View File
@@ -477,16 +477,29 @@ fn collect_text_in_region(
return String::new(); return String::new();
} }
// Sort top→bottom (descending Y in bottom-left coords), then left→right // Sort top→bottom (descending Y in bottom-left coords), then left→right.
// Uses total_cmp to avoid panics on NaN values from bogus font metrics.
matched.sort_by(|a, b| { matched.sort_by(|a, b| {
let line_threshold = a.font_size.max(b.font_size) * 0.5; let fs_a = if a.font_size.is_finite() {
let y_diff = b.y - a.y; // descending Y = top to bottom a.font_size
if y_diff.abs() < line_threshold {
a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)
} else { } else {
y_diff 0.0
.partial_cmp(&0.0_f32) };
.unwrap_or(std::cmp::Ordering::Equal) let fs_b = if b.font_size.is_finite() {
b.font_size
} else {
0.0
};
let line_threshold = fs_a.max(fs_b) * 0.5;
let ay = if a.y.is_finite() { a.y } else { 0.0 };
let by = if b.y.is_finite() { b.y } else { 0.0 };
let y_diff = by - ay; // descending Y = top to bottom
if y_diff.abs() < line_threshold {
let ax = if a.x.is_finite() { a.x } else { 0.0 };
let bx = if b.x.is_finite() { b.x } else { 0.0 };
ax.total_cmp(&bx)
} else {
by.total_cmp(&ay)
} }
}); });
+2 -2
View File
@@ -121,7 +121,7 @@ pub(crate) fn compute_paragraph_threshold(lines: &[TextLine], base_size: f32) ->
return fallback; return fallback;
} }
gaps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); gaps.sort_by(|a, b| a.total_cmp(b));
let median = gaps[gaps.len() / 2]; let median = gaps[gaps.len() / 2];
@@ -221,7 +221,7 @@ pub(crate) fn compute_heading_tiers(lines: &[TextLine], base_size: f32) -> Vec<f
} }
// Sort descending // Sort descending
heading_sizes.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); heading_sizes.sort_by(|a, b| b.total_cmp(a));
// Cluster sizes within 0.5pt into same tier (use first value as representative) // Cluster sizes within 0.5pt into same tier (use first value as representative)
let mut tiers: Vec<f32> = Vec::new(); let mut tiers: Vec<f32> = Vec::new();
+4 -4
View File
@@ -42,7 +42,7 @@ pub(crate) fn split_side_by_side(items: &[TextItem]) -> Vec<(f32, f32)> {
// Sort items by left edge // Sort items by left edge
let mut xs: Vec<f32> = items.iter().map(|i| i.x).collect(); let mut xs: Vec<f32> = items.iter().map(|i| i.x).collect();
xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); xs.sort_by(|a, b| a.total_cmp(b));
// Find all candidate gaps: ≥30pt, in the middle 60% of the X range, // Find all candidate gaps: ≥30pt, in the middle 60% of the X range,
// with ≥20 items on each side. // with ≥20 items on each side.
@@ -118,7 +118,7 @@ pub(crate) fn split_side_by_side(items: &[TextItem]) -> Vec<(f32, f32)> {
}) })
.copied() .copied()
.collect(); .collect();
balanced_positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); balanced_positions.sort_by(|a, b| a.total_cmp(b));
balanced_positions.dedup_by(|a, b| (*a - *b).abs() < 50.0); balanced_positions.dedup_by(|a, b| (*a - *b).abs() < 50.0);
if balanced_positions.len() > 1 { if balanced_positions.len() > 1 {
return vec![]; return vec![];
@@ -209,7 +209,7 @@ fn split_from_hint_regions(items: &[TextItem], rects: &[PdfRect], page: u32) ->
// Width outlier filter (same as detect_tables_from_rects) // Width outlier filter (same as detect_tables_from_rects)
let mut widths: Vec<f32> = page_rects.iter().map(|&(_, _, w, _)| w).collect(); let mut widths: Vec<f32> = page_rects.iter().map(|&(_, _, w, _)| w).collect();
widths.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); widths.sort_by(|a, b| a.total_cmp(b));
let median_width = widths[widths.len() / 2]; let median_width = widths[widths.len() / 2];
page_rects.retain(|&(_, _, w, _)| w <= median_width * 10.0); page_rects.retain(|&(_, _, w, _)| w <= median_width * 10.0);
@@ -1037,7 +1037,7 @@ pub(crate) fn to_markdown_from_items_with_rects_and_lines(
} }
// Sort by Y descending (top to bottom) so left and right // Sort by Y descending (top to bottom) so left and right
// band lines interleave in visual reading order. // band lines interleave in visual reading order.
page_lines.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap_or(std::cmp::Ordering::Equal)); page_lines.sort_by(|a, b| b.y.total_cmp(&a.y));
all_lines.extend(page_lines); all_lines.extend(page_lines);
} }
all_lines all_lines
+1 -1
View File
@@ -275,7 +275,7 @@ pub(crate) fn strip_repeated_lines(lines: Vec<TextLine>, page_count: u32) -> Vec
page_sorted_ys.entry(line.page).or_default().push(line.y); page_sorted_ys.entry(line.page).or_default().push(line.y);
} }
for ys in page_sorted_ys.values_mut() { for ys in page_sorted_ys.values_mut() {
ys.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); ys.sort_by(|a, b| a.total_cmp(b));
ys.dedup(); ys.dedup();
} }
+7 -9
View File
@@ -48,7 +48,7 @@ pub(crate) fn merge_adjacent_items(items: &[TextItem]) -> (Vec<TextItem>, Vec<Ve
} }
// Sort groups by Y descending (top of page first) // Sort groups by Y descending (top of page first)
line_groups.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); line_groups.sort_by(|a, b| b.0.total_cmp(&a.0));
let mut merged_items = Vec::new(); let mut merged_items = Vec::new();
let mut index_map: Vec<Vec<usize>> = Vec::new(); let mut index_map: Vec<Vec<usize>> = Vec::new();
@@ -284,7 +284,7 @@ fn find_table_regions(items: &[(usize, &TextItem)]) -> Vec<(f32, f32)> {
} }
let mut y_positions: Vec<f32> = items.iter().map(|(_, i)| i.y).collect(); let mut y_positions: Vec<f32> = items.iter().map(|(_, i)| i.y).collect();
y_positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); y_positions.sort_by(|a, b| a.total_cmp(b));
// Find clusters of Y positions (table regions) // Find clusters of Y positions (table regions)
let mut regions = Vec::new(); let mut regions = Vec::new();
@@ -347,7 +347,7 @@ fn find_table_regions_strict(items: &[(usize, &TextItem)]) -> Vec<(f32, f32, f32
let mut qualifying_rows: Vec<(f32, Vec<f32>)> = Vec::new(); // (y, cluster_starts) let mut qualifying_rows: Vec<(f32, Vec<f32>)> = Vec::new(); // (y, cluster_starts)
for (y, x_positions) in &row_groups { for (y, x_positions) in &row_groups {
let mut sorted_xs = x_positions.clone(); let mut sorted_xs = x_positions.clone();
sorted_xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); sorted_xs.sort_by(|a, b| a.total_cmp(b));
if sorted_xs.is_empty() { if sorted_xs.is_empty() {
continue; continue;
@@ -379,14 +379,14 @@ fn find_table_regions_strict(items: &[(usize, &TextItem)]) -> Vec<(f32, f32, f32
// Step 3: Find contiguous runs of qualifying rows. // Step 3: Find contiguous runs of qualifying rows.
// Use adaptive gap: median spacing × 3 (handles wrapped cells where // Use adaptive gap: median spacing × 3 (handles wrapped cells where
// qualifying rows are spaced further apart), with a floor of 25pt. // qualifying rows are spaced further apart), with a floor of 25pt.
qualifying_rows.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); qualifying_rows.sort_by(|a, b| a.0.total_cmp(&b.0));
let max_gap = if qualifying_rows.len() >= 3 { let max_gap = if qualifying_rows.len() >= 3 {
let mut gaps: Vec<f32> = qualifying_rows let mut gaps: Vec<f32> = qualifying_rows
.windows(2) .windows(2)
.map(|w| (w[1].0 - w[0].0).abs()) .map(|w| (w[1].0 - w[0].0).abs())
.collect(); .collect();
gaps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); gaps.sort_by(|a, b| a.total_cmp(b));
let median_gap = gaps[gaps.len() / 2]; let median_gap = gaps[gaps.len() / 2];
(median_gap * 3.0).max(25.0) (median_gap * 3.0).max(25.0)
} else { } else {
@@ -566,11 +566,9 @@ fn detect_table_in_region(items: &[(usize, &TextItem)], mode: TableDetectionMode
// Sort by X position (direction-aware) // Sort by X position (direction-aware)
let rtl = is_rtl_text(col_items.iter().map(|i| &i.text)); let rtl = is_rtl_text(col_items.iter().map(|i| &i.text));
if rtl { if rtl {
col_items col_items.sort_by(|a, b| b.x.total_cmp(&a.x));
.sort_by(|a, b| b.x.partial_cmp(&a.x).unwrap_or(std::cmp::Ordering::Equal));
} else { } else {
col_items col_items.sort_by(|a, b| a.x.total_cmp(&b.x));
.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
} }
// Join items with subscript-aware spacing // Join items with subscript-aware spacing
+1 -1
View File
@@ -167,7 +167,7 @@ pub fn detect_tables_from_lines(items: &[TextItem], lines: &[PdfLine], page: u32
// Row edges need to be in descending order (top of page = higher Y first) // Row edges need to be in descending order (top of page = higher Y first)
let mut row_edges_desc = row_edges; let mut row_edges_desc = row_edges;
row_edges_desc.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); row_edges_desc.sort_by(|a, b| b.total_cmp(a));
log::debug!( log::debug!(
"detect_lines p{}: {} row_edges, {} col_edges, table=({:.0},{:.0})-({:.0},{:.0}), spanning_h={}, spanning_v={}", "detect_lines p{}: {} row_edges, {} col_edges, table=({:.0},{:.0})-({:.0},{:.0}), spanning_h={}, spanning_v={}",
+17 -17
View File
@@ -144,7 +144,7 @@ fn split_wide_cluster(
// Build sorted list of X-intervals (x_left, x_right) from each rect // Build sorted list of X-intervals (x_left, x_right) from each rect
let mut intervals: Vec<(f32, f32)> = rects.iter().map(|&(x, _, w, _)| (x, x + w)).collect(); let mut intervals: Vec<(f32, f32)> = rects.iter().map(|&(x, _, w, _)| (x, x + w)).collect();
intervals.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)); intervals.sort_by(|a, b| a.0.total_cmp(&b.0));
// Merge overlapping intervals to find contiguous X-bands // Merge overlapping intervals to find contiguous X-bands
let mut merged: Vec<(f32, f32)> = Vec::new(); let mut merged: Vec<(f32, f32)> = Vec::new();
@@ -262,7 +262,7 @@ pub fn detect_tables_from_rects(
// background fills stand out clearly. // background fills stand out clearly.
if page_rects.len() >= 6 { if page_rects.len() >= 6 {
let mut widths: Vec<f32> = page_rects.iter().map(|&(_, _, w, _)| w).collect(); let mut widths: Vec<f32> = page_rects.iter().map(|&(_, _, w, _)| w).collect();
widths.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); widths.sort_by(|a, b| a.total_cmp(b));
let median_width = widths[widths.len() / 2]; let median_width = widths[widths.len() / 2];
let width_threshold = median_width * 10.0; let width_threshold = median_width * 10.0;
let before = page_rects.len(); let before = page_rects.len();
@@ -333,7 +333,7 @@ pub fn detect_tables_from_rects(
// cluster they overlap, so grid detection still has their edges. // cluster they overlap, so grid detection still has their edges.
let is_page_bg = { let is_page_bg = {
let mut heights: Vec<f32> = page_rects.iter().map(|&(_, _, _, h)| h).collect(); let mut heights: Vec<f32> = page_rects.iter().map(|&(_, _, _, h)| h).collect();
heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); heights.sort_by(|a, b| a.total_cmp(b));
let median_height = heights[heights.len() / 2]; let median_height = heights[heights.len() / 2];
let height_threshold = median_height * 20.0; let height_threshold = median_height * 20.0;
let flags: Vec<bool> = page_rects let flags: Vec<bool> = page_rects
@@ -621,7 +621,7 @@ fn merge_overlapping_hints(mut hints: Vec<RectHintRegion>) -> Vec<RectHintRegion
return hints; return hints;
} }
loop { loop {
hints.sort_by(|a, b| a.x_left.partial_cmp(&b.x_left).unwrap()); hints.sort_by(|a, b| a.x_left.total_cmp(&b.x_left));
let mut merged: Vec<RectHintRegion> = Vec::new(); let mut merged: Vec<RectHintRegion> = Vec::new();
let mut any_merged = false; let mut any_merged = false;
for hint in &hints { for hint in &hints {
@@ -685,7 +685,7 @@ fn extract_hint_region(group_rects: &[(f32, f32, f32, f32)]) -> Option<RectHintR
// Compute median height to identify cell-sized rects // Compute median height to identify cell-sized rects
let mut heights: Vec<f32> = group_rects.iter().map(|&(_, _, _, h)| h).collect(); let mut heights: Vec<f32> = group_rects.iter().map(|&(_, _, _, h)| h).collect();
heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); heights.sort_by(|a, b| a.total_cmp(b));
let median_h = heights[heights.len() / 2]; let median_h = heights[heights.len() / 2];
// Keep only cell-sized rects (height ≤ 4× median) // Keep only cell-sized rects (height ≤ 4× median)
@@ -857,9 +857,9 @@ fn try_build_grid(
// Sort column edges left-to-right, row edges top-to-bottom (highest Y first for PDF) // Sort column edges left-to-right, row edges top-to-bottom (highest Y first for PDF)
let mut col_edges = x_edges; let mut col_edges = x_edges;
col_edges.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); col_edges.sort_by(|a, b| a.total_cmp(b));
let mut row_edges = y_edges; let mut row_edges = y_edges;
row_edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); row_edges.sort_by(|a, b| b.total_cmp(a));
let num_cols = col_edges.len() - 1; let num_cols = col_edges.len() - 1;
let num_rows = row_edges.len() - 1; let num_rows = row_edges.len() - 1;
@@ -1048,7 +1048,7 @@ fn try_build_grid(
/// Deduplicate nearby edge values within a tolerance, returning sorted unique edges. /// Deduplicate nearby edge values within a tolerance, returning sorted unique edges.
pub(crate) fn snap_edges(values: &[f32], tolerance: f32) -> Vec<f32> { pub(crate) fn snap_edges(values: &[f32], tolerance: f32) -> Vec<f32> {
let mut sorted: Vec<f32> = values.to_vec(); let mut sorted: Vec<f32> = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); sorted.sort_by(|a, b| a.total_cmp(b));
let mut snapped: Vec<f32> = Vec::new(); let mut snapped: Vec<f32> = Vec::new();
for &v in &sorted { for &v in &sorted {
@@ -1204,7 +1204,7 @@ fn is_row_stripe_pattern(rects: &[(f32, f32, f32, f32)]) -> bool {
} }
let mut widths: Vec<f32> = rects.iter().map(|&(_, _, w, _)| w).collect(); let mut widths: Vec<f32> = rects.iter().map(|&(_, _, w, _)| w).collect();
widths.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); widths.sort_by(|a, b| a.total_cmp(b));
let median_width = widths[widths.len() / 2]; let median_width = widths[widths.len() / 2];
// Must be page-spanning (>200pt) // Must be page-spanning (>200pt)
@@ -1252,7 +1252,7 @@ fn detect_row_stripe_table(
// Sort row edges top-to-bottom (highest Y first for PDF) // Sort row edges top-to-bottom (highest Y first for PDF)
let mut row_edges = y_edges; let mut row_edges = y_edges;
row_edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); row_edges.sort_by(|a, b| b.total_cmp(a));
// Compute the bounding box of the stripe region for filtering items // Compute the bounding box of the stripe region for filtering items
let y_top = row_edges[0]; let y_top = row_edges[0];
@@ -1476,7 +1476,7 @@ fn detect_row_stripe_table_from_cell_rects(
// bounding box to scope items and derive rows from text Y-positions. // bounding box to scope items and derive rows from text Y-positions.
let row_edges = if y_edges.len() >= 4 { let row_edges = if y_edges.len() >= 4 {
let mut edges = y_edges; let mut edges = y_edges;
edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); edges.sort_by(|a, b| b.total_cmp(a));
edges edges
} else { } else {
// Fall back: gather items in the rect region and cluster by Y // Fall back: gather items in the rect region and cluster by Y
@@ -1508,11 +1508,11 @@ fn detect_row_stripe_table_from_cell_rects(
// Cluster Y positions using median font height as threshold // Cluster Y positions using median font height as threshold
let median_h = { let median_h = {
let mut hs: Vec<f32> = region_items.iter().map(|i| i.height).collect(); let mut hs: Vec<f32> = region_items.iter().map(|i| i.height).collect();
hs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); hs.sort_by(|a, b| a.total_cmp(b));
hs[hs.len() / 2] hs[hs.len() / 2]
}; };
let mut ys: Vec<f32> = region_items.iter().map(|i| i.y).collect(); let mut ys: Vec<f32> = region_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));
let mut edges = Vec::new(); let mut edges = Vec::new();
let threshold = median_h * 0.8; let threshold = median_h * 0.8;
let mut cluster_start = ys[0]; let mut cluster_start = ys[0];
@@ -1536,7 +1536,7 @@ fn detect_row_stripe_table_from_cell_rects(
edges.push(center - median_h * 0.5); edges.push(center - median_h * 0.5);
let _ = cluster_start; // suppress unused warning let _ = cluster_start; // suppress unused warning
edges = snap_edges(&edges, 3.0); edges = snap_edges(&edges, 3.0);
edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); edges.sort_by(|a, b| b.total_cmp(a));
if edges.len() < 4 { if edges.len() < 4 {
return None; return None;
} }
@@ -1546,7 +1546,7 @@ fn detect_row_stripe_table_from_cell_rects(
// Compute bounding box from non-full-page rects // Compute bounding box from non-full-page rects
let median_h = { let median_h = {
let mut heights: Vec<f32> = group_rects.iter().map(|&(_, _, _, h)| h).collect(); let mut heights: Vec<f32> = group_rects.iter().map(|&(_, _, _, h)| h).collect();
heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); heights.sort_by(|a, b| a.total_cmp(b));
heights[heights.len() / 2] heights[heights.len() / 2]
}; };
let content_rects: Vec<_> = group_rects let content_rects: Vec<_> = group_rects
@@ -1724,7 +1724,7 @@ fn detect_merged_cluster_table(
} }
let mut row_edges = y_edges; let mut row_edges = y_edges;
row_edges.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); row_edges.sort_by(|a, b| b.total_cmp(a));
// Bounding box of all rects // Bounding box of all rects
let y_top = row_edges[0]; let y_top = row_edges[0];
@@ -1890,7 +1890,7 @@ fn detect_merged_cluster_table(
/// (no need for anti-paragraph safeguards). /// (no need for anti-paragraph safeguards).
fn cluster_x_positions(items: &[(usize, &TextItem)], min_threshold: f32) -> Vec<f32> { fn cluster_x_positions(items: &[(usize, &TextItem)], min_threshold: f32) -> Vec<f32> {
let mut x_positions: Vec<f32> = items.iter().map(|(_, i)| i.x).collect(); let mut x_positions: Vec<f32> = items.iter().map(|(_, i)| i.x).collect();
x_positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); x_positions.sort_by(|a, b| a.total_cmp(b));
if x_positions.is_empty() { if x_positions.is_empty() {
return vec![]; return vec![];
+4 -4
View File
@@ -9,7 +9,7 @@ pub(crate) fn find_column_boundaries(
mode: TableDetectionMode, mode: TableDetectionMode,
) -> Vec<f32> { ) -> Vec<f32> {
let mut x_positions: Vec<f32> = items.iter().map(|(_, i)| i.x).collect(); let mut x_positions: Vec<f32> = items.iter().map(|(_, i)| i.x).collect();
x_positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); x_positions.sort_by(|a, b| a.total_cmp(b));
if x_positions.is_empty() { if x_positions.is_empty() {
return vec![]; return vec![];
@@ -43,7 +43,7 @@ pub(crate) fn find_column_boundaries(
.collect(); .collect();
if consec_gaps.len() > 2 { if consec_gaps.len() > 2 {
consec_gaps.sort_by(|a, b| a.partial_cmp(b).unwrap()); consec_gaps.sort_by(|a, b| a.total_cmp(b));
// Find the biggest jump in the sorted gap sequence — natural break // Find the biggest jump in the sorted gap sequence — natural break
// between within-column jitter and between-column spacing. // between within-column jitter and between-column spacing.
// Require at least 3 values on each side to avoid outlier-dominated // Require at least 3 values on each side to avoid outlier-dominated
@@ -151,7 +151,7 @@ pub(crate) fn find_column_boundaries(
/// Find row boundaries by clustering Y positions /// Find row boundaries by clustering Y positions
pub(crate) fn find_row_boundaries(items: &[(usize, &TextItem)]) -> Vec<f32> { pub(crate) fn find_row_boundaries(items: &[(usize, &TextItem)]) -> Vec<f32> {
let mut y_positions: Vec<f32> = items.iter().map(|(_, i)| i.y).collect(); let mut y_positions: Vec<f32> = items.iter().map(|(_, i)| i.y).collect();
y_positions.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); // Descending y_positions.sort_by(|a, b| b.total_cmp(a)); // Descending
if y_positions.is_empty() { if y_positions.is_empty() {
return vec![]; return vec![];
@@ -162,7 +162,7 @@ pub(crate) fn find_row_boundaries(items: &[(usize, &TextItem)]) -> Vec<f32> {
// inter-row gaps (≥1× font size), preventing row merging in uniform-spaced PDFs. // inter-row gaps (≥1× font size), preventing row merging in uniform-spaced PDFs.
let cluster_threshold = { let cluster_threshold = {
let mut font_sizes: Vec<f32> = items.iter().map(|(_, i)| i.font_size).collect(); let mut font_sizes: Vec<f32> = items.iter().map(|(_, i)| i.font_size).collect();
font_sizes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); font_sizes.sort_by(|a, b| a.total_cmp(b));
let median_font = font_sizes[font_sizes.len() / 2]; let median_font = font_sizes[font_sizes.len() / 2];
(median_font * 0.8).max(4.0) (median_font * 0.8).max(4.0)
}; };
+6 -6
View File
@@ -33,7 +33,7 @@ pub(crate) fn try_build_rect_guided_table(
// 1. Derive column boundaries from rect X positions (snapped to 2pt tolerance) // 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(); 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 // Snap: deduplicate within 2pt tolerance
let mut col_boundaries: Vec<f32> = Vec::new(); let mut col_boundaries: Vec<f32> = Vec::new();
for x in &x_lefts { for x in &x_lefts {
@@ -54,7 +54,7 @@ pub(crate) fn try_build_rect_guided_table(
// boundaries so every day gets a column. // boundaries so every day gets a column.
if col_boundaries.len() >= 2 { if col_boundaries.len() >= 2 {
let mut spacings: Vec<f32> = col_boundaries.windows(2).map(|w| w[1] - w[0]).collect(); 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 median_spacing = spacings[spacings.len() / 2];
let threshold = median_spacing * 1.5; 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) // 3. Derive row boundaries from item Y positions (5pt tolerance)
let mut y_values: Vec<f32> = expanded_items.iter().map(|(item, _)| item.y).collect(); 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(); let mut row_boundaries: Vec<f32> = Vec::new();
for y in &y_values { for y in &y_values {
if row_boundaries 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) // 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(); 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); ys.dedup_by(|a, b| (*a - *b).abs() < y_tol);
for &header_y in ys.iter().take(5) { 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 { if col_items.len() >= 2 {
// Sort by X and find the split point // Sort by X and find the split point
let mut sorted: Vec<f32> = col_items.iter().map(|i| i.x).collect(); 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 // Split at the midpoint between the two items
let split_x = (sorted[0] let split_x = (sorted[0]
+ col_items.iter().find(|i| i.x == sorted[0]).unwrap().width + 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 { if row_ys.len() < 3 || row_ys.len() > 40 {
return None; return None;
+4 -4
View File
@@ -68,9 +68,9 @@ where
pub(crate) fn sort_line_items(items: &mut [TextItem]) { pub(crate) fn sort_line_items(items: &mut [TextItem]) {
let rtl = is_rtl_text(items.iter().map(|i| &i.text)); let rtl = is_rtl_text(items.iter().map(|i| &i.text));
if rtl { if rtl {
items.sort_by(|a, b| b.x.partial_cmp(&a.x).unwrap_or(std::cmp::Ordering::Equal)); items.sort_by(|a, b| b.x.total_cmp(&a.x));
} else { } else {
items.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal)); items.sort_by(|a, b| a.x.total_cmp(&b.x));
} }
} }
@@ -376,7 +376,7 @@ fn compute_canva_join_threshold(items: &[TextItem]) -> f32 {
} }
let mut sorted: Vec<f32> = ratios; let mut sorted: Vec<f32> = ratios;
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); sorted.sort_by(|a, b| a.total_cmp(b));
if sorted[sorted.len() - 1] < 0.40 || sorted[0] < 0.40 { if sorted[sorted.len() - 1] < 0.40 || sorted[0] < 0.40 {
return DEFAULT; return DEFAULT;
@@ -478,7 +478,7 @@ fn compute_single_char_join_threshold(items: &[TextItem]) -> f32 {
return DEFAULT; return DEFAULT;
} }
ratios.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); ratios.sort_by(|a, b| a.total_cmp(b));
// If all gaps are tight (max < 0.40), use default — normal PDF // If all gaps are tight (max < 0.40), use default — normal PDF
let max_ratio = ratios[ratios.len() - 1]; let max_ratio = ratios[ratios.len() - 1];