diff --git a/.github/workflows/test_quick.yml b/.github/workflows/test_quick.yml index e1a782ffa..8a95d837e 100644 --- a/.github/workflows/test_quick.yml +++ b/.github/workflows/test_quick.yml @@ -36,4 +36,4 @@ jobs: import subprocess import sys subprocess.run(f'git clone https://github.com/ArtifexSoftware/aptest.git', shell=1, check=1) - subprocess.run(f'{sys.executable} aptest/aptest.py -a APTEST_MUPDF_LOCATION -p . --4llm git: --layout git: build test -t p -a APTEST_args', shell=1, check=1) + subprocess.run(f'{sys.executable} aptest/aptest.py -a APTEST_MUPDF_LOCATION -p . --4llm git: --layout git: build test -t p -e PYMUPDF_TEST_QUICK=1 -a APTEST_args', shell=1, check=1) diff --git a/tests/resources/test_5118_expected_class_fill.png b/tests/resources/test_5118_expected_class_fill.png new file mode 100644 index 000000000..acf087a19 Binary files /dev/null and b/tests/resources/test_5118_expected_class_fill.png differ diff --git a/tests/resources/test_5118_expected_element_fill.png b/tests/resources/test_5118_expected_element_fill.png new file mode 100644 index 000000000..acf087a19 Binary files /dev/null and b/tests/resources/test_5118_expected_element_fill.png differ diff --git a/tests/resources/test_5118_expected_gradient.png b/tests/resources/test_5118_expected_gradient.png new file mode 100644 index 000000000..f263e8b7e Binary files /dev/null and b/tests/resources/test_5118_expected_gradient.png differ diff --git a/tests/resources/test_5118_expected_gradient_percent_coordinates.png b/tests/resources/test_5118_expected_gradient_percent_coordinates.png new file mode 100644 index 000000000..f263e8b7e Binary files /dev/null and b/tests/resources/test_5118_expected_gradient_percent_coordinates.png differ diff --git a/tests/resources/test_5118_expected_gradient_percent_stops.png b/tests/resources/test_5118_expected_gradient_percent_stops.png new file mode 100644 index 000000000..f263e8b7e Binary files /dev/null and b/tests/resources/test_5118_expected_gradient_percent_stops.png differ diff --git a/tests/resources/test_5118_expected_gradient_stop_style.png b/tests/resources/test_5118_expected_gradient_stop_style.png new file mode 100644 index 000000000..f263e8b7e Binary files /dev/null and b/tests/resources/test_5118_expected_gradient_stop_style.png differ diff --git a/tests/resources/test_5118_expected_gradient_user_space.png b/tests/resources/test_5118_expected_gradient_user_space.png new file mode 100644 index 000000000..f263e8b7e Binary files /dev/null and b/tests/resources/test_5118_expected_gradient_user_space.png differ diff --git a/tests/resources/test_5118_expected_inline_fill_control.png b/tests/resources/test_5118_expected_inline_fill_control.png new file mode 100644 index 000000000..acf087a19 Binary files /dev/null and b/tests/resources/test_5118_expected_inline_fill_control.png differ diff --git a/tests/resources/test_5118_expected_solid_fill_control.png b/tests/resources/test_5118_expected_solid_fill_control.png new file mode 100644 index 000000000..0a06bd9ea Binary files /dev/null and b/tests/resources/test_5118_expected_solid_fill_control.png differ diff --git a/tests/resources/test_5118_expected_text_anchor_group_only.png b/tests/resources/test_5118_expected_text_anchor_group_only.png new file mode 100644 index 000000000..16befb86e Binary files /dev/null and b/tests/resources/test_5118_expected_text_anchor_group_only.png differ diff --git a/tests/resources/test_5118_expected_text_direct.png b/tests/resources/test_5118_expected_text_direct.png new file mode 100644 index 000000000..16befb86e Binary files /dev/null and b/tests/resources/test_5118_expected_text_direct.png differ diff --git a/tests/resources/test_5118_expected_text_edge_direct.png b/tests/resources/test_5118_expected_text_edge_direct.png new file mode 100644 index 000000000..2bac44c4c Binary files /dev/null and b/tests/resources/test_5118_expected_text_edge_direct.png differ diff --git a/tests/resources/test_5118_expected_text_edge_group.png b/tests/resources/test_5118_expected_text_edge_group.png new file mode 100644 index 000000000..2bac44c4c Binary files /dev/null and b/tests/resources/test_5118_expected_text_edge_group.png differ diff --git a/tests/resources/test_5118_expected_text_family_group_only.png b/tests/resources/test_5118_expected_text_family_group_only.png new file mode 100644 index 000000000..830c1aea3 Binary files /dev/null and b/tests/resources/test_5118_expected_text_family_group_only.png differ diff --git a/tests/resources/test_5118_expected_text_group.png b/tests/resources/test_5118_expected_text_group.png new file mode 100644 index 000000000..830c1aea3 Binary files /dev/null and b/tests/resources/test_5118_expected_text_group.png differ diff --git a/tests/resources/test_5118_expected_text_inline.png b/tests/resources/test_5118_expected_text_inline.png new file mode 100644 index 000000000..16befb86e Binary files /dev/null and b/tests/resources/test_5118_expected_text_inline.png differ diff --git a/tests/test_story.py b/tests/test_story.py index b9b00debc..da71eb1a1 100644 --- a/tests/test_story.py +++ b/tests/test_story.py @@ -325,3 +325,40 @@ def test_5110(): else: # 2026-09-11: Expect error. assert num_drawings == 1 + + +def test_5119(): + # This does not yet reproduce the problem - does not hang. + + chinese_text = '\u4f60\u7684' * 1000 + html = textwrap.dedent(f''' + +
+ {chinese_text} +
+ ''') + path_out = os.path.normpath(f'{__file__}/../../tests/test_5119_out.pdf') + story = pymupdf.Story(html=html) + mediabox = pymupdf.paper_rect("a4") + where = mediabox + (36, 36, -36, -36) + writer = pymupdf.DocumentWriter(path_out) + more = 1 + page_count = 0 + while more: + page_count += 1 + # Safety limit added only to demonstrate the problem. + if page_count > 100: + raise RuntimeError('Story.place() did not make progress') + device = writer.begin_page(mediabox) + more, filled = story.place(where) + print( + f'page={page_count}, more={more}, filled={filled}' + ) + story.draw(device) + writer.end_page() + writer.close() + diff --git a/tests/test_svg.py b/tests/test_svg.py new file mode 100644 index 000000000..11904b236 --- /dev/null +++ b/tests/test_svg.py @@ -0,0 +1,146 @@ +import os +import pathlib + +import pymupdf + +import gentle_compare + + +def test_5118(): + print() + out = pathlib.Path(os.path.normpath(f'{__file__}/../../tests/test_5118_out')) + print(f'{out=}') + out.mkdir(exist_ok=True) + + def svg(body): + return ( + '' + + body + + '' + ) + + + gradient = svg(''' + + + + + + + + ''') + + text_attributes = ( + 'text-anchor="middle" font-family="Arial, sans-serif" font-size="20"' + ) + cases = { + "gradient": gradient, + "gradient_percent_stops": gradient.replace( + 'offset="0"', 'offset="0%"' + ).replace('offset="1"', 'offset="100%"'), + "gradient_percent_coordinates": gradient.replace( + 'x1="0" x2="1"', 'x1="0%" y1="0%" x2="100%" y2="0%"' + ), + "gradient_stop_style": gradient.replace( + 'stop-color="#f5f8ff"', 'style="stop-color:#f5f8ff"' + ).replace('stop-color="#eefbf6"', 'style="stop-color:#eefbf6"'), + "gradient_user_space": gradient.replace( + 'x1="0" x2="1"', + 'gradientUnits="userSpaceOnUse" x1="1" y1="1" x2="399" y2="1"', + ), + "solid_fill_control": gradient.replace('url(#bg)', '#f5f8ff'), + "text_direct": svg( + f'middle label' + ), + "text_group": svg( + f'' + 'middle label' + ), + "text_inline": svg( + '' + 'middle label' + ), + "text_anchor_group_only": svg( + '' + '' + 'middle label' + ), + "text_family_group_only": svg( + '' + '' + 'middle label' + ), + "class_fill": svg( + '' + '' + ), + "element_fill": svg( + '' + '' + ), + "inline_fill_control": svg( + '' + ), + } + cases["text_edge_direct"] = cases["text_direct"].replace('x="200"', 'x="320"') + cases["text_edge_group"] = cases["text_anchor_group_only"].replace( + 'x="200"', 'x="320"' + ) + + png_names = list() + + for name, content in cases.items(): + svg_bytes = content.encode("utf-8") + (out / f"{name}.svg").write_bytes(svg_bytes) + with pymupdf.open(stream=svg_bytes, filetype="svg") as doc: + page = doc[0] + pix = page.get_pixmap(alpha=False) + png_name = f"{name}.png" + pix.save(out / png_name) + png_names.append(png_name) + print(name, "center RGB", pix.pixel(200, 100)) + for block in page.get_text("dict")["blocks"]: + if block["type"] != 0: + continue + for line in block["lines"]: + for span in line["spans"]: + print( + " text", repr(span["text"]), + "font", span["font"], + "size", span["size"], + "origin", tuple(round(v, 2) for v in span["origin"]), + ) + + # Check .png files. + num_errors = 0 + for png_name in png_names: + expected = os.path.normpath(f'{__file__}/../../tests/resources/test_5118_expected_{png_name}') + actual = str(out / png_name) + rms = gentle_compare.pixmaps_rms(expected, actual, verbose=0) + print(f'{png_name=}: {rms=}') + if rms > 1: + num_errors += 1 + + # Confirm the same problems through Markdown-to-PDF conversion. + for name in ("gradient", "text_direct", "text_group", "class_fill"): + markdown_path = out / f"{name}.md" + markdown_path.write_text(f"![SVG example]({name}.svg)\n", encoding="utf-8") + with pymupdf.open( + markdown_path, + rect=pymupdf.paper_rect("A4"), + archive=pymupdf.Archive(str(out)), + ) as doc: + doc.ez_save(out / f"{name}_from_markdown.pdf") + + print(f'{num_errors=}') + + if pymupdf.mupdf_version_tuple >= (1, 29): + assert not num_errors, f'{num_errors=}' + else: + # 2026-10-09. + # num_errors = 9 with mupdf-1.28.x 393e18b3c66c4 `When adding colorspace do not write Device colorspaces as ICC.`. + # num_errors = 10 with pymupdf-1.28.2. + assert num_errors