diff --git a/tests/resources/test_3478.pdf b/tests/resources/test_3478.pdf new file mode 100644 index 000000000..468278df6 Binary files /dev/null and b/tests/resources/test_3478.pdf differ diff --git a/tests/resources/test_5108.pdf b/tests/resources/test_5108.pdf new file mode 100644 index 000000000..d493a5aa0 Binary files /dev/null and b/tests/resources/test_5108.pdf differ diff --git a/tests/test_clip_page.py b/tests/test_clip_page.py index 170418a93..894e528fb 100644 --- a/tests/test_clip_page.py +++ b/tests/test_clip_page.py @@ -36,3 +36,33 @@ def test_clip(): assert bbox.intersects( rect ), f"Character '{char['c']}' at {bbox} is outside of {rect}." + + +def test_5108(): + print() + path = os.path.normpath(f'{__file__}/../../tests/resources/test_5108.pdf') + def clipped_content(i, clip): + with pymupdf.open(path) as document: + page = document[0] + page.clip_to_rect(clip) + with pymupdf.open(stream=document.tobytes(garbage=3, clean=True)) as document2: + page = document2[0] + ret = b''.join(document2.xref_stream(x) for x in page.get_contents()) + page.draw_rect(clip, (1, 0, 0)) + document2.save(f'{path}.out.{i}.pdf') + return ret + for i, (label, clip) in enumerate([ + ('whole page ', pymupdf.Rect(0, 0, 400, 400)), + ('right half (holds shading) ', pymupdf.Rect(200, 0, 400, 400)), + ('page inset by 1pt ', pymupdf.Rect(1, 1, 399, 399)), + ('1x1 at PDF origin, no shading in it ', pymupdf.Rect(0, 399, 1, 400)), + (' ', pymupdf.Rect(0, 0, 10, 10)), + (' ', pymupdf.Rect(0, 2, 10, 7)), + ]): + content = clipped_content(i, clip) + sh_kept = str(b'sh' in content) + print(f' {label} -> {sh_kept=:5s} {content!r}') + + + + diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 4506a16d5..2408c1fa0 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -2,6 +2,7 @@ """ Test PDF field (widget) insertion. """ +import gc import pymupdf import os from pymupdf import mupdf @@ -570,3 +571,90 @@ def test_5101(): else: assert 0, 'test_5101(): Expected exception from document2.insert_pdf.' +def test_3478(): + print() + print(f'test_3478(): {pymupdf.version=}') + path = os.path.normpath(f'{__file__}/../../tests/resources/test_3478.pdf') + path_out = os.path.normpath(f'{__file__}/../../tests/test_3478_out.pdf') + path_out2 = os.path.normpath(f'{__file__}/../../tests/test_3478_out2.pdf') + + def get_acro_names(document): + ret = list() + obj = mupdf.pdf_dict_getl( + mupdf.pdf_trailer(pymupdf._as_pdf_document(document)), + pymupdf.PDF_NAME('Root'), + pymupdf.PDF_NAME('AcroForm'), + pymupdf.PDF_NAME('Fields'), + ) + assert pymupdf.mupdf.pdf_is_array(obj) + l = pymupdf.mupdf.pdf_array_len(obj) + for i in range(l): + field = pymupdf.mupdf.pdf_array_get(obj, i) + assert pymupdf.mupdf.pdf_is_dict(field) + T = mupdf.pdf_dict_getp(field, 'T').pdf_to_text_string() + ret.append(T) + return ret + + with pymupdf.open(path) as document: + + print(f'test_3478(): Root/AcroForm/Fields/[]/T:') + acro_names = get_acro_names(document) + for acro_name in acro_names: + if acro_name.startswith('sig') or acro_name.startswith('init'): + print(f'test_3478(): {acro_name}') + + print(f'test_3478(): Page widgets:') + for page in document: + # iterate the fields on this page. + widget = page.first_widget + while widget: + name = widget.field_name + # If it's a signature, remove it. + if name.startswith('sig') or name.startswith('init'): + xref = widget.xref + print(f'test_3478(): Calling page.delete_widget() for {name=}.') + widget = page.delete_widget(widget) + else: + widget = widget.next + + document.save(path_out2) + document.ez_save(path_out) + + # 2026-09-10: page.delete_widget() introduces a cyclic dependency which + # results in us leaving a fd open and a warning from conftest.py, so do a + # collection here to keep output clean. + gc.collect() + + + # In the ez_save()'d document: + # + # * Check that deleted widget fields are not in page widgets. + # (This has always been ok.) + # * Check that deleted widget fields are not in Root/AcroForm/Fields[]/T. + # (This appears to have been fixed in pymupdf-1.25.3.) + # + print(f'test_3478(): looking at ez_save document.') + num_still_present = 0 + num_still_present_acro = 0 + + with pymupdf.open(path_out) as document: + + print(f'test_3478(): Page widgets:') + for page_i, page in enumerate(document): + for (widget_i, widget) in enumerate(page.widgets()): + name = widget.field_name + if name.startswith('sig') or name.startswith('init'): + print(f'test_3478(): {page_i=} {widget_i=}: {name}') + num_still_present += 1 + print(f'test_3478(): {num_still_present=}') + + acro_names = get_acro_names(document) + for acro_name in acro_names: + if acro_name.startswith('sig') or acro_name.startswith('init'): + print(f'test_3478(): {i}: {acro_name}') + num_still_present_acro += 1 + print(f'test_3478(): {num_still_present_acro=}') + + assert num_still_present == 0, f'{num_still_present=}' + assert num_still_present_acro == 0, f'{num_still_present_acro=}' +