summaryrefslogtreecommitdiffstats
path: root/share/extensions/ungroup_deep.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xshare/extensions/ungroup_deep.py205
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()