From a82365ec8cc09149e71e739335135d3748299df9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 1 Apr 2026 20:57:35 -0700 Subject: [PATCH] [svg] fix duplicate svg ids --- masque/file/svg.py | 22 +++++++++++++++++++--- masque/test/test_svg.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/masque/file/svg.py b/masque/file/svg.py index f235b50..621bcdb 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -30,6 +30,21 @@ def _ref_to_svg_transform(ref) -> str: return f'matrix({a:g} {b:g} {c:g} {d:g} {e:g} {f:g})' +def _make_svg_ids(names: Mapping[str, Pattern]) -> dict[str, str]: + svg_ids: dict[str, str] = {} + seen_ids: set[str] = set() + for name in names: + base_id = mangle_name(name) + svg_id = base_id + suffix = 1 + while svg_id in seen_ids: + suffix += 1 + svg_id = f'{base_id}_{suffix}' + seen_ids.add(svg_id) + svg_ids[name] = svg_id + return svg_ids + + def writefile( library: Mapping[str, Pattern], top: str, @@ -81,10 +96,11 @@ def writefile( # Create file svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string, debug=(not custom_attributes)) + svg_ids = _make_svg_ids(library) # Now create a group for each pattern and add in any Boundary and Use elements for name, pat in library.items(): - svg_group = svg.g(id=mangle_name(name), fill='blue', stroke='red') + svg_group = svg.g(id=svg_ids[name], fill='blue', stroke='red') for layer, shapes in pat.shapes.items(): for shape in shapes: @@ -123,11 +139,11 @@ def writefile( continue for ref in refs: transform = _ref_to_svg_transform(ref) - use = svg.use(href='#' + mangle_name(target), transform=transform) + use = svg.use(href='#' + svg_ids[target], transform=transform) svg_group.add(use) svg.defs.add(svg_group) - svg.add(svg.use(href='#' + mangle_name(top))) + svg.add(svg.use(href='#' + svg_ids[top])) svg.save() diff --git a/masque/test/test_svg.py b/masque/test/test_svg.py index a3261b6..b637853 100644 --- a/masque/test/test_svg.py +++ b/masque/test/test_svg.py @@ -68,3 +68,31 @@ def test_svg_ref_mirroring_changes_affine_transform(tmp_path: Path) -> None: assert_allclose(plain_transform, (0, 2, -2, 0, 3, 4), atol=1e-10) assert_allclose(mirrored_transform, (0, 2, 2, 0, 3, 4), atol=1e-10) + + +def test_svg_uses_unique_ids_for_colliding_mangled_names(tmp_path: Path) -> None: + lib = Library() + first = Pattern() + first.polygon("1", vertices=[[0, 0], [1, 0], [0, 1]]) + lib["a b"] = first + + second = Pattern() + second.polygon("1", vertices=[[0, 0], [2, 0], [0, 2]]) + lib["a-b"] = second + + top = Pattern() + top.ref("a b") + top.ref("a-b", offset=(5, 0)) + lib["top"] = top + + svg_path = tmp_path / "colliding_ids.svg" + svg.writefile(lib, "top", str(svg_path)) + + root = ET.fromstring(svg_path.read_text()) + ids = [group.attrib["id"] for group in root.iter(f"{SVG_NS}g")] + hrefs = [use.attrib[XLINK_HREF] for use in root.iter(f"{SVG_NS}use")] + + assert ids.count("a_b") == 1 + assert len(set(ids)) == len(ids) + assert "#a_b" in hrefs + assert "#a_b_2" in hrefs