From 87c293b93dc8150b0c64d4a390b83904e5219767 Mon Sep 17 00:00:00 2001 From: "Tom Villani, Ph.D." Date: Sun, 25 Oct 2026 15:48:47 -0400 Subject: [PATCH 1/2] src/table.py: find_tables(): keep quad corrections on for the line-based 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 #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 #5198. Co-Authored-By: Claude Opus 5.5 --- src/table.py | 9 +++++++++ tests/test_tables.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/table.py b/src/table.py index eb3aaf412..ae35e7f92 100644 --- a/src/table.py +++ b/src/table.py @@ -3225,6 +3225,15 @@ def find_tables( tset = TableSettings.resolve(settings=settings) page.table_settings = tset + if not boxes: + # Without layout boxes this is the line-based finder, whose + # characters need the minimum glyph heights set above. Those + # only take effect with quad corrections on; without them each + # char keeps its exact glyph box, a "." sits lower than the + # digits around it and is grouped onto a line of its own + # ("0.74" becomes "0 74\n."). Turn corrections on, whatever + # get_layout() above or an import of pymupdf4llm left them at. + pymupdf.TOOLS.unset_quad_corrections(False) TEXTPAGE = make_chars(page, clip=clip) # create character list of page make_edges( page, diff --git a/tests/test_tables.py b/tests/test_tables.py index df2344b15..70d16130a 100644 --- a/tests/test_tables.py +++ b/tests/test_tables.py @@ -1962,3 +1962,45 @@ def test_find_tables_union_keeps_one_coherent_table_group_without_content_suppor assert (tables[0].row_count, tables[0].col_count) == (1, 2) finally: doc.close() + + +def test_5198(): + """Cell text keeps decimal points when quad corrections are off. + + Importing pymupdf4llm turns quad corrections off for the whole process. + find_tables() then built its characters from exact glyph boxes, so the + "." of "0.74" sat lower than the digits, was grouped onto a line of its + own and the cell read "0 74\\n.". + """ + doc = pymupdf.open() + page = doc.new_page(width=595, height=842) + x0, y0, x1, y1, xm, ym = 72, 72, 472, 152, 272, 112 + for p1, p2 in [ + ((x0, y0), (x1, y0)), + ((x0, ym), (x1, ym)), + ((x0, y1), (x1, y1)), + ((x0, y0), (x0, y1)), + ((xm, y0), (xm, y1)), + ((x1, y0), (x1, y1)), + ]: + page.draw_line(p1, p2) + for text, point in [ + ("P for interaction", (80, 100)), + ("Cooking fuel type", (280, 100)), + ("0.74", (80, 140)), + ("12.5 mg", (280, 140)), + ]: + page.insert_text(point, text, fontsize=11) + + quad_before = bool(pymupdf.TOOLS.unset_quad_corrections()) + pymupdf.TOOLS.unset_quad_corrections(True) # as importing pymupdf4llm does + try: + tables = page.find_tables(use_layout=False).tables + assert [t.extract() for t in tables] == [ + [["P for interaction", "Cooking fuel type"], ["0.74", "12.5 mg"]] + ] + # find_tables() restores the setting it found + assert pymupdf.TOOLS.unset_quad_corrections() is True + finally: + pymupdf.TOOLS.unset_quad_corrections(quad_before) + doc.close() From b4b97fe432947c3824897e95663f20ac257de1d4 Mon Sep 17 00:00:00 2001 From: "Tom Villani, Ph.D." Date: Fri, 6 Nov 2026 09:12:03 -0500 Subject: [PATCH 2/2] Restore test_5175 and test_5241 dropped in merge from main The conflict resolution in c9b463ac1 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 --- tests/test_tables.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_tables.py b/tests/test_tables.py index 70d16130a..a7c76a607 100644 --- a/tests/test_tables.py +++ b/tests/test_tables.py @@ -1964,6 +1964,60 @@ def test_find_tables_union_keeps_one_coherent_table_group_without_content_suppor doc.close() +def test_5175(): + """lines_strict: an opaque fill+stroke rectangle whose stroke has the same + color as its fill draws no visible line and must not add a phantom row.""" + + def make_page(doc, color): + page = doc.new_page(width=600, height=350) + xs, ys = [50, 200, 300, 400, 500], [50, 100, 150, 200] + for x in xs: + page.draw_line((x, ys[0]), (x, ys[-1]), color=(0, 0, 0), width=0.5) + for y in ys: + page.draw_line((xs[0], y), (xs[-1], y), color=(0, 0, 0), width=0.5) + for row in range(3): + for col in range(4): + page.insert_text( + (xs[col] + 10, ys[row] + 25), f"{row+1}:{col+1}", fontsize=10 + ) + page.draw_rect((301, 185, 313, 198), color=color, fill=(1, 1, 1), width=1) + return page + + doc = pymupdf.open() + # same-colored (invisible) border: ignored like a fill-only rectangle + page = make_page(doc, (1, 1, 1)) + tables = page.find_tables(strategy="lines_strict", use_layout=False).tables + assert len(tables) == 1 + assert (tables[0].row_count, tables[0].col_count) == (3, 4) + + # contrasting (visible) border: still contributes lines + page = make_page(doc, (0, 0, 0)) + tables = page.find_tables(strategy="lines_strict", use_layout=False).tables + assert len(tables) == 1 + assert tables[0].row_count == 4 + doc.close() + + +def test_5241(): + """Ensure that 'to_pandas()' leaves column headers untouched.""" + doc = pymupdf.open() + page = doc.new_page() + page.draw_rect([50, 50, 200, 100]) + page.draw_line([50, 75], [200, 75]) + page.draw_line([125, 50], [125, 100]) + page.insert_text([60, 65], "Total") + page.insert_text([135, 65], "Total") + page.insert_text([60, 90], "10") + page.insert_text([135, 90], "20") + + tabs = page.find_tables() + tab = tabs[0] + names_before = list(tab.header.names) + tab.to_pandas() + names_after = list(tab.header.names) + assert names_before == names_after, "Column headers changed by to_pandas()" + + def test_5198(): """Cell text keeps decimal points when quad corrections are off.