Skip to content

Commit

Permalink
Made text elements optionally allowed
Browse files Browse the repository at this point in the history
- Added `allow_text` options to both `topicosvg()` and `checkpicosvg()`
  to enable simplifying SVGs with text elements. Note that the text
  elements will not be converted to paths, they will only pass through.
- Added an `allow_text` boolean flag to the command line tool to control
  this behaviour.
  • Loading branch information
zond committed Feb 22, 2023
1 parent 67a8563 commit 44192de
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 26 deletions.
9 changes: 7 additions & 2 deletions src/picosvg/picosvg.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@

flags.DEFINE_bool("clip_to_viewbox", False, "Whether to clip content outside viewbox")
flags.DEFINE_string("output_file", "-", "Output SVG file ('-' means stdout)")
flags.DEFINE_bool(
"allow_text",
False,
"Whether to allow text elements. Note that they will not be converted to paths, just pass through to the output.",
)


def _run(argv):
Expand All @@ -40,9 +45,9 @@ def _run(argv):
input_file = None

if input_file:
svg = SVG.parse(input_file).topicosvg()
svg = SVG.parse(input_file).topicosvg(allow_text=FLAGS.allow_text)
else:
svg = SVG.fromstring(sys.stdin.read()).topicosvg()
svg = SVG.fromstring(sys.stdin.read()).topicosvg(allow_text=FLAGS.allow_text)

if FLAGS.clip_to_viewbox:
svg.clip_to_viewbox(inplace=True)
Expand Down
53 changes: 29 additions & 24 deletions src/picosvg/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ def __init__(self, svg_root):
self.svg_root = svg_root
self.elements = []

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

def _elements(self) -> List[Tuple[etree.Element, Tuple[SVGShape, ...]]]:
if self.elements:
return self.elements
Expand Down Expand Up @@ -403,7 +406,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 +418,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 +429,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 +447,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 +549,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 +796,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 +841,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 +895,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 +908,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 +918,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 +930,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 +950,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 @@ -1001,7 +1004,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 +1019,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 +1032,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 +1046,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 +1061,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 +1075,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 +1170,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 @@ -1273,7 +1276,7 @@ def _remove_orphaned_gradients(self):
if grad.attrib.get("id") not in used_gradient_ids:
_safe_remove(grad)

def checkpicosvg(self):
def checkpicosvg(self, allow_text=False):
"""Check for nano violations, return xpaths to bad elements.
If result sequence empty then this is a valid picosvg.
Expand All @@ -1290,6 +1293,8 @@ def checkpicosvg(self):
r"^/svg\[0\]/defs\[0\]/(linear|radial)Gradient\[\d+\](/stop\[\d+\])?$",
r"^/svg\[0\](/(path|g)\[\d+\])+$",
}
if allow_text:
path_allowlist.add(r"^/svg\[0\](/text\[\d+\])+$")
paths_required = {
"/svg[0]",
"/svg[0]/defs[0]",
Expand Down Expand Up @@ -1323,10 +1328,10 @@ def checkpicosvg(self):

return tuple(errors)

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

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

nano_violations = self.checkpicosvg()
nano_violations = self.checkpicosvg(allow_text=allow_text)
if nano_violations:
raise ValueError(
"Unable to convert to picosvg: " + ",".join(nano_violations)
Expand Down
10 changes: 10 additions & 0 deletions tests/svg_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,3 +672,13 @@ 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_allow_text():
text_svg = load_test_svg("text-before.svg")
with pytest.raises(
ValueError,
match=r"Unable to convert to picosvg: BadElement: /svg\[0\]/text\[0\]",
):
text_svg.topicosvg()
assert "text" in text_svg.topicosvg(allow_text=True).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.

0 comments on commit 44192de

Please sign in to comment.