Skip to content

Commit

Permalink
Added optional support for text elements.
Browse files Browse the repository at this point in the history
- Added a remove_text function.
- Added an allow_text bool attribute.
- Added a _clone() method that copies the etree and the allow_text attributes.
- Made all SVG(...) using deepcopy to clone the SVG use the _clone() method instead.
- Made topicosvg() use remove_text unless self.allow_text.
- Made checkpicosvg() allow text elements if self.allow_text.
  • Loading branch information
zond committed Feb 22, 2023
1 parent 67a8563 commit 0e50298
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 21 deletions.
66 changes: 45 additions & 21 deletions src/picosvg/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,15 @@ def is_group(self):
class SVG:
svg_root: etree.Element
elements: List[Tuple[etree.Element, Tuple[SVGShape, ...]]]
allow_text: bool

def __init__(self, svg_root):
def __init__(self, svg_root, allow_text: bool = False):
self.svg_root = svg_root
self.elements = []
self.allow_text = allow_text

def _clone(self) -> "SVG":
return SVG(svg_root=copy.deepcopy(self.svg_root), allow_text=self.allow_text)

def _elements(self) -> List[Tuple[etree.Element, Tuple[SVGShape, ...]]]:
if self.elements:
Expand Down Expand Up @@ -403,7 +408,7 @@ def shapes(self):
def absolute(self, inplace=False):
"""Converts all basic shapes to their equivalent path."""
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.absolute(inplace=True)
return svg

Expand All @@ -415,7 +420,7 @@ def absolute(self, inplace=False):
def shapes_to_paths(self, inplace=False):
"""Converts all basic shapes to their equivalent path."""
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.shapes_to_paths(inplace=True)
return svg

Expand All @@ -426,7 +431,7 @@ def shapes_to_paths(self, inplace=False):

def expand_shorthand(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.expand_shorthand(inplace=True)
return svg

Expand All @@ -444,7 +449,7 @@ def _apply_styles(self, el: etree.Element):
def apply_style_attributes(self, inplace=False):
"""Converts inlined CSS "style" attributes to equivalent SVG attributes."""
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.apply_style_attributes(inplace=True)
return svg

Expand Down Expand Up @@ -546,7 +551,7 @@ def resolve_use(self, inplace=False):
https://www.w3.org/TR/SVG11/struct.html#UseElement"""
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.resolve_use(inplace=True)
return svg

Expand Down Expand Up @@ -793,7 +798,7 @@ def _simplify(self):

def simplify(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.simplify(inplace=True)
return svg

Expand Down Expand Up @@ -838,7 +843,7 @@ def _stroke(self, shape):

def clip_to_viewbox(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.clip_to_viewbox(inplace=True)
return svg

Expand Down Expand Up @@ -892,7 +897,7 @@ def clip_to_viewbox(self, inplace=False):

def evenodd_to_nonzero_winding(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.evenodd_to_nonzero_winding(inplace=True)
return svg

Expand All @@ -905,7 +910,7 @@ def evenodd_to_nonzero_winding(self, inplace=False):

def round_floats(self, ndigits: int, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.round_floats(ndigits, inplace=True)
return svg

Expand All @@ -915,7 +920,7 @@ def round_floats(self, ndigits: int, inplace=False):

def remove_empty_subpaths(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.remove_empty_subpaths(inplace=True)
return svg

Expand All @@ -927,7 +932,7 @@ def remove_empty_subpaths(self, inplace=False):

def remove_unpainted_shapes(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.remove_unpainted_shapes(inplace=True)
return svg

Expand All @@ -947,7 +952,7 @@ def remove_unpainted_shapes(self, inplace=False):

def remove_nonsvg_content(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.remove_nonsvg_content(inplace=True)
return svg

Expand Down Expand Up @@ -986,6 +991,19 @@ def remove_nonsvg_content(self, inplace=False):

return self

def remove_text(self, inplace=False):
if not inplace:
svg = self._clone()
svg.remove_text(inplace=True)
return svg

self._update_etree()

for el in self.xpath("//*[name()='text']"):
el.getparent().remove(el)

return self

def remove_processing_instructions(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
Expand All @@ -1001,7 +1019,7 @@ def remove_processing_instructions(self, inplace=False):

def remove_comments(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.remove_comments(inplace=True)
return svg

Expand All @@ -1016,7 +1034,7 @@ def remove_anonymous_symbols(self, inplace=False):
# No id makes a symbol useless
# https://github.com/googlefonts/picosvg/issues/46
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.remove_anonymous_symbols(inplace=True)
return svg

Expand All @@ -1029,7 +1047,7 @@ def remove_anonymous_symbols(self, inplace=False):

def remove_title_meta_desc(self, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.remove_title_meta_desc(inplace=True)
return svg

Expand All @@ -1043,7 +1061,7 @@ def remove_title_meta_desc(self, inplace=False):

def set_attributes(self, name_values, xpath="/svg:svg", inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.set_attributes(name_values, xpath=xpath, inplace=True)
return svg

Expand All @@ -1058,7 +1076,7 @@ def set_attributes(self, name_values, xpath="/svg:svg", inplace=False):
def remove_attributes(self, names, xpath="/svg:svg", inplace=False):
"""Drop things like viewBox, width, height that set size of overall svg"""
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.remove_attributes(names, xpath=xpath, inplace=True)
return svg

Expand All @@ -1072,7 +1090,7 @@ def remove_attributes(self, names, xpath="/svg:svg", inplace=False):
def normalize_opacity(self, inplace=False):
"""Merge '{fill,stroke}_opacity' with generic 'opacity' when possible."""
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.normalize_opacity(inplace=True)
return svg

Expand Down Expand Up @@ -1167,7 +1185,7 @@ def resolve_nested_svgs(self, inplace=False):
- https://www.sarasoueidan.com/blog/nesting-svgs/
"""
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.resolve_nested_svgs(inplace=True)
return svg

Expand Down Expand Up @@ -1290,6 +1308,8 @@ def checkpicosvg(self):
r"^/svg\[0\]/defs\[0\]/(linear|radial)Gradient\[\d+\](/stop\[\d+\])?$",
r"^/svg\[0\](/(path|g)\[\d+\])+$",
}
if self.allow_text:
path_allowlist.add(r"^/svg\[0\](/text\[\d+\])+$")
paths_required = {
"/svg[0]",
"/svg[0]/defs[0]",
Expand Down Expand Up @@ -1325,7 +1345,7 @@ def checkpicosvg(self):

def topicosvg(self, *, ndigits=3, inplace=False):
if not inplace:
svg = SVG(copy.deepcopy(self.svg_root))
svg = self._clone()
svg.topicosvg(ndigits=ndigits, inplace=True)
return svg

Expand Down Expand Up @@ -1358,6 +1378,10 @@ def topicosvg(self, *, ndigits=3, inplace=False):
self.remove_empty_subpaths(inplace=True)
self.remove_unpainted_shapes(inplace=True)

# Remove text if disallowed
if not self.allow_text:
self.remove_text(inplace=True)

nano_violations = self.checkpicosvg()
if nano_violations:
raise ValueError(
Expand Down
11 changes: 11 additions & 0 deletions tests/svg_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ def test_resolve_use(actual, expected_result):
"xpacket-before.svg",
"xpacket-nano.svg",
),
(
"text-before.svg",
"text-nano.svg",
),
],
)
def test_topicosvg(actual, expected_result):
Expand Down Expand Up @@ -672,3 +676,10 @@ def test_remove_processing_instructions():
assert "xpacket" in xpacket_svg.tostring()
pico_svg = xpacket_svg.remove_processing_instructions()
assert "xpacket" not in pico_svg.tostring()


def test_remove_text():
text_svg = load_test_svg("text-before.svg")
assert "text" in text_svg.tostring()
pico_svg = text_svg.remove_text()
assert "text" not in pico_svg.tostring()
5 changes: 5 additions & 0 deletions tests/text-before.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions tests/text-nano.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 0e50298

Please sign in to comment.