From 3a752826e647b46e368acead259bf9ecb0894aeb Mon Sep 17 00:00:00 2001 From: Manoj Date: Mon, 26 Oct 2026 18:46:44 +0530 Subject: [PATCH] table: ignore opaque fill+stroke rects with identical colors in lines_strict Treat opaque 'fs' paths whose stroke matches fill like fill-only paths to prevent phantom table rows, and add regression test for #5175. Fixes #5175 --- src/table.py | 11 ++++++++++- tests/test_tables.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/table.py b/src/table.py index eb3aaf412..ffc6eec5c 100644 --- a/src/table.py +++ b/src/table.py @@ -2546,9 +2546,18 @@ def clean_graphics(npaths=None): # and keep those items (see _is_batched_rule). They contribute no # enveloping bbox: bbox_paths holds only the paths whose own rect is # real graphics. + # A fill+stroke path whose stroke has the same color as its fill + # (both opaque) draws no visible border either, so it is treated + # like a fill-only path, e.g. white redaction boxes: issue #5175. + is_invisible_fs = ( + p["type"] == "fs" + and p.get("fill") == p.get("color") + and p.get("fill_opacity") == 1 + and p.get("stroke_opacity") == 1 + ) if ( lines_strict - and p["type"] == "f" + and (p["type"] == "f" or is_invisible_fs) and p["rect"].width > snap_x and p["rect"].height > snap_y ): diff --git a/tests/test_tables.py b/tests/test_tables.py index df2344b15..20944cdd3 100644 --- a/tests/test_tables.py +++ b/tests/test_tables.py @@ -1962,3 +1962,37 @@ 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_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()