Commit Graph
324 Commits
Author SHA1 Message Date
Abimael MartellandClaude Opus 4.6 2f23f07f6e fix: use AND logic for looks_like_scan heuristic in detector (#39)
The looks_like_scan check incorrectly used OR logic, causing any single
condition (image_count <= 1, text_ops < 50, alphanum < 10) to flag a page
as a scan. A real scan has ALL three: single full-page image AND low text
AND low alphanum. Text pages with one figure were falsely flagged for OCR.

Bump napi to 0.7.3.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:05:22 -07:00
Abimael MartellandClaude Opus 4.6 0a9c120a6b fix: reduce false OCR recommendations for text PDFs with figures (#38)
* fix: reduce false OCR recommendations for text PDFs with figure images

Two fixes in the detector:

1. Fix Tf operator parsing: some PDFs concatenate Tf directly with the
   next operator (e.g. "25 Tf[<01>...") without whitespace. The scanner
   now accepts [, (, <, / as valid followers, fixing font_changes being
   reported as 0.

2. Distinguish text-with-figures from scanned-with-OCR: pages with
   multiple images (image_count > 1) and strong text signals (text_ops
   >= 50, alphanum >= 10) are recognized as text pages with figures,
   not scanned templates. Scanned PDFs have exactly 1 full-page image.

This prevents academic papers, reports with charts, and similar PDFs
from being incorrectly classified as Mixed/OCR-needed when their text
is perfectly extractable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump napi version to 0.7.2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove template image influence from page classification

Template images (large background/figure images) no longer affect
pages_needing_ocr. In the region-based pipeline, text regions are
extracted independently from image regions, and per-region needs_ocr
quality checks handle scanned-with-OCR garbage text.

Also makes the invisible text retry (for OCR text layers) trigger on
text quality rather than PDF type, so it works regardless of
classification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "fix: remove template image influence from page classification"

This reverts commit 100cbe5453.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:42:19 -07:00
Abimael MartellandClaude Opus 4.6 7c8b09be67 fix: improve table detection for numeric columns and multi-line headers (#35)
* fix: improve heuristic table detection for numeric columns and multi-line headers

Two fixes for tables that have clean extractable text but fail heuristic
structure detection:

1. Numeric column merge pass (grid.rs): After initial X-position
   clustering, adjacent clusters are merged when one is sparse (header
   text) and the other is dense with >50% numeric items (data column).
   Multi-line wrapped headers often land slightly offset from their
   data column — the merge closes gaps within 1.5× the clustering
   threshold. New is_numeric_text() helper matches decimals, percentages,
   negative numbers, and comma-separated thousands.

2. Duplicate-header skip (detect_heuristic.rs): Spanning super-headers
   like "First Degree | First Degree | Higher Degree" contain duplicate
   cells that trigger looks_like_partial_table_ex rejection. Now skips
   rows with duplicate cells when a better header candidate exists
   within the next 3 rows (higher fill ratio or numeric cells).

Tested on BITS Pilani university report (430 pages, 314 table pages).
Page 4 (multi-line header + numeric data) previously returned
needs_ocr=true; now correctly detects the table structure.

Eval: 197 PDFs, zero regressions, all 104+ tests pass, zero clippy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* bump version to 0.7.1

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:27:59 -07:00
Abimael MartellandClaude Opus 4.6 35445c3208 Auto-publish npm package when version changes in package.json (#33)
Replace tag-based trigger with push-to-main trigger that detects
version changes in napi/package.json, removing the need for manual
git tags to publish new npm releases.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 13:12:55 -07:00
Abimael MartellandClaude Opus 4.6 20f24d1f8d extractPagesMarkdown: return classification metadata (0.7.0) (#32)
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
Combine per-page markdown extraction with layout classification into a
single parse. extractPagesMarkdown now returns PagesExtractionResult with
pages_with_tables, pages_with_columns, pages_needing_ocr, and is_complex
alongside the per-page markdown — eliminating redundant PDF parses for
callers that need both.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.7.0
2026-04-14 13:08:07 -07:00
Abimael MartellandClaude Opus 4.6 abb0b925fb Add extractPagesMarkdown for per-page markdown extraction (#31)
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
* add extract_pages_markdown_mem for per-page markdown extraction

Enables hybrid OCR pipelines to skip GPU render+layout for simple text
pages by providing per-page markdown with needs_ocr flags. Font stats
are computed document-wide for consistent header detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* bump napi package version to 0.6.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.6.0
2026-04-14 12:37:32 -07:00
Abimael MartellandClaude Opus 4.6 00c5c18e2a napi: use string enums for PdfType and ItemType (0.5.0) (#29)
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
Replace stringly-typed pdf_type and item_type fields with
#[napi(string_enum)] enums for proper TypeScript type checking.
Add link_url field to TextItem instead of encoding URL in the
item_type string.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.5.0
2026-04-14 11:25:26 -07:00
Abimael MartellandClaude Opus 4.6 5159abe9c2 fix clippy warnings: prefix unused page_has_gid, cfg(test) wrapper
Publish npm package / Build x86_64-unknown-linux-gnu (push) Has been cancelled
Publish npm package / Publish to npm (push) Has been cancelled
Publish npm package / Build aarch64-apple-darwin (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.4.3
2026-04-13 19:53:28 -07:00
Abimael MartellandClaude Opus 4.6 843a745460 relax table extraction validation for layout-assisted regions (0.4.3)
Two changes that reduce false needsOcr rejections without hurting quality:

1. Per-region GID check instead of per-page blanket rejection.
   Previously, if ANY font on the page used GID-encoded glyphs (common
   in logos, decorative fonts), ALL table and text regions on that page
   were forced to GPU OCR via needsOcr=true. Now the page-level bail is
   removed; per-region text quality checks (is_garbage_text, is_cid_garbage,
   detect_encoding_issues) catch actual GID corruption in the extracted
   content. Tables whose text is clean pass through even if an unrelated
   font elsewhere on the page is GID-encoded.

2. Relaxed looks_like_partial_table for layout-assisted extraction.
   When the layout model already identified a region as a table (i.e.,
   extract_tables_in_regions_mem), boundary-detection heuristics are
   less necessary — we're not guessing "is this a table?" anymore, only
   "can we extract it correctly?". Relaxations:
   - Numeric first header cell accepted (e.g., year "2024")
   - 1 empty header cell allowed in 3+ column tables (merged headers)
   - Sparse first data row threshold relaxed from 33% to 50%
   Paragraph detection and duplicate-header checks remain strict.

Eval: 196/196 pass (full regression suite), 91/91 Rust tests pass
including 7 new layout-assisted validation tests. Zero regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:46:56 -07:00
Abimael MartellandClaude Opus 4.6 780efdb955 extract_tables_in_regions: detect paragraph-as-table misreads (0.4.2)
Publish npm package / Build x86_64-unknown-linux-gnu (push) Has been cancelled
Publish npm package / Publish to npm (push) Has been cancelled
Publish npm package / Build aarch64-apple-darwin (push) Has been cancelled
Adds a 5th failure-mode check to looks_like_partial_table: when the
heuristic mis-detects text-wrapped paragraph prose as a multi-column
table, cells in the same column tend to start with lowercase letters
or continuation punctuation (commas, closing quotes) — because they're
actually sentence fragments. Real tables almost never have most data
cells starting lowercase.

Trigger: ≥2 cols, ≥4 data rows, ≥60% of non-empty data cells start
with lowercase or continuation punctuation → return needs_ocr=true.

Caught in the eval as the next-largest failure mode after the 0.4.1 fix:
PDFs 088, 182, 090 — heuristic produced "tables" like:

  |Approval is needed from the|Acquisitions of|
  |Treasurer if the acquisition|residential and|
  |constitutes a "significant|agricultural|
  |action," including acquiring an|land by foreign|

Reading column 1 top-to-bottom: "Approval is needed from the Treasurer
if the acquisition constitutes a 'significant action,' including
acquiring an interest..." — a paragraph, not tabular data.

Tests: 2 new tests (the 088-style failure case + a real multi-word
table that must NOT be flagged). All 11 looks_like_partial_table tests
pass; 323 unit + 91 integration tests still green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.4.2
2026-04-13 15:09:08 -07:00
Abimael MartellandClaude Opus 4.6 d0dd067e70 extract_tables_in_regions: needs_ocr on suspicious table structure (0.4.1)
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 the heuristic returns markdown that looks like a partial / mis-detected
table, set needs_ocr=true so the caller falls back to GPU OCR. Previously the
same cases returned the broken table with needs_ocr=false, which produced
real-world TEDS=0 scores in fire-pdf evals (heuristic-built table didn't
match ground truth structure at all, but caller had no signal to fall back).

Four failure modes detected, all observed in opendataloader-bench eval losses:

1. **Header looks like a data row** — first cell of header is a bare number
   (e.g. `|2|...`), suggesting the actual header row was skipped. Real
   headers almost never start with just a number.

2. **Empty header cells in a multi-column table** — ≥3 cols, ≥1 empty cell
   in the header row. Indicates poor column boundary detection.

3. **Duplicate header cells** — same non-empty value appearing twice in the
   header (e.g. "Administration|Administration"). Means a multi-line header
   was collapsed wrong.

4. **Sparse first data row** — ≥3 cols and ≥1/3 of first-data-row cells are
   empty. Multi-row headers in the source PDF get smashed into header +
   sparse data row by the heuristic; this catches that.

Tests: 9 new unit tests in `looks_like_partial_table_tests` cover each
failure mode plus realistic non-failures (well-formed table, single-column
list, two-col with a single empty cell). All 91 existing tests still pass.

Bumps `napi/package.json` to 0.4.1 since this changes the function's return
behaviour for callers (some inputs that returned needs_ocr=false now return
true). The output text field is also cleared on the new fallback path so
callers don't accidentally use the broken markdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.4.1
2026-04-13 14:10:44 -07:00
Abimael Martell 8282c2f8ee bump napi package
Publish npm package / Publish to npm (push) Has been cancelled
Publish npm package / Build aarch64-apple-darwin (push) Has been cancelled
Publish npm package / Build x86_64-unknown-linux-gnu (push) Has been cancelled
v0.4.0
2026-04-13 12:43:28 -07:00
Abimael MartellandClaude Opus 4.6 8e8ab4a19d feat: add extractTablesInRegions NAPI binding for region-based table extraction (#27)
Adds a new function that takes a PDF buffer and page+bbox regions (same interface
as extractTextInRegions), runs heuristic table detection on items within each region,
and returns markdown pipe-tables. Falls back to needs_ocr=true when no table
structure is found or text quality is suspect.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:32:02 -07:00
Abimael MartellandClaude Opus 4.6 8e3084183c fix: suppress overused struct tree heading tags for better paragraph detection (#28)
Some PDFs (e.g. British Academy grant guidance, Carter BloodCare privacy
policy) have structure trees that incorrectly tag body text as H2 headings.
This caused every line within numbered paragraphs to render as a separate
## heading instead of being joined into flowing paragraph text.

Added detect_overused_struct_heading_levels() which pre-scans heading tag
frequency and suppresses levels appearing on >15% of tagged lines, allowing
those lines to fall through to normal paragraph joining.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:31:42 -07:00
Abimael MartellandClaude Opus 4.6 d8bb0f5898 chore: bump npm version to 0.3.6
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.3.6
2026-04-08 23:16:20 -07:00
Abimael MartellandClaude Opus 4.6 1b4f1f4640 fix: reduce false OCR flags for Identity-H fonts with fallback decoding (#26)
The detector flagged pages for OCR whenever any font was Identity-H
without ToUnicode, even when the extraction pipeline could decode the
font via fallback paths (CID-as-Unicode passthrough or embedded TrueType
cmap). This caused false positives on PDFs from Chromium, wkhtmltopdf,
and other generators that use Identity-H with Unicode CID values.

Now checks DescendantFonts W array and embedded font cmap before
flagging. Fonts that are genuinely undecodable (stripped cmap, low GID
CIDs) are still correctly flagged.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 18:22:11 -07:00
Abimael MartellandClaude Opus 4.6 2455f1437b chore: bump npm version to 0.3.5
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.3.5
2026-04-08 16:44:45 -07:00
Abimael MartellandClaude Opus 4.6 640cdaaa13 fix: stop counting Do operators as images in content stream scanner (#25)
Do invokes any XObject (Form or Image), but scan_content_for_text_operators
was counting every Do as an image. PDFs with Form XObjects (e.g. ACS
publisher watermark pages) were misclassified as ImageBased because the
inflated image_count raised the min text ops threshold above the actual
text operator count.

Image detection is already correctly handled by scan_xobjects_in_resources
(checks Subtype) and analyze_page_images (measures pixel area).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:41:40 -07:00
Abimael MartellandClaude Opus 4.6 be313cdb81 fix: require strong signal for rarity-based heading detection (#24)
In multi-column PDFs, column switches break paragraph continuity,
making body text lines appear "standalone". Combined with moderate
font-size rarity from minor size variation between columns, this
caused hundreds of false heading classifications (e.g. 281 false ##
headings on a single academic paper).

Non-bold, non-isolated lines now require very high rarity (≥0.97)
and short word count (≤8) to qualify as headings via the rarity path.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:45:51 -07:00
Abimael MartellandClaude Opus 4.6 6a9ff170dc 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>
v0.3.4
2026-04-04 21:58:05 -07:00
Abimael MartellandClaude Opus 4.6 0db9863919 feat: lookahead-based isolated line heading detection
Pre-scan lines to identify "isolated" ones — short lines (1-6 words)
with paragraph breaks both before AND after. These are heading
candidates even at body font size, common in academic papers
("Acknowledgements", "Limitations", "B.3 Prompt Engineering").

Inspired by opendataloader's HeadingProcessor which passes prevNode
and nextNode context to the heading probability scorer.

The isolated signal (+0.3) combines with rarity/bold/standalone
signals. A per-page density guard prevents false positives on
multi-column pages where many lines appear isolated. Continuation
word detection (ending in "the", "and", etc.) filters wrapped
paragraph lines.

MHS=0 docs: 18→13. MHS-S +0.004. No regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 12:32:31 -07:00
Abimael MartellandClaude Opus 4.6 14154ee5ee docs: update benchmark scores
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 12:19:38 -07:00
Abimael MartellandClaude Opus 4.6 10dd7e2881 feat: XY-cut fallback for column detection on asymmetric layouts
When the histogram-based column detector finds no valleys (common with
sidebar/asymmetric layouts), fall back to a simplified XY-cut: find the
largest horizontal gap between item edges and split there if both sides
have enough items with vertical overlap.

Inspired by opendataloader's XY-Cut++ algorithm but implemented as a
single-level fallback rather than full recursive segmentation.

Doc 156: NID 0.545→0.966, Doc 157: NID 0.564→0.962.
NID-S +0.007, TEDS-S +0.066 across 200 docs. No regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 12:11:45 -07:00
Abimael MartellandClaude Opus 4.6 cf7e6b895d Bump napi package version to 0.3.3
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v0.3.3
2026-04-04 11:57:32 -07:00
Abimael MartellandClaude Opus 4.6 af34bbb6b5 feat: synthesize lines from thin filled rects as last-resort table detection
PDFs that draw table borders as thin filled rectangles (< 2pt, common in
spreadsheet exports) were invisible to both rect-based and line-based
table detectors. Now converts these thin rects to PdfLine objects and
runs line-based detection, but ONLY as a last resort after all other
methods (rect, line, heuristic, column-based) found nothing.

This avoids the regression from the earlier attempt which ran synthesis
at step 2, preempting the heuristic detector on PDFs where it worked
better.

Also relaxes uniform row spacing threshold (CV 0.05→0.02) to accept
spreadsheet-exported tables with even row heights.

Benchmark: TEDS 0.519→0.586 (+0.067), overall +0.006, no regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:44:11 -07:00
Abimael MartellandClaude Opus 4.6 dbc2de3e9b revert: undo table splitting changes that caused TEDS regression
Reverts commits 937311c, 1dcb0c6, 0300e96, 999f9a2. The thin-rect-to-line
synthesis and stacked table splitting improved extraction for specific
government PDFs but caused -0.05 TEDS regression on the benchmark by
preempting the heuristic detector with worse line-based grids.

These features need more targeted guards before re-enabling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:20:05 -07:00
Abimael MartellandClaude Opus 4.6 999f9a2f2d feat: split stacked tables at rows without vertical borders
Rows that sit between horizontal rules but lack vertical border coverage
are not table cells — they're freestanding text (e.g. "Note: The cutoff
mark is out of 120"). These rows now split the grid into separate
sub-tables, with the unbounded text emitted as plain text between them.

Single-cell "tables" (from the split) render as plain text instead of
a degenerate 1x1 markdown table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:24:28 -07:00
Abimael MartellandClaude Opus 4.6 0300e96b7d fix: don't merge column header rows as continuations
Rows with 3+ short-valued cells (avg ≤10 chars) and an empty first cell
are column headers (e.g. "UR | SC | ST | OBC | EWS"), not text overflow
from the previous row. Prevents them from being merged into the
preceding section title row.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 02:24:52 -07:00
Abimael MartellandClaude Opus 4.6 1dcb0c6feb fix: improve table row splitting for stacked sub-tables
Three fixes for better table extraction from spreadsheet-exported PDFs:

1. Convert thin filled rects (< 2pt) to PdfLine objects before line-based
   table detection. Many PDFs draw table borders as narrow filled rectangles
   instead of stroked paths — these were invisible to the line detector.

2. Relax uniform row spacing rejection (CV 0.05 → 0.02). Spreadsheet
   exports have very even row heights that were being rejected as "chart
   grids".

3. Fix continuation row merging: don't merge rows where the only non-first
   cell content is a long label (section headers like "Category No. 03").
   Don't merge first-cell-only rows with long text ("Note: ...").

Also adds multi-Y row splitting in line-based detection and column-aware
table detection skipping for multi-column pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 02:20:42 -07:00
Abimael MartellandClaude Opus 4.6 937311ce27 feat: convert thin filled rects to lines for table border detection
Many PDFs (especially spreadsheet exports) draw table borders as thin
filled rectangles (height/width < 2pt) instead of stroked paths. These
were invisible to our line-based table detector since only stroke
operations produced PdfLine objects.

Now synthesizes PdfLine from thin rects before line-based detection,
enabling table detection on border-drawn PDFs like government forms.

Also relaxes the uniform row spacing rejection threshold (CV 0.05→0.02)
to accept spreadsheet-exported tables with even row heights.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 02:12:05 -07:00
Abimael MartellandClaude Opus 4.6 9c463b9c95 fix: prevent multi-column text from being misdetected as tables
On pages where column detection finds 2+ columns, skip body-font
heuristic table detection in the merged-band retry path. This prevents
sidebar/two-column prose from being formatted as markdown tables.

The fix is targeted: per-band heuristic detection still runs (bands
are scoped to single columns), so real tables within columns are
still detected. Only the merged-band retry (which sees all items
across columns) is gated.

Also relaxes column validation to accept asymmetric layouts (sidebars)
where one side has fewer items, and tries center-based item assignment
before edge-based to improve column splitting for asymmetric layouts.

Benchmark: NID 0.865→0.869, NID-S 0.798→0.805, overall +0.002.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 02:01:24 -07:00
Abimael MartellandClaude Opus 4.6 1c48a014f7 docs: update benchmark MHS score after rarity-based detection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:38:21 -07:00
Abimael MartellandClaude Opus 4.6 6e5abd0b48 feat: rarity-based heading detection inspired by opendataloader
Replace ad-hoc bold/ratio heading checks with a unified scoring system
based on font size rarity. For each line, compute:
  score = font_rarity * 0.5 + bold * 0.3 + standalone * 0.2

Font rarity measures how infrequently a font size appears across the
document — heading fonts are rare while body text is common. This
approach (from opendataloader's ModeWeightStatistics) naturally adapts
to each document's font distribution instead of relying on fixed
thresholds.

Guards: require font_size >= 0.95 * base_size (no small-font headings),
word_count >= 3, and standalone (paragraph break before).

Benchmark improvement: MHS 0.56→0.58, MHS-S 0.66→0.70, overall +0.003.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:38:03 -07:00
Abimael MartellandClaude Opus 4.6 b7ec80097b chore: update lopdf to latest commit
7a05512d831415b1f2b1ce522391d6beab8a1284

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:27:32 -07:00
Abimael MartellandClaude Opus 4.6 895e47b094 docs: add opendataloader-bench results to README
Compare pdf-inspector against other direct text extraction engines
(no OCR/ML) on the opendataloader-bench corpus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:25:08 -07:00
Abimael MartellandClaude Opus 4.6 2f9390e4bf feat: detect slightly-larger-than-body text as headings
Lines with font size 1.10-1.20x body text that are standalone and short
(1-8 words) are promoted to headings. This catches academic paper
headings where the font is only ~10% larger than body text, below the
previous 1.2x threshold.

Also syncs the simpler to_markdown_from_lines path to match the
table-aware path (removes stale colon exclusion).

Benchmark improvement: MHS 0.54→0.56, overall 0.761→0.766.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:18:31 -07:00
Abimael MartellandClaude Opus 4.6 b25a122655 fix: require digit after Table/Figure prefix in caption detection
Caption detection was incorrectly classifying "Table of Contents" as a
caption because it starts with "Table ". Now "Table" and "Figure"
prefixes require a digit, parenthesis, or hash after them — matching
actual captions like "Table 1", "Figure 3.2" but not titles.

Also removes debug logging left from previous iteration.

Benchmark improvement: MHS 0.52→0.54, overall 0.757→0.761.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:13:17 -07:00
Abimael MartellandClaude Opus 4.6 103995c200 feat: remove colon exclusion from bold heading detection
The colon exclusion was preventing legitimate headings like "Steps for
Using the Microscope:" and "Changing objectives:" from being detected.
The single edge case it was protecting (chart sub-headers) is less
impactful than the many headings it was blocking.

Benchmark improvement: MHS 0.51→0.52, MHS-S 0.61→0.62.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:09:19 -07:00
Abimael MartellandClaude Opus 4.6 28823bd849 feat: improve heuristic table detection for small body-font tables
- Lower minimum item count for body-font table candidates from 9 to 6,
  allowing small 2-3 row tables to be detected.
- Allow 2-column body-font tables with short cells (avg ≤25 chars) to
  bypass the "table-like content" validation. This catches text-only
  definition/category tables (e.g., species lists) without false-positiving
  on 2-column paragraph text (which has longer cells).

Benchmark improvement: TEDS 0.498→0.519, overall 0.750→0.754.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:07:08 -07:00
Abimael MartellandClaude Opus 4.6 830b8d955b feat: detect bold-only lines as section headings
Bold lines at body font size that are standalone (preceded by a paragraph
break) and have ≥3 words are promoted to headings. This catches the
common pattern in academic/technical PDFs where section headings use
bold text at the same size as body text.

Guards against false positives: minimum word count, colon-ending
exclusion (labels like "Table I:").

Benchmark improvement: MHS 0.37→0.50, overall 0.71→0.75.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:56:18 -07:00
Abimael Martell f664f3056d fix: harden hybrid OCR region extraction path (#23)
Align region filtering with rotated-page coordinate rewrites, switch region text assembly to the shared line-grouping pipeline, and retain edge-overlap text to avoid false empty regions that incorrectly trigger OCR fallback. Also make Python region inputs fail fast with clear ValueError messages for malformed boxes.

Made-with: Cursor
2026-04-03 22:48:13 -07:00
Abimael Martell ebb01ab6a3 Merge pull request #22 from firecrawl/perf/skip-truetype-fallback-in-region-extract
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
perf: skip TrueType fallback in extract_text_in_regions (22,000x faster font parsing)
v0.3.2
2026-04-02 18:14:05 -07:00
Abimael MartellandClaude Opus 4.6 53eff94147 Bump napi package version to 0.3.2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 18:13:20 -07:00
Abimael MartellandClaude Opus 4.6 30244078d8 fix: sort panic and missing CID garbage check in extract_text_in_regions_mem
Two bugs in collect_text_in_region / extract_text_in_regions_mem:

1. The threshold-based sort comparator in collect_text_in_region was not
   transitive, causing Rust's sort to panic on certain PDFs. Replaced with
   strict total_cmp ordering — the line-grouping phase already handles
   fuzzy Y matching via threshold.

2. The needs_ocr check was missing is_cid_garbage, so Identity-H fonts
   with CID garbage (C1 control chars, high Latin mojibake) could pass
   all quality checks and be served as real text with needs_ocr=false.

Also adds 7 integration tests for extract_text_in_regions_mem (previously
had zero coverage): basic extraction, Identity-H needs_ocr, multiple
regions, nonexistent page, empty region, invalid input, and a fast-vs-normal
comparison test across all text-based fixtures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 18:13:11 -07:00
Abimael MartellandClaude Opus 4.6 57673ebb69 perf: skip expensive TrueType fallback in extract_text_in_regions
FontCMaps::from_doc can spend 4.5+ seconds decompressing and parsing
large embedded TrueType fonts for CID fonts with sparse ToUnicode
CMaps. For extract_text_in_regions (hybrid OCR pipeline), this is
unnecessary — fonts that can't be decoded cheaply will produce
empty/garbage text, triggering needs_ocr=true and GPU OCR fallback.

Changes:
- Add FontCMaps::from_doc_pages_fast() that skips TrueType font
  fallback parsing (build_fallback_cmap_for_type0) and Identity-H/V
  second pass entirely
- Add FontCMaps::from_doc_pages() for filtered page sets
- extract_text_in_regions_mem uses fast mode
- Restructure fallback chain: try cheap fallbacks first, only attempt
  expensive TrueType parsing when needed and not in fast mode

Benchmark on nihms-1771367.pdf (19-page chemistry paper):
- FontCMaps fast:  201µs
- FontCMaps slow:  4.47s
- 22,000x speedup on font parsing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:13:11 -07:00
Abimael MartellandClaude Opus 4.6 2d1c6f9ff8 Bump napi package version to 0.3.1
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
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
v0.3.1
2026-04-02 15:12:47 -07:00
Abimael Martell 55b32d542e Merge pull request #21 from firecrawl/fix/catch-unwind-napi-panics
fix: catch_unwind in NAPI layer to prevent process abort on panic
2026-04-02 15:12:28 -07:00
Abimael MartellandClaude Opus 4.6 79e53f779d wrap all NAPI functions in catch_unwind to prevent process abort on panic
Rust panics in NAPI modules abort the Node.js process with no chance
to report errors. This wraps every exported function in catch_unwind,
converting panics into JS Error exceptions that can be caught and
reported to Sentry.

Buffer data is extracted to Vec<u8> before the catch_unwind boundary
to satisfy UnwindSafe requirements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:10:15 -07:00
Abimael MartellandClaude Opus 4.6 6dfa370fd4 Bump napi package version to 0.3.0
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
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
v0.3.0
2026-04-02 15:01:59 -07:00
Abimael MartellandClaude Opus 4.6 d3d76d0660 Fix js-bindings artifact path in publish workflow
upload-artifact strips the common napi/ prefix, so files are at
artifacts/js-bindings/index.js not artifacts/js-bindings/napi/index.js.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 14:58:00 -07:00