From e00950d27a5b0a2e0f253eecfea7d8d632c2b04b Mon Sep 17 00:00:00 2001 From: "Tom Villani, Ph.D." Date: Fri, 6 Nov 2026 12:13:10 -0500 Subject: [PATCH] get_textbox(), get_text_selection(): leave out text hidden by a clip path Both build their own TextPage when none is passed, without TEXT_MEDIABOX_CLIP. Text that a clip path removes from the rendered page then comes back, although get_text() and search_for() leave it out. Add only that flag; the other defaults are unchanged, and an explicit textpage still decides what is extracted. Co-Authored-By: Claude Opus 5.5 --- src/__init__.py | 4 +++- src/utils.py | 9 +++++++-- tests/test_textextract.py | 24 ++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 20345638c..eae4ecaab 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -12881,7 +12881,9 @@ def get_textbox( ) -> str: tp = textpage if tp is None: - tp = page.get_textpage() + # Leave out text that a clip path hides, as get_text() and + # search_for() do. Otherwise the default flags (0) are kept. + tp = page.get_textpage(flags=TEXT_MEDIABOX_CLIP) elif getattr(tp, "parent") != page: raise ValueError("not a textpage of this page") rc = tp.extractTextbox(rect) diff --git a/src/utils.py b/src/utils.py index a0655764e..89fcf7019 100644 --- a/src/utils.py +++ b/src/utils.py @@ -276,7 +276,9 @@ def get_textbox( ) -> str: tp = textpage if tp is None: - tp = page.get_textpage() + # Leave out text that a clip path hides, as get_text() and + # search_for() do. Otherwise the default flags (0) are kept. + tp = page.get_textpage(flags=pymupdf.TEXT_MEDIABOX_CLIP) elif getattr(tp, "parent") != page: raise ValueError("not a textpage of this page") rc = tp.extractTextbox(rect) @@ -295,7 +297,10 @@ def get_text_selection( pymupdf.CheckParent(page) tp = textpage if tp is None: - tp = page.get_textpage(clip=clip, flags=pymupdf.TEXT_DEHYPHENATE) + tp = page.get_textpage( + clip=clip, + flags=pymupdf.TEXT_DEHYPHENATE | pymupdf.TEXT_MEDIABOX_CLIP, + ) elif getattr(tp, "parent") != page: raise ValueError("not a textpage of this page") rc = tp.extractSelection(p1, p2) diff --git a/tests/test_textextract.py b/tests/test_textextract.py index 4b8762349..6c97a3fa7 100644 --- a/tests/test_textextract.py +++ b/tests/test_textextract.py @@ -1033,3 +1033,27 @@ def test_4994(): assert len(search_results) == 2 #assert text == 'planet-warming gas driving climate change.' sys.stdout.flush() + + +def test_textbox_clipped_text(): + # Text hidden by a clip path is not on the rendered page. get_text() and + # search_for() leave it out; get_textbox() and get_text_selection() must + # leave it out too, unless the caller passes a textpage of their own. + src = pymupdf.open() + src_page = src.new_page(width=400, height=200) + src_page.insert_text((20, 50), 'VISIBLE left part', fontsize=12) + src_page.insert_text((250, 50), 'HIDDEN right part', fontsize=12) + + doc = pymupdf.open() + page = doc.new_page(width=400, height=200) + left = pymupdf.Rect(0, 0, 200, 200) + page.show_pdf_page(left, src, 0, clip=left) # clips away the right half + + assert 'HIDDEN' not in page.get_text() + assert page.search_for('HIDDEN') == [] + assert page.get_textbox(page.rect) == 'VISIBLE left part' + assert page.get_text_selection((0, 40), (400, 60)) == 'VISIBLE left part' + + # An explicit textpage still decides what is extracted. + tp = page.get_textpage(flags=0) + assert 'HIDDEN' in page.get_textbox(page.rect, textpage=tp)