diff --git a/src/picosvg/picosvg.py b/src/picosvg/picosvg.py index f9aebae..39ec42a 100644 --- a/src/picosvg/picosvg.py +++ b/src/picosvg/picosvg.py @@ -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): @@ -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) diff --git a/src/picosvg/svg.py b/src/picosvg/svg.py index a8739a4..14f2bd5 100644 --- a/src/picosvg/svg.py +++ b/src/picosvg/svg.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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. @@ -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]", @@ -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() @@ -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) diff --git a/tests/svg_test.py b/tests/svg_test.py index 4df79c9..45499ea 100644 --- a/tests/svg_test.py +++ b/tests/svg_test.py @@ -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() diff --git a/tests/text-before.svg b/tests/text-before.svg new file mode 100644 index 0000000..390a696 --- /dev/null +++ b/tests/text-before.svg @@ -0,0 +1,5 @@ + + Hello +