Skip to content

find_tables(): keep quad corrections on for the line-based finder (#5198) - #5213

Open
thomas-villani wants to merge 5 commits into
pymupdf:mainfrom
thomas-villani:fix-table-quad-corrections
Open

thomas-villani wants to merge 5 commits into
pymupdf:mainfrom
thomas-villani:fix-table-quad-corrections

Conversation

@thomas-villani

Copy link
Copy Markdown

Fixes #5198.

Cause

find_tables() calls TOOLS.set_small_glyph_heights(True) ("we need minimum bboxes") so that every character on a line shares one top and bottom. That setting only takes effect while quad corrections are on: JM_char_quad() returns ch->quad unchanged as soon as g_skip_quad_corrections is set, before it reaches the small-heights code. With corrections off, each character keeps its exact glyph box. A "." then sits about 7pt below the digits around it, extract_text() groups it onto a line of its own (y_tolerance 3), and the gap it leaves becomes a space: "0.74" comes back as "0 74\n.".

The line-based finder never sets the toggle itself, so its output depends on what the process last set it to:

Change

Before make_chars(), turn quad corrections on when no layout boxes are used. The layout path, which turns them off deliberately, is unchanged. The existing finally already restores the caller's value.

test_5198() sets the toggle as an import of pymupdf4llm does and checks the extracted cells, and that find_tables() leaves the toggle as it found it. It fails without the change.

Checks

On the 2.0 wheel with this table.py (2.0 plus #5205's four lines, the only table.py difference between 2.0 and main):

  • tests/test_tables.py: 47 passed, including test_5198. Without the change it is the one failure.
  • 66 PubMed Central articles, every page, use_layout=False, toggle on as after import pymupdf4llm: the output is identical to 1.28.0 (90 tables on 62 pages, every bbox and every cell). Without the change, 85 cells have split decimals, 1,840 cells differ from 1.28.0, and 3 extra tables are found.
  • 11 of those articles with layout: identical to unpatched main, so the layout path does not change.

Not in this PR: the layout path

Turning corrections on for the layout path too fixes its cell text, but it changes detection: in test_4017, rows merge (13 to 8), and in test_paths_param, "Header2" splits across two cells. That suggests the layout path's row segmentation relies on the exact glyph boxes. Its cell text has split decimals since layout support was added (reproduced on 1.28.2 with pymupdf.layout imported), and the layout expectations in tests/test_tables.py updated in 12e17c8 record that output: '2755 9\n.', '269\n-' for -269, '13144%\n.' for 131.44%. Fixing it would need separate character boxes for structure and for text. #5198 has the details.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@thomas-villani
thomas-villani force-pushed the fix-table-quad-corrections branch from 80ad995 to d6480df Compare October 25, 2026 19:48
@thomas-villani

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

…sed finder.

find_tables() sets minimum glyph heights so every char on a line shares
one top and bottom, but those only apply while quad corrections are on.
Importing pymupdf4llm turns them off for the whole process (eagerly on
`import pymupdf` in 2.0), and since pymupdf#5205 find_tables() itself turns them
off before get_layout() whenever use_layout is set. The line-based finder
then built its chars from exact glyph boxes: a "." sits lower than the
digits around it, is grouped onto a line of its own, and "0.74" is
extracted as "0 74\n.".

Turn corrections on before make_chars() when no layout boxes are used.
The layout path, which turns them off on purpose, is unchanged.

tests/test_tables.py: test_5198(): new.

Fixes pymupdf#5198.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thomas-villani
thomas-villani force-pushed the fix-table-quad-corrections branch from d6480df to 87c293b Compare October 25, 2026 19:50
github-actions Bot added a commit that referenced this pull request Oct 25, 2026
@JorjMcKie
JorjMcKie requested a review from veget-able October 27, 2026 08:03
@veget-able

Copy link
Copy Markdown
Collaborator

Approved. With corrections back on, the line-based finder matches the corrections-on output on my 168-page ParseBench sample, and the layout path is unchanged.

@JorjMcKie

Copy link
Copy Markdown
Collaborator

@thomas-villani please resolve the conflicts with the base branch, so we can merge the PR.

The conflict resolution in c9b463a kept only test_5198 at the end of
tests/test_tables.py, which would remove both tests from main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thomas-villani

Copy link
Copy Markdown
Author

Thanks for merging #5243. Since it changes what this PR is for, I re-checked both against current main (28b271b) on real files. Short version: #5243 fixes the split decimals by itself. This PR is still needed for a narrower reason: import pymupdf4llm should not change what find_tables() returns.

Also, I've pushed b4b97fe. My merge from main had dropped test_5175 and test_5241; that commit puts them back. Against main the PR is now additive only: src/table.py +9, tests/test_tables.py +41 (test_5198).

Setup. PyPI 2.1 wheel (MuPDF 1.28.6) with main's src/table.py on top, Windows, Python 3.12. The test set is 8 open-access PMC articles chosen because they are table-heavy, so it is not a random sample: 111 pages, 30 tables. A cell counts as "split" when its last line is only ./, (for example 59.83 (9.07) coming back as 5983(907)\n. .).

#5243 on real files. Number of split cells:

find_tables() (default) find_tables(use_layout=False) after import pymupdf4llm
stock 2.1 737 / 3,239 781 / 3,562
main 0 0
main + this PR 0 0

Why this PR still matters. On main, the same find_tables(use_layout=False) call returns different tables depending on whether pymupdf4llm has been imported. pymupdf4llm turns quad corrections off for the whole process at import time (helpers/document_layout.py:38 and others). On 4 of the 111 pages that changes the output:

Page plain main (no import) main after import pymupdf4llm
PMC12000001 p15 (vertical header text) (95%CI) β β (95%CI) (3 cells)
PMC2000354 p6 (axis labels) 5 / 0\n5\n0… '' / 5\n0\n5\n0… (moves a value between cells)
PMC2000354 p8 (dot leaders) '' '.' and a cell of 62 dots
PMC10000026 p8 (figure detected as a table) Distance\nID₁\nEuklidean… pairwiseEuklideanDistance\nID₁…

With this PR, main gives identical output with or without the import on all 111 pages. On the first three pages it also matches 1.28.2 cell for cell.

I'm not claiming that either reading of the vertical header is the correct one. The point is that importing a library shouldn't change the result.

Two things I noticed about #5243, for whoever reviews table changes. These come from 2.1 with both patches, before main's other recent table commits:

  • PMC9500000 p13: https://github.com/jjmetzger/\norganoid s becomes …organoid_s, which is better.
  • PMC10000026 p8: on the figure detected as a table, ID₁ Distance becomes Distance\nID₁, a change in order. It's a figure, so it's probably harmless.

One thing neither PR touches. The default find_tables() path adds an empty middle column to a plain 2-column fully ruled table, on stock 2.1 and on main:

import pymupdf
d = pymupdf.open(); p = d.new_page(width=595, height=842)
for a, b in [((72,72),(472,72)), ((72,112),(472,112)), ((72,152),(472,152)),
             ((72,72),(72,152)), ((272,72),(272,152)), ((472,72),(472,152))]:
    p.draw_line(a, b)
for t, pt in [("P for interaction",(80,100)), ("Cooking fuel type",(280,100)),
              ("0.74",(80,140)), ("12.5 mg",(280,140))]:
    p.insert_text(pt, t, fontsize=11)
print(p.find_tables().tables[0].extract())
# [['P for interaction', '', 'Cooking fuel type'], ['0.74', '', '12.5 mg']]
print(p.find_tables(use_layout=False).tables[0].extract())
# [['P for interaction', 'Cooking fuel type'], ['0.74', '12.5 mg']]

I can open a separate issue for it if that's more useful.

The articles are open access on PMC (CC BY 4.0, except PMC9500000, which is CC BY-NC-ND): PMC12000001, PMC2000354, PMC10000026, PMC9500000. I'm happy to share the scan script or attach particular PDFs.

@thomas-villani

Copy link
Copy Markdown
Author

@JorjMcKie the conflicts are resolved as of b4b97fe; GH shows the PR as mergeable, with only src/table.py (+9) and tests/test_tables.py (+41) changed against main. Ready whenever you are.

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.

2.0: Table.extract() moves the decimal point out of numbers ("0.74" comes back as "0 74", newline, ".") — regression from 1.28.2

3 participants