From 059515a489edfd56a54a55e91a37e4db0414fe84 Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Mon, 23 May 2022 21:53:08 +0200 Subject: [PATCH 1/2] fix id clash in pdflatex --- inkex/elements/_base.py | 64 +++++++++++++++++-- inkex/elements/_svg.py | 62 ++++++++++++++++-- pdflatex.py | 7 +- ..._______frac__1____sqrt__5______2______.out | 6 +- tests/test_inkex_elements_base.py | 20 ++++++ 5 files changed, 143 insertions(+), 16 deletions(-) diff --git a/inkex/elements/_base.py b/inkex/elements/_base.py index 48eb92b8..2f9f3427 100644 --- a/inkex/elements/_base.py +++ b/inkex/elements/_base.py @@ -28,7 +28,7 @@ give path, transform, and property access easily. from __future__ import annotations from copy import deepcopy -from typing import Any, Tuple, Optional, overload, TypeVar +from typing import Any, Tuple, Optional, overload, TypeVar, List from lxml import etree from ..interfaces.IElement import IBaseElement, ISVGDocumentElement @@ -236,14 +236,61 @@ class BaseElement(IBaseElement): svg.append(self.copy()) return svg.tostring().split(b">\n ", 1)[-1][:-6] - def set_random_id(self, prefix=None, size=4, backlinks=False): - """Sets the id attribute if it is not already set.""" + def set_random_id( + self, + prefix: str = None, + size: Optional[int] = None, + backlinks: bool = False, + blacklist: Optional[List[str]] = None, + ): + """Sets the id attribute if it is not already set. + + The id consists of a prefix and an appended random integer of length size. + Args: + prefix (str, optional): the prefix of the new ID. Defaults to the tag name. + size (Optional[int], optional): number of digits of the second part of the + id. If None, the length is chosen based on the amount of existing + objects. Defaults to None. + + .. versionchanged:: 1.2 + The default of this value has been changed from 4 to None. + backlinks (bool, optional): Whether to update the links in existing objects + that reference this element. Defaults to False. + blacklist (List[str], optional): An additional list of ids that are not + allowed to be used. This is useful when bulk inserting objects. + Defaults to None. + + .. versionadded:: 1.2 + """ prefix = str(self) if prefix is None else prefix - self.set_id(self.root.get_unique_id(prefix, size=size), backlinks=backlinks) + self.set_id( + self.root.get_unique_id(prefix, size=size, blacklist=blacklist), + backlinks=backlinks, + ) - def set_random_ids(self, prefix=None, levels=-1, backlinks=False): - """Same as set_random_id, but will apply also to children""" - self.set_random_id(prefix=prefix, backlinks=backlinks) + def set_random_ids( + self, + prefix: str = None, + levels: int = -1, + backlinks: bool = False, + blacklist: Optional[List[str]] = None, + ): + """Same as set_random_id, but will apply also to children + + The id consists of a prefix and an appended random integer of length size. + Args: + prefix (str, optional): the prefix of the new ID. Defaults to the tag name. + levels (int, optional): the depth of the tree traversion, if negative, no + limit is imposed. Defaults to -1. + backlinks (bool, optional): Whether to update the links in existing objects + that reference this element. Defaults to False. + blacklist (List[str], optional): An additional list of ids that are not + allowed to be used. This is useful when bulk inserting objects. + Defaults to None. + + .. versionadded:: 1.2 + """ + self.set_random_id(prefix=prefix, backlinks=backlinks, blacklist=blacklist) if levels != 0: for child in self: if hasattr(child, "set_random_ids"): @@ -288,6 +335,9 @@ class BaseElement(IBaseElement): if backlinks and old_id: for elem in self.root.getElementsByHref(old_id): elem.href = self + for attr in ["clip-path", "mask"]: + for elem in self.root.getElementsByHref(old_id, attribute=attr): + elem.set(attr, f"url(#{new_id})") for elem in self.root.getElementsByStyleUrl(old_id): elem.style.update_urls(old_id, new_id) diff --git a/inkex/elements/_svg.py b/inkex/elements/_svg.py index a9594159..ed40f38e 100644 --- a/inkex/elements/_svg.py +++ b/inkex/elements/_svg.py @@ -43,6 +43,8 @@ from ..styles import StyleSheets from ._base import BaseElement from ._meta import StyleElement, NamedView +from typing import Optional, List + if False: # pylint: disable=using-constant-test import typing # pylint: disable=unused-import @@ -72,11 +74,42 @@ class SvgDocumentElement(DeprecatedSvgMixin, ISVGDocumentElement, BaseElement): self.ids = set(self.xpath("//@id")) return self.ids - def get_unique_id(self, prefix, size=None): - """Generate a new id from an existing old_id""" + def get_unique_id( + self, + prefix: str, + size: Optional[int] = None, + blacklist: Optional[List[str]] = None, + ): + """Generate a new id from an existing old_id + + The id consists of a prefix and an appended random integer with size digits. + + If size is not given, it is determined automatically from the length of + existing ids, i.e. those in the document plus those in the blacklist. + + Args: + prefix (str): the prefix of the new ID. + size (Optional[int], optional): number of digits of the second part of the + id. If None, the length is chosen based on the amount of existing + objects. Defaults to None. + + .. versionchanged:: 1.1 + The default of this parameter has been changed from 4 to None. + blacklist (Optional[Iterable[str]], optional): An additional iterable of ids + that are not allowed to be used. This is useful when bulk inserting + objects. + Defaults to None. + + .. versionadded:: 1.2 + + Returns: + _type_: _description_ + """ ids = self.get_ids() if size is None: size = max(math.ceil(math.log10(len(ids) or 1000)) + 1, 4) + if blacklist is not None: + ids.update(blacklist) new_id = None _from = 10**size - 1 _to = 10**size @@ -150,9 +183,28 @@ class SvgDocumentElement(DeprecatedSvgMixin, ISVGDocumentElement, BaseElement): return self.xpath(ConditionalRule(f".{class_name}").to_xpath()) - def getElementsByHref(self, eid): # pylint: disable=invalid-name - """Get elements by their href xlink attribute""" - return self.xpath(f'//*[@xlink:href="#{eid}"]') + def getElementsByHref( + self, eid: str, attribute="xlink:href" + ): # pylint: disable=invalid-name + """Get elements that reference the element with id eid. + + Args: + eid (str): _description_ + attribute (str, optional): Attribute to look for. + Valid choices: "xlink:href", "mask", "clip-path". + Defaults to "xlink:href". + + .. versionadded:: 1.2 + + Returns: + Any: list of elements + """ + if attribute == "xlink:href": + return self.xpath(f'//*[@xlink:href="#{eid}"]') + elif attribute == "mask": + return self.xpath(f'//*[@mask="url(#{eid})"]') + elif attribute == "clip-path": + return self.xpath(f'//*[@clip-path="url(#{eid})"]') def getElementsByStyleUrl(self, eid, style=None): # pylint: disable=invalid-name """Get elements by a style attribute url""" diff --git a/pdflatex.py b/pdflatex.py index e5cfd5cc..f989191f 100755 --- a/pdflatex.py +++ b/pdflatex.py @@ -107,10 +107,15 @@ class PdfLatex(TempDirMixin, inkex.GenerateExtension): yield child elif isinstance(child, Defs): for def_child in child: + # The ids of both the imported and the target document should + # be off-limits + def_child.set_random_ids( + backlinks=True, blacklist=self.svg.get_ids() + ) self.svg.defs.append(def_child) def write_latex(self, stream): - """Takes a forumle and wraps it in latex""" + """Takes a formula and wraps it in latex""" if self.options.standalone: docclass = ( f"\\documentclass[fontsize={self.options.font_size}pt, " diff --git a/tests/data/refs/pdflatex__--font_size__20__--formule________frac__1____sqrt__5______2______.out b/tests/data/refs/pdflatex__--font_size__20__--formule________frac__1____sqrt__5______2______.out index 952e1b05..e862eeca 100644 --- a/tests/data/refs/pdflatex__--font_size__20__--formule________frac__1____sqrt__5______2______.out +++ b/tests/data/refs/pdflatex__--font_size__20__--formule________frac__1____sqrt__5______2______.out @@ -1,6 +1,6 @@ - - + + @@ -23,7 +23,7 @@ - + diff --git a/tests/test_inkex_elements_base.py b/tests/test_inkex_elements_base.py index 13b8b402..7ba38fa8 100644 --- a/tests/test_inkex_elements_base.py +++ b/tests/test_inkex_elements_base.py @@ -3,6 +3,7 @@ """ Test the element API base classes and basic functionality """ +import random from lxml import etree from inkex.elements import ( @@ -217,12 +218,31 @@ class AttributeHandelingTestCase(SvgTestCase): def test_set_id_backlinks(self): """Changing an id can update backlinks""" elem = self.svg.getElementById("path1") + rect1 = Rectangle() + rect1.set("clip-path", "url(#path1)") + rect2 = Rectangle() + rect2.set("mask", "url(#path1)") + elem.addnext(rect1) + self.svg.getElementById("G").add(rect2) elem.set_id("plant54", True) self.assertEqual(self.svg.getElementById("G").get("xlink:href"), "#plant54") self.assertEqual(self.svg.getElementById("G").href, elem) self.assertEqual( str(self.svg.getElementById("B").style), "fill:#eee;joker:url(#plant54)" ) + self.assertEqual(rect1.get("clip-path"), "url(#plant54)") + self.assertEqual(rect2.get("mask"), "url(#plant54)") + + def test_set_id_blacklist(self): + """Test that the set_random_id function respects a blacklist""" + elem: BaseElement = self.svg.getElementById("D") + + state = random.getstate() + elem.set_random_id("Thing") + self.assertEqual(elem.get("id"), "Thing5815") + random.setstate(state) + elem.set_random_id("Other", blacklist=["Other5815"]) + self.assertEqual(elem.get("id"), "Other8555") def test_get_element_by_name(self): """Get elements by name""" -- GitLab From 7b7ec9ac3cfc26cebd4db5891b671d18b9461fb1 Mon Sep 17 00:00:00 2001 From: Jonathan Neuhauser Date: Tue, 24 May 2022 10:36:53 +0200 Subject: [PATCH 2/2] use the get_id function to set the url --- inkex/elements/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inkex/elements/_base.py b/inkex/elements/_base.py index 2f9f3427..20522e41 100644 --- a/inkex/elements/_base.py +++ b/inkex/elements/_base.py @@ -337,7 +337,7 @@ class BaseElement(IBaseElement): elem.href = self for attr in ["clip-path", "mask"]: for elem in self.root.getElementsByHref(old_id, attribute=attr): - elem.set(attr, f"url(#{new_id})") + elem.set(attr, self.get_id(2)) for elem in self.root.getElementsByStyleUrl(old_id): elem.style.update_urls(old_id, new_id) -- GitLab