diff options
Diffstat (limited to 'share/extensions/ungroup_deep.py')
-rwxr-xr-x | share/extensions/ungroup_deep.py | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/share/extensions/ungroup_deep.py b/share/extensions/ungroup_deep.py new file mode 100755 index 0000000..36a01bf --- /dev/null +++ b/share/extensions/ungroup_deep.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +see #inkscape on Freenode and +https://github.com/nikitakit/svg2sif/blob/master/synfig_prepare.py#L370 +for an example how to do the transform of parent to children. +""" + +import inkex +from inkex import ( + Group, + Anchor, + Switch, + NamedView, + Defs, + Metadata, + ForeignObject, + ClipPath, + Use, + SvgDocumentElement, +) + + +class UngroupDeep(inkex.EffectExtension): + def add_arguments(self, pars): + pars.add_argument( + "--startdepth", type=int, default=0, help="starting depth for ungrouping" + ) + pars.add_argument( + "--maxdepth", type=int, default=65535, help="maximum ungrouping depth" + ) + pars.add_argument( + "--keepdepth", + type=int, + default=0, + help="levels of ungrouping to leave untouched", + ) + + @staticmethod + def _merge_style(node, style): + """Propagate style and transform to remove inheritance + Originally from + https://github.com/nikitakit/svg2sif/blob/master/synfig_prepare.py#L370 + """ + + # Compose the style attribs + this_style = node.style + remaining_style = {} # Style attributes that are not propagated + + # Filters should remain on the top ancestor + non_propagated = ["filter"] + for key in non_propagated: + if key in this_style.keys(): + remaining_style[key] = this_style[key] + del this_style[key] + + # Create a copy of the parent style, and merge this style into it + parent_style_copy = style.copy() + parent_style_copy.update(this_style) + this_style = parent_style_copy + + # Merge in any attributes outside of the style + style_attribs = ["fill", "stroke"] + for attrib in style_attribs: + if node.get(attrib): + this_style[attrib] = node.get(attrib) + del node.attrib[attrib] + + if isinstance(node, (SvgDocumentElement, Anchor, Group, Switch)): + # Leave only non-propagating style attributes + if not remaining_style: + if "style" in node.keys(): + del node.attrib["style"] + else: + node.style = remaining_style + + else: + # This element is not a container + + # Merge remaining_style into this_style + this_style.update(remaining_style) + + # Set the element's style attribs + node.style = this_style + + def _merge_clippath(self, node, clippathurl): + if clippathurl and clippathurl != "none": + node_transform = node.transform + if node_transform: + # Clip-paths on nodes with a transform have the transform + # applied to the clipPath as well, which we don't want. So, we + # create new clipPath element with references to all existing + # clippath subelements, but with the inverse transform applied + new_clippath = self.svg.defs.add( + ClipPath(clipPathUnits="userSpaceOnUse") + ) + new_clippath.set_random_id("clipPath") + clippath = self.svg.getElementById(clippathurl[5:-1]) + for child in clippath.iterchildren(): + new_clippath.add(Use.new(child, 0, 0)) + + # Set the clippathurl to be the one with the inverse transform + clippathurl = "url(#" + new_clippath.get("id") + ")" + + # Reference the parent clip-path to keep clipping intersection + # Find end of clip-path chain and add reference there + node_clippathurl = node.get("clip-path") + while node_clippathurl: + node = self.svg.getElementById(node_clippathurl[5:-1]) + node_clippathurl = node.get("clip-path") + node.set("clip-path", clippathurl) + + # Flatten a group into same z-order as parent, propagating attribs + def _ungroup(self, node): + node_parent = node.getparent() + node_index = list(node_parent).index(node) + node_style = node.style + + node_transform = node.transform + node_clippathurl = node.get("clip-path") + for child in reversed(list(node)): + if not isinstance(child, inkex.BaseElement): + continue + child.transform = node_transform @ child.transform + + if node.get("style") is not None: + self._merge_style(child, node_style) + self._merge_clippath(child, node_clippathurl) + node_parent.insert(node_index, child) + node_parent.remove(node) + + # Put all ungrouping restrictions here + def _want_ungroup(self, node, depth, height): + if ( + isinstance(node, Group) + and node.getparent() is not None + and height > self.options.keepdepth + and self.options.startdepth <= depth <= self.options.maxdepth + ): + return True + return False + + def _deep_ungroup(self, node): + # using iteration instead of recursion to avoid hitting Python + # max recursion depth limits, which is a problem in converted PDFs + + # Seed the queue (stack) with initial node + q = [{"node": node, "depth": 0, "prev": {"height": None}, "height": None}] + + while q: + current = q[-1] + node = current["node"] + depth = current["depth"] + height = current["height"] + + # Recursion path + if height is None: + # Don't enter non-graphical portions of the document + if isinstance(node, (NamedView, Defs, Metadata, ForeignObject)): + q.pop() + + # Base case: Leaf node + if not isinstance(node, Group) or not list(node): + current["height"] = 0 + + # Recursive case: Group element with children + else: + depth += 1 + for child in node.iterchildren(): + q.append( + { + "node": child, + "prev": current, + "depth": depth, + "height": None, + } + ) + + # Return path + else: + # Ungroup if desired + if self._want_ungroup(node, depth, height): + self._ungroup(node) + + # Propagate (max) height up the call chain + height += 1 + previous = current["prev"] + prev_height = previous["height"] + if prev_height is None or prev_height < height: + previous["height"] = height + + # Only process each node once + q.pop() + + def effect(self): + if self.svg.selection: + for node in self.svg.selection.values(): + self._deep_ungroup(node) + else: + for node in self.document.getroot(): + self._deep_ungroup(node) + + +if __name__ == "__main__": + UngroupDeep().run() |