Commit Graph
302 Commits
Author SHA1 Message Date
Abimael MartellandClaude Opus 4.6 9bca448da2 fix(tables): Merge continuation tables across page breaks
When consecutive pages each have exactly one table with the same column
count, treat them as a single table spanning multiple pages. Strips the
redundant header+separator rows from continuation pages and appends
their data rows to the first page's table.

Closed-Business-Accounts PDF now produces one 2080-row table instead of
19 separate tables each with their own header.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 13:46:02 -08:00
Abimael MartellandClaude Opus 4.6 b6907494b6 fix(tables): Detect dense body-font tables and fix table-only page rendering
Raise BodyFont max_rows from 100 to 200 so tables with ~115 rows
(like the Closed Business Accounts PDF) pass validation. Add date
pattern recognition (MM/DD/YYYY, YYYY-MM-DD) to looks_like_table_data()
so date columns count toward the 30% data threshold.

Fix a bug where pages whose text is entirely consumed by tables
produced empty output — the post-loop cleanup only checked
current_page (stuck at 0), missing all table content. Now iterates
all pages with uninserted tables/images.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 13:33:41 -08:00
Abimael MartellandClaude Opus 4.6 b2510ffe3e fix(spacing): Refine single-char threshold to distinguish fragments from per-glyph text
The symmetric single-char check (prev==1 OR curr==1) was too aggressive
for PDFs using per-glyph positioning (each letter as a separate item),
causing word spaces to disappear ("EffectiveDate" instead of
"Effective Date").

Now distinguishes three cases:
- Asymmetric (one single-char, other multi-char): generous 0.25 threshold
  for fragment rejoining ("b"+"illion", "C"+"ultural")
- Both single-char numeric: generous 0.25 threshold for number continuity
  ("1"+"0"+"0" within per-glyph numbers)
- Both single-char alphabetic: normal 0.15 threshold to preserve word
  spaces in per-glyph Latin text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 16:45:37 -08:00
Abimael MartellandClaude Opus 4.6 2c825b034d fix(spacing): Reduce spurious spaces in numbers and single-char fragments
Two targeted threshold adjustments in the accurate-width path of
should_join_items():

1. Numeric continuity (0.3x threshold): When adjacent items form a
   number sequence (digits, commas, periods, percent signs), use a
   generous threshold. Fixes splits like "34,20 8" → "34,208" and
   "+13. 0 %" → "+13.0%".

2. Single-character fragments (0.25x threshold): Single-char items from
   per-glyph positioning are almost never standalone words. Fixes splits
   like "b illion", "C ultural", "togeth er".

The general 0.15x threshold is preserved for multi-character items to
avoid regressions in normal word spacing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 13:32:37 -08:00
Abimael MartellandClaude Opus 4.6 579bd4f3b1 fix(headers): Reduce false heading detection and merge split headings
Three changes to improve heading quality:

1. Raise heading threshold from 1.1x to 1.2x base font size, reducing
   false positives where slightly larger body text was promoted to headers.

2. Add word count guard (max 15 words) to skip heading detection for
   long body paragraphs that happen to use a larger font.

3. Add merge_heading_lines() preprocessing that joins consecutive lines
   at the same heading level on the same page (e.g., "About Glenair,
   the Mission-Critical" + "Interconnect Company" → single heading).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 13:29:23 -08:00
Abimael MartellandClaude Opus 4.6 d2c96e2842 fix(cjk): Skip Latin case heuristics for CJK text in fallback spacing path
The fallback estimated-width path in should_join_items() used Latin
case-based thresholds (uppercase/lowercase transitions) to decide word
boundaries. For CJK text emitted as per-glyph items, these heuristics
incorrectly inserted spaces within words (e.g., "ス テ ップ" instead of
"ステップ"). Now detects CJK characters and uses a generous join
threshold, matching the behavior already applied in the accurate-width path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 13:25:22 -08:00
Abimael MartellandClaude Opus 4.6 17634e80f4 fix(tables): Use font-size-adaptive threshold for row boundary detection
Row merging occurred in uniform-spacing PDFs (e.g., 210603_ROOFING_BIDRESP)
because the fixed 10.0pt cluster threshold matched the exact line spacing,
causing adjacent rows to merge in pairs. Now derives threshold from 0.8×
median font size, fixes >= comparison, and raises SmallFont max_rows to 200.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 13:18:41 -08:00
Abimael MartellandClaude Opus 4.6 d7b0149703 fix(columns): Read multi-column PDFs column-by-column instead of interleaving
Store column lines per-column instead of in a flat Vec, and use section-based
merge (spanning items define vertical zones) so each column is read top-to-bottom
before moving to the next column.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 13:05:25 -08:00
Abimael MartellandClaude Opus 4.6 d3d3f27451 chore: Fix clippy warnings in debug binaries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 12:57:49 -08:00
Abimael MartellandClaude Opus 4.6 8c4b9d2d07 fix(text): Improve word spacing, paragraph joining, and page breaks
- Preserve leading whitespace in text_with_formatting() to fix ~80% of
  missing-space issues (e.g. "holdermeans" -> "holder means")
- Tighten word boundary gap threshold from 0.05 to 0.01 and add
  word-count heuristic to distinguish CID word-level operators from
  Type1 line-level operators, fixing spurious spaces (e.g. "t emporary")
- Replace fixed paragraph threshold (1.8x base_size) with dynamic
  median-based computation for double-spaced documents
- Remove --- page break markers between pages
- Fix page number y-threshold for US Letter (720pt vs 800pt)

Eval: +1.4% mean word_sim, zero char_sim regressions across 63 PDFs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 12:55:00 -08:00
Abimael MartellandClaude Opus 4.6 b94df22d37 fix(text): Skip word-boundary heuristic for CJK text
CJK languages (Chinese, Japanese, Korean) don't use spaces between
words. The multi-char word-boundary detection was inserting unwanted
spaces in Japanese text (e.g., "である 履行義務" instead of
"である履行義務"). Skip the heuristic when either item boundary
involves a CJK character.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:40:44 -08:00
Abimael MartellandClaude Opus 4.6 a08fb61cc5 fix(text): Add word boundary detection for CID font missing spaces
Multi-character words from separate Tj operators (CID/Identity-H fonts)
were being joined without spaces when positioned back-to-back (gap≈0).
Detect multi-char items touching at near-zero gap and insert spaces,
while preserving single-char per-glyph joining for running headers.

Also strip null bytes and control characters in expand_ligatures() to
clean Latin-1 fallback artifacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:30:02 -08:00
Abimael MartellandClaude Opus 4.6 f94be6093c chore: Add missing debug_ygaps.rs binary
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:02:04 -08:00
Abimael MartellandClaude Opus 4.6 906051b832 fix(text): Fix ligature corruption and expand ligature characters
Fix Differences encoding path dropping standard characters when only
ligature bytes matched the sparse encoding map. Now combines Differences
entries with Latin-1 fallback for printable bytes instead of using
filter_map which silently dropped unmapped bytes.

Add expand_ligatures() to replace Unicode ligature characters (U+FB00-FB04)
with their ASCII components (ff, fi, fl, ffi, ffl) at all TextItem
creation sites. Add underscore-variant glyph names (f_f, f_f_i, f_f_l).

Eliminates 669 ligature corruptions across the eval suite.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:59:04 -08:00
Abimael MartellandClaude Opus 4.6 56cfd895b5 fix(tables): Prevent paragraph text from being falsely detected as tables
Add pairwise cross-row column alignment check to find_table_regions_strict().
Real tables have fixed column X positions that repeat across rows (score ~1.0),
while paragraph text has varying word positions (score ~0.3-0.4). Regions with
average pairwise alignment score < 0.5 are now rejected.

Also remove the num_cols >= 5 content-check bypass for BodyFont mode, requiring
body-font tables to always have ≥30% data-like cell content.

Fixes false table detection across 9+ PDFs including AI_Cultural_Collections
(-730 false lines), 131212888 (-740), Data-Processing-Agreement (-108),
CEP_DN_Interest_Rates (-93). No regressions on legitimate tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:10:10 -08:00
Abimael Martell d0e14fe4cd improve header detection 2026-02-11 17:22:56 -08:00
Abimael MartellandClaude Opus 4.6 9abb7a9c8b feat(tables): Add two-pass detection for body-font tables
Tables at body font size were invisible because detect_tables() gated on
font_size <= base * 0.90. Add a second pass with stricter structural
criteria (3+ columns, 70% alignment, 3+ rows with 3+ X-clusters) to
detect body-font tables without false-positiving on paragraphs. Also
raise the row limit from 30 to 50/100 to support large data tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 15:02:20 -08:00
Abimael MartellandClaude Opus 4.6 423e969843 fix(columns): Replace X-gap column detection with horizontal projection profile
The previous approach used a 30% page-width gap threshold on sorted X positions,
which never detected real column gutters (typically 2-5% of page width). This
caused text from adjacent columns to be interleaved on the same line (e.g.
"Brookfieldchief", "areclinging").

The new approach builds an occupancy histogram across the page width, finds empty
valleys (gutters), and validates them with vertical consistency checks. Also adds
spanning-item detection so full-width headers/titles are handled correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:29:11 -08:00
Abimael Martell 9078c7bd14 improve spacing detection 2026-02-11 14:12:16 -08:00
Abimael Martell 8d0ce9ea0a fix(encoding): Fix CJK/Unicode encoding bug 2026-02-11 12:33:06 -08:00
Abimael Martell 68643e0c37 fix encoding, and spacing on custom fonts 2026-02-11 12:17:42 -08:00
Abimael MartellandClaude Opus 4.5 cd68515028 Add image and hyperlink extraction, improve line grouping
- Add ItemType enum to distinguish Text, Image, and Link items
- Extract XObject images from page resources with position/dimensions
- Parse Link annotations to extract hyperlinks with URLs
- Add include_images and include_links options to MarkdownOptions
- Fix line grouping to better detect new lines vs same-line items
- Add samples/ and scripts/ to .gitignore

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 08:31:20 -08:00
Abimael Martell 5afbb67b90 Fix list continuation, improve table detection, and add more font styling detection 2026-02-10 13:30:33 -08:00
Abimael Martell 6389c85ab9 fix type 2026-02-09 12:56:08 -08:00
Abimael Martell 3d6772fc20 improve spacing detection, and layout detection 2026-02-09 12:45:08 -08:00
Abimael Martell 0e5e978a33 improve table header detection 2026-02-09 10:13:16 -08:00
Abimael Martell ec96311a65 implement ToUnicode CMap support for proper text extraction from PDFs with custom font encodings 2026-02-09 09:46:07 -08:00
Abimael Martell 99db21810c update readme 2026-02-09 09:05:15 -08:00
Abimael Martell 0f2f890a5c Table Improvements
1. Merge Continuation Rows (clean_table_cells)
2. Footnote Extraction (is_footnote_row)
3. Empty Row Removal
2026-02-08 22:04:25 -08:00
Abimael Martell 5e44e468ac Formatting Improvements
1. Word Fragment Joining (extractor.rs:63-94)

  Added is_word_continuation() to detect when text items are fragments of the same word and should be joined without spaces:

2. Caption Detection (markdown.rs:621-658)

  Added is_caption_line() to detect figures, tables, and source citations:
  - Ensures captions are on their own line with paragraph breaks

3. Paragraph Threshold Adjustment

  Changed from base_size * 2.0 to base_size * 1.8 for better paragraph detection.
2026-02-08 21:58:02 -08:00
Abimael Martell d530f9c888 implement Subscript/Superscript detection 2026-02-07 22:27:54 -08:00
Abimael Martell 4f66cf1a6e clippy happy 2026-02-07 20:16:44 -08:00
Abimael Martell 2953ec8203 fix: Page number filtering, Hyphen joining and Column detection threshold 2026-02-07 20:14:23 -08:00
Abimael Martell 3bbbc63c48 fix merging 2026-02-07 20:05:27 -08:00
Abimael Martell 3548423747 fix sorting 2026-02-07 19:57:05 -08:00
Abimael Martell 25ec142413 improve table detection 2026-02-07 19:50:53 -08:00
Abimael Martell 0acb3ecc8b format tables code 2026-02-07 19:40:28 -08:00
Abimael Martell 15981b7a15 add table detection 2026-02-07 19:39:42 -08:00
Abimael Martell 1fd392a10e fix footnote detection 2026-02-07 19:31:51 -08:00
Abimael Martell 96bdaa9f4a fix sorting, and columns 2026-02-07 18:25:01 -08:00
Abimael Martell c2c528ff23 url formatting, page no detection, additional cleanup 2026-02-07 14:12:19 -08:00
Abimael MartellandClaude Opus 4.5 ca09103ca9 Join paragraph lines with space instead of newlines
Paragraphs now flow as continuous text, with only actual paragraph
breaks (large Y gaps) creating newlines between them.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 13:52:40 -08:00
Abimael MartellandClaude Opus 4.5 5f772071e4 Fix formatting
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 13:48:46 -08:00
Abimael MartellandClaude Opus 4.5 fb80f5490d Add drop cap detection and merging
Drop caps are large decorative first letters that span multiple lines.
Due to PDF coordinate sorting, they often appear after the text they
belong to. This change:

- Detects drop caps: single uppercase char with font size >= 2.5x base
- Finds the paragraph start (first lowercase-starting line after header)
- Merges the drop cap with that line

Example: "G" + "lenair brings..." -> "Glenair brings..."

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 13:45:04 -08:00
Abimael MartellandClaude Opus 4.5 04ee58bb9e Trim leading and trailing whitespace from markdown output
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 13:37:51 -08:00
Abimael MartellandClaude Opus 4.5 c14e26495d Improve text extraction with visual reading order and header detection
Major changes:
- Switch from lopdf.extract_text() to position-aware extraction
- Text is now sorted by visual reading order (top→bottom, left→right)
- Fixed font size calculation to account for text matrix scaling
- Headers are now detected based on font size ratios
- Skip very short text (≤3 chars) for header detection to avoid drop caps

This significantly improves output quality for PDFs with complex layouts.
Before: Title appeared at end, no structure detected
After: Correct reading order, headers marked with # syntax

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 13:34:37 -08:00
Abimael MartellandClaude Opus 4.5 f99d155c52 Fix panic on multi-byte UTF-8 characters in list detection
The previous code used `trimmed.len() >= 2` to check if there were at
least 2 characters, but `len()` returns byte count, not character count.
For multi-byte UTF-8 characters (e.g., "é" which is 2 bytes), this check
would pass but `chars().nth(1)` would return None, causing a panic.

Fixed by using iterator pattern matching to safely extract the first
two characters.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 22:20:17 -08:00
Abimael Martell 286c61d128 fix ci 2026-02-06 21:40:11 -08:00
Abimael Martell 0ef7b2adfc add ci, plus new package 2026-02-06 21:39:21 -08:00
Abimael Martell ee09e96cc4 add tests and readme 2026-02-06 17:58:27 -08:00