213 lines
7.3 KiB
Python
Executable file
213 lines
7.3 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# 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,
|
|
Layer,
|
|
Anchor,
|
|
Switch,
|
|
NamedView,
|
|
Defs,
|
|
Metadata,
|
|
ForeignObject,
|
|
ClipPath,
|
|
Use,
|
|
SvgDocumentElement,
|
|
)
|
|
|
|
|
|
class UngroupDeep(inkex.EffectExtension):
|
|
def add_arguments(self, pars):
|
|
pars.add_argument(
|
|
"--preserve_layers",
|
|
type=inkex.Boolean,
|
|
default=False,
|
|
help="Do not ungroup layers",
|
|
)
|
|
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")
|
|
node_parent.remove(node)
|
|
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)
|
|
|
|
# Put all ungrouping restrictions here
|
|
def _want_ungroup(self, node, depth, height):
|
|
if (
|
|
isinstance(node, Group)
|
|
and not (self.options.preserve_layers and isinstance(node, Layer))
|
|
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()
|