Commit Graph
100 Commits
Author SHA1 Message Date
Abimael MartellandClaude Opus 4.6 aa387503c3 fix(fonts): suppress garbage CID text on OCR-flagged pages (#9)
When Identity-H or Type3 fonts lack a ToUnicode CMap and the
CID-as-Unicode passthrough doesn't produce valid text, the raw CID
byte values appear as mojibake (random Latin Extended characters mixed
with C1 control codes).

The detector already flags these pages in pages_needing_ocr, but the
markdown pipeline still emitted the garbage. Now, for TextBased PDFs,
we check each OCR-flagged page's extracted text for CID garbage
(C1 control characters U+0080–U+009F at ≥5% density) and strip items
from pages that fail the check.

This is scoped to TextBased PDFs only — Mixed PDFs flag pages for OCR
due to template images, not font encoding issues.

Adds test fixture (shinagawa_identity_h.pdf) and integration test.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 10:35:45 -07:00
Abimael MartellandClaude Opus 4.6 6c005ef4c2 fix(fonts): CID-as-Unicode passthrough and subscript merging (#8)
* fix(fonts): CID-as-Unicode passthrough and subscript/superscript merging

Add smart CID-as-Unicode passthrough for Identity-H fonts without
ToUnicode maps. Uses /W array median CID heuristic to distinguish
Unicode-CID PDFs (Chromium-generated) from GID-based subsets.

Add merge_subscript_items() pass that merges small-font items (<75%
of dominant font size, ≤4 chars, tightly adjacent) into parent items.
Fixes chemical formulas (NH3, H2O, KClO3), footnote references, and
subscript notation (vf, Hfg, m3/kg) that were previously orphaned
as separate text items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(subscripts): restrict merge to purely numeric text only

Tighten subscript merging to only merge items containing ASCII digits
(0-9). This avoids false positives with ordinal indicators (º), letter
subscripts (sol, vf), and small bullet characters (▶) that caused
table restructuring regressions.

Numeric-only keeps the primary wins: chemical formulas (NH3, H2O),
footnote references, and unit notation (m2, m3).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(subscripts): restrict merge to parent text ending with a letter

Only merge numeric subscripts when the parent item's text ends with
an alphabetic character. Prevents false merges like "33" + "1" in
fractions (33 1/3%), table credit numbers after spaces, and footnote
refs after punctuation (land.1 → land. 1). Chemical formulas (NH3,
H2O, KClO3) still merge correctly since parent ends with a letter.

Reduces pdf-eval regressions from 13 to 2 (both are correct reversions
of over-aggressive footnote merging from the prior commit).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 09:41:10 -07:00
Abimael MartellandClaude Opus 4.6 7f2995ad0c chore: update lopdf to firecrawl fork with uncompressed stream fix
Points to firecrawl/lopdf@edf6279 which fixes decompressed_content()
for streams without a /Filter entry. Our xobjects.rs workaround remains
as a defensive fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 21:15:36 -07:00
Abimael MartellandClaude Opus 4.6 0c17230211 fix(xobjects): fall back to raw bytes for uncompressed Form XObject streams
lopdf's decompressed_content() fails on Form XObjects without a /Filter
entry (uncompressed streams). This caused all body text to be lost in PDFs
generated by pdfrw and similar tools that wrap page content in uncompressed
Form XObjects (e.g. Cambridge University Press excerpts).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 21:07:43 -07:00
Abimael Martell 95b45d154e fix(fonts): fall back to raw stream for uncompressed ToUnicode CMaps
lopdf's decompressed_content() fails with DictKey("Filter") when a
ToUnicode stream has no /Filter entry (uncompressed raw text). Now
falls back to the raw stream.content when decompression fails.

Fixes Identity-H fonts with valid uncompressed ToUnicode CMaps
producing empty text (e.g. neoenergia tabela PDF).
2026-03-20 19:42:11 -07:00
Abimael Martell 2a8e628396 feat(detect): flag Type3-only pages without ToUnicode for OCR
Type3 fonts render each glyph as a custom drawing/bitmap. Without a
ToUnicode CMap the character codes can't be mapped to Unicode, so
extracted text is garbage. Pages using only Type3 fonts are now
excluded from pages_with_text and added to pages_needing_ocr.

Fixes Korean/CJK PDFs using Type3 fonts (e.g. D2Coding) being
classified as TextBased when no usable text can be extracted.
2026-03-20 17:26:20 -07:00
Abimael Martell 672be2e9e5 fix(preprocess): keep first occurrence of repeated headers/footers
Document titles, column headers, and newsletter mastheads that repeat
on every page were being fully stripped by strip_repeated_lines. Now
the first page's occurrence is preserved so the content appears once.

Fixes missing titles like "VOICE OF SOUTH MARION" in tax certificate
PDFs and column headers in IRS forms.
2026-03-20 16:13:57 -07:00
Abimael MartellandClaude Opus 4.6 967b70a788 fix: suppress markdown when all pages have gid-encoded fonts
When every page uses fonts with unresolvable gid-encoded glyphs,
the extracted text is unreliable garbage. Suppress the markdown
output so consumers know to use OCR instead.

Only triggers for gid-encoded fonts specifically, not for other
OCR signals (Identity-H without ToUnicode, template images) where
the text layer may still be partially useful.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 18:37:06 -07:00
Abimael MartellandClaude Opus 4.6 587c4bed95 feat(detect): flag Identity-H fonts without ToUnicode for OCR (#7)
* feat(detect): flag Identity-H fonts without ToUnicode for OCR

Cyrillic (and other non-Latin) PDFs with Type0/Identity-H encoded fonts
and no ToUnicode CMap produce garbage text from direct extraction. Two
fixes:

1. Detector: new `page_has_identity_h_no_tounicode` check adds affected
   pages to `pages_needing_ocr` regardless of PDF classification.
2. Extraction: extend garbage-text safety net to TextBased PDFs — when
   extracted text is <50% alphanumeric, drop the markdown, set
   `has_encoding_issues`, and flag all pages for OCR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: replace integration tests with synthetic unit tests

Remove PDF fixture dependencies from detector and lib tests. Use
in-memory lopdf documents to test Identity-H/ToUnicode detection logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 15:40:48 -07:00
Abimael MartellandClaude Opus 4.6 57560f1692 feat(tables): column-based table detection for borderless layouts
When rect-based, line-based, and heuristic detection all fail to find
tables on a page, try building a table directly from the layout
engine's column boundaries. Handles borderless tabular layouts like
exam/reference grids where columns are defined purely by text
alignment.

Includes header-row column refinement: when a detected column
contains multiple header items, it gets split at the gap between
them to recover the correct number of columns.

Guards against false positives: requires ≥4 columns, ≤40 rows,
>50% multi-column rows, short cells (avg ≤40 chars), no prose
content, no dominant single column, and no structural elements
(≥6 rects or ≥4 lines) on the page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 14:41:30 -07:00
Abimael MartellandClaude Opus 4.6 74d416e8ce feat(detect): flag pages with gid-encoded fonts for OCR
Fonts using raw glyph ID names (gidNNNNN) in their Differences
encoding cannot be decoded to Unicode without the original font's
cmap table. Detect this pattern during font parsing and add
affected pages to pages_needing_ocr so downstream consumers
know to use OCR instead.

Fixes text extraction on PDFs like Tezukuri_Food-Menu.pdf where
the main body font (AcuminVariableConcept) uses gid-encoded
glyphs — even PyMuPDF and ODL fail on these.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 08:36:16 -07:00
Abimael MartellandClaude Opus 4.6 e5a048f674 feat(tables): recover label columns for numeric-only tables
Two changes to improve balance sheet / financial table detection:

1. split_side_by_side: Don't split when one side is text labels
   and the other is numeric data at matching Y positions. This
   prevents splitting a single label+number table into two
   independent regions.

2. try_add_label_column: After detecting a numeric-only table,
   look for unclaimed text items to the left at matching Y
   positions and prepend them as column 0 (row labels).

Tested on IN_Annual_Report_2017 balance sheet which now produces
proper 3-column tables (Label|2016|2017) instead of separated
number tables and paragraph text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:11:51 -07:00
Abimael MartellandClaude Opus 4.6 e6adb29eb8 feat(tables): generate hint regions from failed rect clusters
When rect clusters have valid bounding boxes but insufficient grid
structure (e.g. 2x2 edges from outer borders), emit their bounding
box as a RectHintRegion so the heuristic detector can be scoped to
the table area. Requires reasonable dimensions (100-600pt height,
≤500pt width) and ≥6 text items inside the region.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 16:24:43 -07:00
Abimael MartellandClaude Opus 4.6 dcdb0a39ac fix(detect): detect images embedded in Pattern resources
Screenshot PDFs (e.g., Chrome "Save as PDF") embed images inside
tiling Pattern resources rather than as direct XObject images.
Now traverses Pattern resources during image detection, correctly
classifying these as Mixed with OCR recommended.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:01:12 -07:00
Abimael MartellandClaude Opus 4.6 80f9bb7ab2 fix(fonts): revert TrueType cmap override for fonts with explicit encoding
The third-pass change to extract embedded cmaps for TrueType fonts with
WinAnsiEncoding caused apostrophes and other characters to be dropped
in subsetted fonts where the cmap doesn't cover all glyphs. The declared
encoding is authoritative for these fonts. The OCR text extraction fix
(invisible Tr=3 text) doesn't need this cmap override.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:07:21 -07:00
Abimael MartellandClaude Opus 4.6 258210a821 fix(postprocess): collapse consecutive spaces in extracted text
OCR text layers and some PDF producers emit trailing spaces on each
text item, which combine with gap-based joining to produce double
spaces ("Vice  President"). Now collapses runs of 2+ spaces to single
space within lines, preserving leading indentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:49:43 -07:00
Abimael MartellandClaude Opus 4.6 b8211db4b0 feat: extract invisible OCR text layer from Mixed/template PDFs
Scanned PDFs with OCR text layers use rendering mode 3 (invisible text)
positioned behind page images. Previously we skipped all Tr=3 text.

Now for Mixed/template PDFs, if normal extraction produces garbage or
empty output, we retry with invisible text included. This unlocks text
from OCR-generated PDFs without requiring external OCR.

Also adds Windows Unicode BMP (3,1) subtable support to the TrueType
cmap fallback, and allows TrueType fonts with explicit encoding to
extract their embedded cmap (OCR fonts often lie about encoding).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:39:38 -07:00
Abimael MartellandClaude Opus 4.6 8e674eeef5 docs: add CLAUDE.md with project instructions
Architecture overview, build/test commands, key design decisions,
debugging tips, and coding conventions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:06:49 -07:00
Abimael MartellandClaude Opus 4.6 3ff40987d6 fix(detect): classify tiled scans with garbage OCR as Scanned
Scanned PDFs with JBIG2/tiled image strips were misclassified as
TextBased because no individual image tile exceeded the template
threshold. Now checks aggregate image area per page (≥2M pixels).

Also adds is_garbage_text() check: if a Mixed/template PDF's extracted
text is predominantly non-alphanumeric (<50%), upgrade to Scanned so
callers use proper OCR instead of the garbage text layer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 10:40:30 -07:00
Abimael MartellandClaude Opus 4.6 442a169ddf feat(layout): pre-mask spanning lines for multi-column pages
Multi-item lines (titles, section headers) that span across detected
columns were being split into individual column buckets, corrupting
newspaper detection and reading order. Add identify_spanning_lines()
that groups items by Y-proximity and marks lines wider than 1.3x the
widest column that have no gap at a detected gutter boundary.

Uses column-aware thresholds and gutter-interval gap detection for
precision — only fires on pages with detected columns, preserves
original item order via boolean mask.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:22:19 -07:00
Abimael MartellandClaude Opus 4.6 7c1ddd9ba9 fix: merge consecutive struct-tree heading lines into single heading
When a heading wraps across multiple visual lines, the structure tree
creates separate heading nodes per line, producing broken output like
`## Snow` / `## Lake`. Extend merge_heading_lines to consider struct-tree
roles (H1-H6) in addition to font-size heuristics. A 20-word combined
limit prevents merging body text mis-tagged as headings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 21:26:02 -07:00
Abimael MartellandClaude Opus 4.6 d9c2143c32 feat: tagged PDF structure tree support (#4)
* feat: tagged PDF structure tree support for semantic markdown generation

Parse /StructTreeRoot from tagged PDFs and use semantic roles (H1-H6, P,
LI, BlockQuote, Code, Caption) to improve markdown output. Structure tree
headings add to font-size heuristics without suppressing them. Coverage
threshold (≥50%) ensures only properly tagged PDFs activate this path.

Phase 1: Parse structure tree with role maps, MCID collection, flattening
Phase 2: Capture MCIDs from BMC/BDC operators, tag TextItems
Phase 3: Structure-aware markdown generation in convert loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: accumulate consecutive code lines into single fenced block

Per-line code fencing produced broken markdown for multi-line code
blocks (separate ``` open/close per line). Unify struct-tree Code
role and font-based monospace detection into a single is_code_line
check with in_code_block state for proper accumulation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add tagged PDF fixture with Firecrawl docs content

Synthetic 7-page PDF with rich structure tree exercising H1, H2, H3,
P, Code, LI, Caption, TH, TD roles. Generated via fpdf2 script.
Integration test verifies struct tree parsing and code fence output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove python PDF generator script from repo

Keep the generated fixture PDF but don't track the generator script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: handle malformed bare-name struct types in tagged PDFs

Some PDF generators (e.g. fpdf2) write /S Code instead of /S /Code
in structure elements. lopdf silently drops these objects since bare
tokens are invalid PDF syntax. Add a pre-processor that scans for
known bare struct type names and prepends / before loading.

Unifies path and memory loading through the same fix pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update lopdf dependency to main branch

The firecrawl/zlib-checksum-encrypted branch was merged and deleted.
Point to main which includes all previously merged fixes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: switch lopdf to upstream repo pinned at 845cd3d

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 20:50:24 -07:00
Abimael MartellandClaude Opus 4.6 76ea52680b feat(layout): detect sidebar annotations as newspaper columns
Narrow annotation columns beside wide body columns were being
Y-interleaved with body text, producing garbled reading order.
Add sidebar detection with width ratio, line balance, and sparse
density guards to correctly trigger sequential column reading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:38:00 -07:00
Abimael MartellandClaude Opus 4.6 04aa6d4ae5 feat(text): width-based joining for Canva single-char items
Use character-width ratios instead of font_size for Canva-style PDFs:
- Single-char prev: gap/prev.width < 1.25
- Multi→single: gap/avg_prev_char_width < 1.25
- Multi→multi: page-level threshold (gap/font_size)

Also adds second Canva detection path for per-character rendering
without embedded spaces (>50% single-char items).

Improves ebgt7isj04ophcq word accuracy from ~3% to 64%.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:23:02 -07:00
Abimael MartellandClaude Opus 4.6 152de8b56d feat(text): adaptive join threshold for Canva-style letter-spaced PDFs
Canva-generated PDFs render text character-by-character with CSS-style
letter-spacing (~0.5-0.9× font_size). The hardcoded 0.10 threshold
caused every character to get a space inserted ("K a r i b i b").

Detect Canva pages via fix_letterspaced_items (≥50% items match "a b c"
pattern), compute an IQR-based threshold (median × 1.55) on the gap
distribution BEFORE space removal, then propagate per-page thresholds
through PageThresholds → group_into_lines_with_thresholds → TextLine
→ should_join_items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:54:49 -07:00
Abimael MartellandClaude Opus 4.6 80a3b81ff9 perf(tables): remove cosmetic padding from markdown tables
Drop column-width padding from table output. Saves ~37% bytes on
table-heavy PDFs. Markdown renders identically — padding was purely
cosmetic. Optimizes token efficiency for AI agent consumers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:19:30 -07:00
Abimael MartellandClaude Opus 4.6 ba1fbcbff6 fix(arabic): NFKC normalize presentation forms and reverse visual-order RTL text
Arabic PDFs using ToUnicode maps to presentation forms (U+FB50-FDFF,
U+FE70-FEFE) produced unreadable output. Apply NFKC normalization to
convert back to base Arabic (U+0600-06FF), then reverse character order
within items that had presentation forms (visual→logical order).

Improves Arabic PDF word overlap with Mistral from ~4.5% to ~78.6%.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 09:20:34 -07:00
Abimael MartellandClaude Opus 4.6 7a0e074fa2 feat(tables): detect dense narrow-column tables via gap-histogram analysis
Add bimodal gap analysis to find_column_boundaries for PDFs with many
narrow columns (e.g. 24-column train schedules at 26pt spacing).  When
a clear gap between within-column jitter and between-column spacing is
detected, switch to edge-based clustering with a lower threshold.

Also raise heuristic max columns from 15 to 25, and relax column
consistency validation for very wide tables (>15 cols).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 21:41:16 -07:00
Abimael MartellandClaude Opus 4.6 f2e3d51e49 fix(tables): cap column alignment width at 40 chars to reduce whitespace bloat
Tables with very wide cells (e.g. 230+ chars from merged schedule columns)
caused massive whitespace padding in every row. Capping alignment at 40
characters reduces output size significantly (franklin: 77KB→24KB,
Closed-Business: 571KB→502KB) without affecting content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:02:49 -07:00
Abimael MartellandClaude Opus 4.6 f1b32b320b fix(fonts): prefer TrueType cmap over sequential remap for CJK subset fonts
Subset fonts number GIDs by document encounter order, not sorted order.
The sequential remap scrambled character mappings, losing 2,532+ CJK
characters. Now promotes TrueType cmap fallback when it has more entries
than the primary ToUnicode CMap. Also adds CJK scoring to score_text()
as defense-in-depth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:37:42 -07:00
Abimael MartellandClaude Opus 4.6 cc92e554d0 fix: use lopdf fork with zlib checksum fallback for encrypted PDFs
Points to firecrawl/lopdf branch that falls back to raw deflate when
zlib adler32 checksum is corrupted after RC4 decryption. Fixes 5
encrypted BrokerCheck PDFs that were misclassified as scanned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 15:54:12 -07:00
Abimael MartellandClaude Opus 4.6 e3990dc065 feat(tables): add rect-guided calendar table builder and extractor improvements
Rect-guided tables:
- Add try_build_rect_guided_table() to build tables from rect cluster
  X positions as column boundaries, bypassing heuristic detection
- Split merged multi-number TextItems (e.g. "10 11 12...31") into
  individual day-column cells using column-advancing boundary assignment
- Interpolate missing column boundaries for holiday/non-work days
  that lack colored rects
- Strip tilde-leader noise from cells (legend text bleeding)
- Filter legend text beyond table area via max-X threshold
- Pass cluster_rects through RectHintRegion for downstream use

Extractor improvements:
- Deduplicate clip-path and fill-path rects before using as table hints
- Add font width fallback for missing glyph metrics via average width
- Improve column detection scoring to prefer balanced gutters
- Handle side-by-side layouts with hint-region derived split points

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 15:09:17 -07:00
Abimael MartellandClaude Opus 4.6 6b8f4d0295 test: add inline unit tests to postprocess, format, grid, and detect_rects
Add 48 new tests (140 → 188 total) covering previously untested private
functions with synthetic data. No PDF fixtures needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:49:47 -07:00
Abimael MartellandClaude Opus 4.6 ea4709517e feat(detector): recurse into Form XObjects for image detection and classify vector-text pages
Images nested inside Form XObjects (e.g. HuttoISDWorkPerks.pdf) were
invisible to the detector. Rewrote analyze_page_images to recursively
follow Form XObjects. Also added pages_with_vector_text tracking so
PDFs with vector-outlined text (e.g. qsop_21-0055.pdf) are correctly
classified as IMAGE-BASED with OCR recommended.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 21:12:25 -07:00
Abimael MartellandClaude Opus 4.6 c1357e79ea fix(tables): only retry grid detection when failed due to few non-empty rows
The retry mechanism (excluding page-background rects) was firing for any
grid rejection, including "column 0 is completely empty".  This caused a
regression on HTM_02-01_Part_A where the retry accepted a bad 12x9 grid
instead of falling through to heuristic detection.

Introduce GridResult enum so the caller only retries on FewNonEmptyRows
(the specific symptom of propagate_merged_cells collapse), not on
structural failures like empty columns or low fill ratio.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:16:16 -07:00
Abimael MartellandClaude Opus 4.6 f1dae660a2 fix(tables): retry grid detection excluding page-background rects
PDFs with full-page background fills (e.g. 6_KE_Chart_5-5-17.pdf) had
their table grids rejected because propagate_merged_cells collapsed all
text into row 0.  Add a retry mechanism: normal detection first, then
if it fails and page-origin background rects are present (>=95% width,
>=90% height, near origin), retry with those rects excluded from X-edge
extraction and propagate_merged_cells.

The retry path uses strict validation (50% non-empty rows, 40% content
density, max 200 chars per cell) to avoid false positives on pages where
paragraph text would be incorrectly captured in the grid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:06:15 -07:00
Abimael MartellandClaude Opus 4.6 8b5010bb3a feat(extractor): recurse into nested Form XObjects for text extraction
Pages using nested Form XObject chains (page → wrapper Form → text Form)
were returning 0 text items because extract_form_xobject_text() didn't
handle the Do operator. Add Do/q/Q/cm handling with recursive descent
(max depth 5) and proper CTM tracking through the nesting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 15:09:20 -07:00
Abimael MartellandClaude Opus 4.6 f3c00678b2 fix(detector): raise text-ops threshold on pages with images
Pages with images AND very few text operators (<10) are image pages
with overlay text (headers/footers), not real text pages. This fixes
image-heavy PDFs like newspaper ads being misclassified as TextBased.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 14:37:00 -07:00
Abimael MartellandClaude Opus 4.6 f2869fff30 chore(rust): revert lopdf to upstream main
The firecrawl/fix-cmap-optional-slash branch caused a regression on
the Italian PDF (garbled CMap-encoded text). The EPEVER fix was a
wrong diagnosis — the PDF extracts correctly on upstream main.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:55:03 -08:00
Abimael MartellandClaude Opus 4.6 75fbaddccb feat(detector): detect vector-outlined text pages needing OCR
Count path construction/painting operators (m, l, c, h, f, S, re, etc.)
during the existing content stream scan. Pages where path ops vastly
outnumber text ops (>=1000 path ops AND >200x text ops) contain
vector-outlined glyphs that need OCR instead of text extraction.

This fixes EPEVER-DataSheet-XTRA-N-G3-Series-3.pdf page 2 which has
41,263 path ops but only 33 Tj ops — now correctly classified as Mixed
with page 2 flagged for OCR.

Also removes the check_page_decode_health phase (Phase 1.5) which
required parsing content streams into operations — the path-based
detection is both simpler and more effective.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 00:20:06 -08:00
Abimael MartellandClaude Opus 4.6 2a08386d62 chore(rust): update lopdf to fix-cmap-optional-slash branch
Switches to a new lopdf branch that includes the CMap parser fix for
PDFs with uncompressed ToUnicode streams that omit the leading slash
on metadata keys (CIDSystemInfo, CMapName, CMapType).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:10:30 -08:00
Abimael MartellandClaude Opus 4.6 b4d127eae3 feat(tables): extract rects from filled m/l/h paths for row-stripe tables
PDFs like fare1011.pdf draw table row stripes and borders using
m/l/h/f* filled paths instead of `re` operators. Extract axis-aligned
rectangles from these filled subpaths as a third-priority rect source
(after `re` rects and clip-path rects).

Key changes:
- Track completed subpaths in `h` handler for f/f* rect extraction
- Update W/W* handler to read from pending_subpaths when pending_lines
  is empty (after h clears them)
- Add row-stripe fallback in detect_rects when clustering produces no
  large clusters (non-overlapping row stripes), with ≥15 rects and
  ≥10 rows required to prevent decorative fill false positives
- Revert body-font upper bound to 1.05x (no longer needed at 1.10x)

fare1011.pdf: 12-col heuristic table → 14-col rect-based table with
all fare values correctly captured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:27:01 -08:00
Abimael MartellandClaude Opus 4.6 5ab92e03e3 feat(tables): detect tables from clip-path rects via merged-cluster fallback
Extract axis-aligned rectangles from W/W* clip operators in content
streams — many PDFs define table cells as clipping paths instead of
stroked rects. Add merged-cluster fallback in detect_tables_from_rects()
that merges all cluster rects when per-cluster detection fails or only
produces narrow false-positives (≤3 columns). Uses rect Y-edges for
rows and text X-clustering for columns.

Also updates lopdf to firecrawl fork (fix-leading-whitespace branch).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:12:26 -08:00
Abimael MartellandClaude Opus 4.6 5e260cb1b3 refactor(detector): eliminate double-pass over content streams
Thread a shared HashSet<u8> through scan_content_for_text_operators and
scan_xobjects_in_resources instead of returning a count and re-scanning
with a separate collect_unique_chars_from_content function.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:02:25 -08:00
Abimael MartellandClaude Opus 4.6 c54e29fd6c fix(detector): detect image-dominated PDFs with minimal text as needing OCR
PDFs like wildfire_waiver.pdf render each line as a separate image XObject
with only bullet characters as actual text operators. Add image-dominance
heuristic (image_count > 10 && image_count > text_ops * 3) and unique text
character check (>= 5 unique non-whitespace chars) so these pages are no
longer misclassified as text-based.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:50:21 -08:00
Abimael Martell b126d4b39f fix: remove white text color filtering, extract all visible text
Only skip text with rendering mode 3 (truly invisible). Removes
fill color tracking that was incorrectly hiding white-on-dark text
in presentations and brochures.
2026-03-05 12:06:31 -08:00
Abimael Martell ead4030f31 chore(rust): update lopdf to latest upstream (dc2887a)
Picks up ObjStm xref conflict fix and clippy cleanup.
2026-03-05 11:40:16 -08:00
Abimael Martell cf5da7fe35 fix(pdf): improve side-by-side table split detection
- Lower gap threshold from 40pt to 30pt to catch narrower inter-table gaps
- Constrain candidates to middle 60% of page X range
- Use center-position balance check instead of left-edge counting
- Add balanced-cluster dedup (50pt) to distinguish side-by-side tables
  from single wide tables with multiple column gaps
- Improve filter_rects_to_band with 70% containment check to exclude
  page-wide background stripes
2026-03-04 23:30:35 -08:00
Abimael MartellandClaude Opus 4.6 0dba1e66ed feat: detect side-by-side tables via X-gap pre-splitting
Split pages with two independent table regions (e.g. D'Addario tension
chart) into separate bands before running rect/line/heuristic detection.
The split function finds X-position gaps ≥40pt, validates that <2% of
items cross the split point, and requires ≥30% balance on each side.

Also adds heuristic table detection fallback in compute_layout_complexity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:01:18 -08:00
Abimael MartellandClaude Opus 4.6 d5e03b8915 fix: track sc/scn color operators to avoid skipping visible text
PDFs that set fill color via `sc`/`scn` (named color space operators)
instead of `g`/`rg`/`k` were incorrectly keeping fill_is_white=true
from earlier page background fills, causing all text to be skipped.

Infer color model from operand count: 3=RGB, 4=CMYK, other=non-white.

Fixes extraction of tension_chart PDF (D'Addario string specifications)
which previously output only glyph names.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:04:18 -08:00
Abimael Martell ccd833dd59 remove agents 2026-03-04 14:05:38 -08:00
Abimael MartellandClaude Opus 4.6 7d73c6a1ce refactor: add PageExtraction type alias for extraction return tuples
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:27:31 -08:00
Abimael MartellandClaude Opus 4.6 f7afc0c439 feat: improve rect-based table detection for wide statistical tables
- Add width-based outlier filter to remove page-spanning clipping paths
  without losing row-stripe background rects
- Deduplicate sub-rects (cell-internal decorations) to prevent spurious
  Y-edge splits, with height constraint to preserve row-stripe patterns
- Raise column limit from 12 to 25 for statistical lookup tables (MWU,
  chi-square)
- Skip propagate_merged_cells for wide tables (>10 cols) where spanning
  rects are background fills, not true merged cells
- Add numeric cell check to continuation-row heuristic so short text
  labels (e.g. "Liquid", "Vapour") are still merged while numeric data
  rows are kept separate
- Raise row-stripe content density threshold from 25% to 40% to reject
  false-positive tables from alternating-shade prose sections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:05:58 -08:00
Abimael MartellandClaude Opus 4.6 2b5fcd7bbb feat: detect tables from PDF line path operators (m/l/S)
Many IRS forms and government PDFs draw table gridlines using path
operators (m/l/S) instead of rectangle (re) operators. This adds
line-based table detection to capture these tables.

- Add PdfLine type for line segments from path operators
- Capture m/l/h/S/s/B/b/f/n path operators in content_stream.rs
- Thread Vec<PdfLine> through extraction pipeline
- New detect_lines.rs: classify lines, snap to grid, validate and
  assign items with extensive false-positive filters
- Integrate in markdown pipeline: rects first, then lines as fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 12:58:59 -08:00
Abimael MartellandClaude Opus 4.6 8adfacd3e3 feat: enhance header/footer stripping with Y-band coalescing and page number normalization
Strip leading/trailing digit sequences (page numbers) before frequency
comparison so headers like "Page 5" and "Page 6" are treated as identical.
Group TextLines at the same Y position into Y-bands and propagate removal
to all siblings when any member is stripped. Increase EDGE_LINE_COUNT from
4 to 5 to cover 5-row form column headers (e.g., IRS p1244).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:46:51 -08:00
Abimael MartellandClaude Opus 4.6 eca2db119c fix: increase edge line count to catch form column headers as repeated content
Bumps EDGE_LINE_COUNT from 3 to 4 to strip repeated form column headers
that sit just inside the page margin (4th-from-edge Y position).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:14:47 -08:00
Abimael MartellandClaude Opus 4.6 7b6bb69feb feat: strip repeated headers/footers from markdown output
Add strip_repeated_lines() preprocessing step that removes running
headers and footers before markdown conversion. Uses multiple guard
rails to avoid false positives:

- Edge-line detection: only considers lines among the first/last 3
  distinct Y positions on each page (not percentage-based margins)
- Y-position consistency: requires low variance across pages (stddev
  < 5% of page span) to distinguish headers from scattered table content
- Position-aware removal: only strips instances at page edges, preserving
  body content that happens to match a header/footer text
- Skips structural lines (headings, list items), short lines (<10 chars),
  and decorative separators (repeated single characters)
- Frequency threshold: >= max(3, page_count * 30%) distinct pages

New strip_headers_footers option on MarkdownOptions (default: true).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:04:17 -08:00
Abimael Martell 8f652b81a9 update lopdf 2026-03-02 10:12:59 -08:00
Abimael MartellandClaude Opus 4.6 0aa4e0a9bc Point lopdf at firecrawl/lopdf firecrawl-fixes branch
Includes %%EOF boundary fix and inline image skip fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:29:54 -08:00
Abimael MartellandClaude Opus 4.6 3a1ada1c6b fix(reader): point lopdf at fork with %%EOF boundary fix (abimaelmartell/lopdf@88f7d9e)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:07:06 -08:00
Abimael Martell a2eab803cf update readme 2026-02-25 13:54:42 -08:00
Abimael MartellandClaude Opus 4.6 f37c911e8e refactor: redesign public API for better usability
- Remove `process_mode` from `MarkdownOptions` (it controlled the
  pipeline, not markdown formatting)
- Remove dead `text` field from `PdfProcessResult`
- Add `PdfOptions` builder consolidating mode, detection, markdown,
  and page filter configuration
- Add convenience functions: `detect_pdf()`, `detect_pdf_mem()`,
  `process_pdf_with_options()`, `process_pdf_mem_with_options()`
- Eliminate double document parsing: load once, share between
  detection and extraction via internal `pub(crate)` helpers
- Deprecate old `process_pdf_with_config*` functions (kept as shims)
- Update binaries to use new API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:00:21 -08:00
Abimael MartellandClaude Opus 4.6 4ed73f7c3b Point lopdf at upstream repo (J-F-Liu/lopdf@ae7c584)
The R6 encryption fix has been merged upstream, so switch from the
fork back to the canonical repository.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:05:38 -08:00
Abimael MartellandClaude Opus 4.6 e72152ce63 Normalize typographic spaces (U+2000–U+200A) to ASCII space
EM SPACE from PDF ActualText entries was lost by text.trim(), breaking
spacing after bullets and numbered list markers. Normalizing to ASCII
space lets should_join_items detect word boundaries naturally. NBSP
(U+00A0) is excluded as it's handled by coordinate-based spacing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 12:08:24 -08:00
Abimael MartellandClaude Opus 4.6 1afc1fdaa2 Strip invisible Unicode chars and expand ligatures on ActualText path
Soft hyphens, zero-width spaces, BOM, ZWNJ/ZWJ, and word joiners now get
stripped in expand_ligatures(). Also call expand_ligatures() on the ActualText
code path which was the only TextItem creation site missing it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 16:00:08 -08:00
Abimael MartellandClaude Opus 4.6 93c5c21384 Point lopdf at fork with R6 padded /O /U fix
Use abimaelmartell/lopdf firecrawl/fix-r6-encryption branch which
accepts /O and /U values longer than 48 bytes in V5/R6 encrypted PDFs.
This unblocks 4 encrypted PDFs in the bench suite that were previously
rejected with InvalidHashLength.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 14:28:02 -08:00
Abimael MartellandClaude Opus 4.6 433132d195 Handle encrypted PDFs with empty user password retry
Map lopdf's Unimplemented("encrypted...") error to PdfError::Encrypted
instead of falling through to PdfError::Parse. Retry all Document::load
calls with an empty password when encryption is detected, so
owner-password-only PDFs can be opened. If the retry also fails, the
user now sees a clear "PDF is encrypted" message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 10:25:42 -08:00
Abimael MartellandClaude Opus 4.6 1bacf34bd0 Add tests for row-stripe table detection
Unit test verifying short sub-header rows (e.g. month names) are not
merged as continuation rows in table formatting. Snapshot test for
2013_app2.pdf pinning the full row-stripe table output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:43:23 -08:00
Abimael MartellandClaude Opus 4.6 dcbea3f652 Add row-stripe rect fallback for table detection
When PDF tables use full-width alternating row shading (row-stripe rects),
the normal rect grid detection fails because all rects share the same X/width,
collapsing to ~1 column. Add a fallback that uses rect Y-edges for rows and
text X-position clustering for columns, with a lower 15pt threshold to
separate narrow columns like row numbers and dates.

Also fix continuation-row merging in table formatting to not merge short
single-cell rows (≤5 chars) that are section sub-headers (e.g. month names).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 09:39:25 -08:00
Abimael MartellandClaude Opus 4.6 43c9a81388 Lower dollar-as-space threshold to catch chest-wall PDF pattern
The chest-wall PDF uses $ as separator but also has trailing $ after
spaces, giving only 13.7% letter-dollar-letter ratio (594 of 4332).
Add absolute count threshold (>20) alongside the ratio check so both
concentrated and dispersed substitution patterns are caught.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 08:59:19 -08:00
Abimael MartellandClaude Opus 4.6 8725e2b487 Detect broken font encodings and flag for OCR fallback
Add has_encoding_issues field to PdfProcessResult that detects garbled
text from broken ToUnicode CMaps (U+FFFD replacement characters or
systematic dollar-as-space substitution). Surfaced in JSON output so
clients can fall back to OCR for affected PDFs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 22:06:29 -08:00
Abimael MartellandClaude Opus 4.6 a7dbb16e86 Strip PUA F000-F0FF characters from decoded text
Symbol/Wingdings fonts often map codes to PUA (U+F000-F0FF) via ToUnicode
CMaps, producing invisible/tofu characters in markdown output. Add
clean_symbol_pua() post-processing to extract_text_from_operand() that
converts these to standard Unicode: bullets to U+2022, checkmark to U+2713,
and ASCII/Latin-1 range by stripping the F000 offset.

Eliminates 1,405 PUA characters across 25+ eval PDFs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 22:42:34 -08:00
Abimael MartellandClaude Opus 4.6 a4a2e1f267 Fix PUA glyph decoding, Symbol font cmap mapping, and UTF-8 detection
- Strip PUA F000 offset for "uniF0XX" glyph names (e.g. uniF072 → 'r')
- Use Mac Roman/Symbol cmap subtables for proper code→GID→Unicode mapping
  instead of assuming GID equals character code in subsetted fonts
- Skip fallback CMap building for fonts with explicit encoding
- Move UTF-8 detection before lopdf single-byte encoding decoder

Fixes systematic letter substitutions (CITY→CITQ), missing French letters,
and UTF-8 mojibake (José→José). Eval: missing_text -32%, encoding_issue 7→4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:54:12 -08:00
Abimael Martell 97910e86bf fix specific decoding issues 2026-02-20 21:16:12 -08:00
Abimael Martell b23d073553 Update real-estate-pricing snapshot 2026-02-20 19:45:09 -08:00
Abimael Martell 7d6dae66df Escape invisible glyph literals 2026-02-20 19:15:52 -08:00
Abimael Martell cbad087ec7 Fix control character literals in glyph map 2026-02-20 19:14:09 -08:00
Abimael Martell 18a01d366a Inline full glyph map and refine encoding fallbacks 2026-02-20 19:13:38 -08:00
Abimael Martell 7775d35207 replace range check with includes 2026-02-20 18:57:32 -08:00
Abimael Martell 7b7bff60ea Fix encoding fallbacks for ASCII and symbol fonts 2026-02-20 18:54:53 -08:00
Abimael MartellandClaude Opus 4.6 65e305d544 feat: Improve CMap handling with binary CMap support, inline ToUnicode, and fallback decoding
- Add pdf.js binary CMap (bcmap) parser for Japan1/GB1/CNS1 CID fonts
- Support inline ToUnicode streams (not just object references)
- Add CMapDecisionCache for heuristic primary vs remapped CMap selection
- Build fallback CMaps from embedded font data and CIDSystemInfo
- Handle simple fonts without ToUnicode via embedded font cmap
- Add Symbol/Wingdings/ZapfDingbats font decoding fallback
- Support usecmap chaining in both text and binary CMap formats
- Scan XObject Forms for text operators in detector
- Change default detection strategy from EarlyExit to Sample(8)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:47:10 -08:00
Abimael MartellandClaude Opus 4.6 66c88a4fc9 fix(tounicode): Remap broken ToUnicode CMaps from subset fonts with GID mismatch
Some PDF generators subset-embed fonts by renumbering GIDs sequentially
but fail to update the ToUnicode CMap, which still references original
GID values. Detect this mismatch (Identity-H, no CIDToGIDMap, W array
starts at CID ≤ 2 but CMap min CID > 2) and remap to sequential positions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 14:12:30 -08:00
Abimael MartellandClaude Opus 4.6 23056dc5ba fix: Properly escape JSON string output in CLI binaries
The hand-rolled JSON escaping only handled \, ", and \n but missed
tabs, carriage returns, and other control characters (U+0000..U+001F),
producing invalid JSON that Python's json.loads would reject. Add a
proper json_escape() function covering all required escapes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 10:52:31 -08:00
Abimael MartellandClaude Opus 4.6 7a5af1e9c3 feat: Add configurable ProcessMode (detect-only / analyze / full)
Introduces a ProcessMode enum that controls how far the PDF pipeline
runs, enabling fast document triage without paying extraction or
markdown conversion costs. Exposed via --detect-only and --analyze
flags in both pdf2md and detect-pdf CLIs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 10:06:30 -08:00
Abimael MartellandClaude Opus 4.6 e62444fee0 feat: Add layout complexity detection (tables and multi-column)
Add LayoutComplexity struct to PdfProcessResult so callers can detect
when a PDF has complex layout (tables or multi-column text) and decide
whether to use the extracted markdown or fall back to OCR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:38:50 -08:00
Abimael MartellandClaude Opus 4.6 0cf80025f6 fix(tables): Prevent graph labels from merging into adjacent tables
When a PDF page has a graph directly above a table (e.g. Figure 2 above
Table II in real-estate-pricing.pdf), the heuristic table detector would
merge graph axis labels and legend items into the table, producing a
corrupted result.

Add rect hint regions: when a small cluster of cell-border rects (4-6)
fails full grid validation (e.g. only row borders, no column dividers),
extract their Y bounding box as a "hint region". The heuristic detector
then runs separately on items inside vs outside hint regions, preventing
unrelated content from being merged into tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 18:03:25 -08:00
Abimael MartellandClaude Opus 4.6 b6828fff8c fix(tables): Handle vertically-merged cells and protect header from continuation merge
Rects spanning multiple grid rows (e.g. classification labels) now have
their text consolidated into the first sub-row via propagate_merged_cells().
Also prevent continuation-row merge from folding body data into the header.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:40:57 -08:00
Abimael MartellandClaude Opus 4.6 28313e1f2d test: Add snapshot regression tests with PDF fixtures
Add 5 stripped public-domain PDF fixtures and golden markdown snapshots
for CI regression testing. Fix non-deterministic output caused by
HashMap iteration order in font stats, table heuristics, and rect
clustering by adding deterministic tie-breaking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:13:55 -08:00
Abimael MartellandClaude Opus 4.6 ceb0cf5913 fix(tables): Increase rect edge snap tolerance to fix table detection
The snap_edges tolerance of 2.0 was too tight for PDFs where header and
body cell boundaries differ by ~3pt due to cell padding. This created
phantom empty columns that caused valid tables to be rejected. Increase
snap and cell coverage tolerances from 2.0/3.0 to 6.0. Also add debug
logging to rect-based table detection pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 15:49:19 -08:00
Abimael MartellandClaude Opus 4.6 421690c7c4 fix(fonts): Fix decoding for custom encodings and CID Korean fonts
Merge CMap and Differences encoding at the byte level for single-byte
fonts, fixing garbled output when partial ToUnicode CMaps caused Latin-1
fallback to block the Differences path. Add Adobe-Korea1 CID-to-Unicode
predefined mapping for Identity-H CID fonts without ToUnicode streams,
and support TrueType cmap extraction via ttf-parser for embedded fonts.
Also handle suffixed glyph names (e.g. zero.tf, a.ss01) per Adobe spec.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 15:32:11 -08:00
Abimael MartellandClaude Opus 4.6 42c1d639f2 chore: Pin lopdf to commit 0387137b
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:35:03 -08:00
Abimael MartellandClaude Opus 4.6 a00ce46ab0 refactor(tounicode): Replace raw byte scanning with lopdf document model for CMap extraction
Eliminates double-parsing of PDFs by using the lopdf document model exclusively
for ToUnicode CMap extraction. The old raw byte scanner parsed PDFs separately
from lopdf and only handled FlateDecode, while lopdf handles FlateDecode + LZW +
ASCII85. The new approach walks page fonts and Form XObject fonts via the document
API, yielding ~7x speedup on text-heavy PDFs and ~1.2x overall.

- Add FontCMaps::from_doc() with recursive Form XObject font walking
- Remove ~270 lines of raw byte scanning code (from_pdf_bytes, extract_stream_from_raw_pdf, etc.)
- Remove flate2 dependency (lopdf handles decompression internally)
- Remove dead CMap lookup fallback branches (by_name, get_with_obj, base_font_name)
- Remove unused font_base_names parameter from extract_text_from_operand
- Skip U+FFFD replacement characters in CMap decode (PDF notdef glyph markers)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 13:33:32 -08:00
Abimael Martell 64d2438e2e chore(debug): Add debugging binary for position issues 2026-02-18 10:38:34 -08:00
Abimael Martell 5401354f2e add logging 2026-02-18 10:34:32 -08:00
Abimael Martell 7328af91bd chore(refactor): Better organization of the codebase, split modules, update docs, add AGENTS.md 2026-02-18 10:16:11 -08:00
Abimael MartellandClaude Opus 4.6 e9c0737bd8 feat(extractor): Sequential newspaper-style column reading order
Detect newspaper-style two-column layouts (dense independent text
flows) and emit columns sequentially instead of Y-interleaving them.
This fixes academic papers where column text was being merged across
columns at the same Y position.

Key changes:
- Add is_newspaper_layout() heuristic (≥15 lines/col, >50% Y-collision)
- Add split_column_stragglers() to separate core column clusters from
  header remnants and per-word items that landed in column buckets
- Filter wide items (>60% page width) from column detection histogram
  so full-width text doesn't fill the gutter and prevent detection
- Emit: above spanning → core columns sequentially → column stragglers
  sequentially → below spanning items
- Fix markdown paragraph break to detect backward Y jumps (y_gap.abs())
  so sequential columns on the same page get proper paragraph breaks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 21:20:45 -08:00
Abimael MartellandClaude Opus 4.6 dca267ac44 feat(extractor): Add RTL text support, vertical WMode, and Korean Hangul
Detect RTL scripts (Hebrew, Arabic, Syriac, etc.) by Unicode ranges and
sort line items by X descending when a line is predominantly RTL. Fix
gap calculation in should_join_items() to be direction-neutral. Extract
WMode from Type0 font dictionaries for vertical text foundation. Add
missing Korean Hangul ranges to is_cjk_char().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:42:04 -08:00
Abimael MartellandClaude Opus 4.6 0f53de0387 feat(tables): Cluster rects with union-find for multi-table detection
PDFs like privacy notices have 6-7 distinct visual table sections per
page, each with its own set of cell rects. Previously all rects were
merged into a single giant sparse grid that failed validation. Now
spatially connected rects are clustered via union-find before running
grid detection independently on each cluster.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:11:00 -08:00
Abimael MartellandClaude Opus 4.6 e7455a0a3c fix(markdown): Add st ligatures and preserve TOC line breaks
- Add U+FB05/FB06 (ſt/st→st) to ligature table matching PyMuPDF's full set
- Refactor expand_ligatures to single-pass match loop (clippy fix)
- Detect dot-leader lines (TOC entries) and prevent list continuation
  merging, which was joining sub-entries onto parent numbered items
- Force newline breaks between consecutive dot-leader lines in paragraphs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 15:32:47 -08:00
Abimael MartellandClaude Opus 4.6 e403acd7f7 fix(extractor): Y-interleave multi-column merge for correct line breaks
Replace section-based column merge (which emitted all left-column lines
then all right-column lines) with Y-interleaved merge that combines
lines at the same Y position from different columns into a single line.

This fixes tabular layouts (like court dockets) where rows span across
columns — previously hearing descriptions were detached from their
entries and line breaks between rows were lost.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 13:26:50 -08:00