Skip to content

feat: rewrite pdf text extraction engine with matrix tracking and geometric layout - #28

Merged
ymettier merged 6 commits into
mainfrom
pdf-stuck
Jul 30, 2026
Merged

feat: rewrite pdf text extraction engine with matrix tracking and geometric layout#28
ymettier merged 6 commits into
mainfrom
pdf-stuck

Conversation

@ymettier

Copy link
Copy Markdown
Owner

Summary

Rewrites the pdftotext package to extract text from PDF content streams using
a matrix-tracked positioned-character pipeline instead of the previous flat char
collector. This fixes extraction quality for bank statement PDFs that
use per-character positioning with standard 14 fonts.

Changes

  • CTM/Tm matrix tracking: tracks the current transformation matrix through
    q/Q (save/restore), cm (matrix concat), Tm (text matrix), Td/TD
    (position offset) operators — essential for PDFs where each character is
    positioned individually via Tj+Td.
  • Geometric line grouping: groups characters into lines by Y-proximity
    (lineGroupTol=2.0), inserts spaces on X-gaps exceeding charSize×0.09.
  • Standard 14 font width tables: ~2000 glyph width entries for Helvetica,
    Times, Courier, Symbol, and ZapfDingbats from pdfminer.six FontMetricsDB.
    Used as fallback when PDF font dicts omit /Widths arrays — the root cause
    of garbled payslip text like "Cotisationm aladie".
  • Latin-1 decoding fix: literal strings route through decodeText so bytes
    0x80-0xFF are decoded to UTF-8; font-metric word gap uses the decoded rune's
    byte value for correct character width lookup.
  • New test fixtures: pdflatex-per-char-text.pdf (pdfTeX per-char layout),
    latin1-word-gap.pdf (Type1 font, Latin-1 accented chars), 12 PDF samples
    from pdfminer.six in testdata/pdfminer-samples/.

Extraction quality

  • Bank statements: correct transaction tables with date/description/amount per
    line (beats pdfminer which interleaves watermark text between real content)
  • 100% statement coverage in pdftotext package, linter clean (0 issues)

ymettier added 6 commits July 30, 2026 11:38
Extract glyph Widths from Type1/TrueType font dicts to detect word
boundaries in PDFs that render each character individually via Tj+Td.

- Fix readKeyword infinite loop on control characters
- Add buildFontWidths() to extract font glyph metrics
- Add flushPending() with font-metric word gap detection using
  previous glyph width and advance comparison
- Extract cidToUint16 helper to reduce byte-shifting duplication
- Move nolint directive to line-scoped on func declaration
- Replace generated test PDF with minimal static per-char-test.pdf
- Add control-char.pdf test fixture
Add pdflatex-per-char-text.pdf from py-pdf/sample-files (CC-BY-SA-4.0)
to verify font metric word gap detection works on genuine pdfTeX output.

Without the fix, pdfTeX-rendered text loses all word spaces
(e.g. "Loremipsumdolorsitamet").
Full rewrite replacing the flat char collector with CTM/Tm matrix
tracking, q/Q save/restore, cm operator, geometric line grouping
(Y-proximity), and gap-based word detection (wordGapRatio=0.09).

Add standard 14 font width tables from pdfminer.six (Helvetica,
Times, Courier, Symbol, ZapfDingbats) as fallback when font dicts
omit /Widths arrays — needed for payslip PDFs using standard fonts.

Fix decodeText to use WriteRune for Latin-1 bytes 0x80-0xFF, route
literal strings through decodeText in all paths, and derive
lastCharCode from the decoded rune for correct font-metric lookup.
@ymettier
ymettier merged commit c2af2d4 into main Jul 30, 2026
5 checks passed
@ymettier
ymettier deleted the pdf-stuck branch July 30, 2026 16:48
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant