summaryrefslogtreecommitdiffstats
path: root/share/extensions/synfig_prepare.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xshare/extensions/synfig_prepare.py323
1 files changed, 323 insertions, 0 deletions
diff --git a/share/extensions/synfig_prepare.py b/share/extensions/synfig_prepare.py
new file mode 100755
index 0000000..9899eec
--- /dev/null
+++ b/share/extensions/synfig_prepare.py
@@ -0,0 +1,323 @@
+#!/usr/bin/env python
+# coding=utf-8
+#
+# Copyright (C) 2011 Nikita Kitaev
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+#
+"""
+Simplifies SVG files in preparation for sif export.
+"""
+
+import os
+import tempfile
+from subprocess import PIPE, Popen
+from inkex.command import inkscape, write_svg, ProgramRunError
+from inkex.localization import inkex_gettext as _
+from inkex.base import TempDirMixin
+
+import inkex
+from inkex import (
+ load_svg,
+ Group,
+ PathElement,
+ ShapeElement,
+ Anchor,
+ Switch,
+ SvgDocumentElement,
+ Transform,
+)
+
+###### Utility Classes ####################################
+
+
+class MalformedSVGError(Exception):
+ """Raised when the SVG document is invalid or contains unsupported features"""
+
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return (
+ _(
+ """SVG document is invalid or contains unsupported features
+
+Error message: %s
+
+The SVG to Synfig converter is designed to handle SVG files that were created using Inkscape. Unsupported features are most likely to occur in SVG files written by other programs.
+"""
+ )
+ % repr(self.value)
+ )
+
+
+###### Utility Functions ##################################
+
+### Path related
+
+
+def fuse_subpaths(path: inkex.Path):
+ """Fuse subpaths of a path. Should only be used on unstroked paths.
+ The idea is to replace every moveto by a lineto, and then walk all the extra lines
+ backwards to get the same fill. For unfilled paths, this gives visually good results
+ in cases with not-too-complex paths, i.e. no intersections.
+ There may be extra zero-length Lineto commands."""
+
+ result = inkex.Path()
+ return_stack = []
+ for i, seg in enumerate(path.proxy_iterator()):
+ if seg.letter not in "zZmM":
+ result.append(seg.command)
+ elif i == 0:
+ result.append(seg.command)
+ first = seg.end_point
+ else:
+ # Add a line instead of the ZoneClose / Move command, and store
+ # the initial position.
+ return_to = seg.previous_end_point
+ if seg.letter in "mM":
+ if seg.previous_end_point != first: # only close if needed
+ # all paths must be closed for this algorithm to work. Since
+ # the path doesn't have a stroke, it's visually irrellevant.
+ result.append(inkex.paths.Line(*first))
+ return_to = first
+
+ return_stack += [return_to]
+ result.append(inkex.paths.Line(*seg.end_point))
+
+ first = seg.end_point
+
+ # also close the last subpath
+ return_stack += [first]
+
+ # now apply the return stack backwards
+ for point in return_stack[::-1]:
+ result.append(inkex.paths.Line(*point))
+
+ return result
+
+
+def split_fill_and_stroke(path_node):
+ """Split a path into two paths, one filled and one stroked
+
+ Returns a the list [fill, stroke], where each is the XML element of the
+ fill or stroke, or None.
+ """
+ style = dict(inkex.Style.parse_str(path_node.get("style", "")))
+
+ # If there is only stroke or only fill, don't split anything
+ if "fill" in style and style["fill"] == "none":
+ if "stroke" not in style or style["stroke"] == "none":
+ return [None, None] # Path has neither stroke nor fill
+ else:
+ return [None, path_node]
+ if "stroke" not in style.keys() or style["stroke"] == "none":
+ return [path_node, None]
+
+ group = Group()
+ fill = group.add(PathElement())
+ stroke = group.add(PathElement())
+
+ d = path_node.pop("d")
+ if d is None:
+ raise AssertionError("Cannot split stroke and fill of non-path element")
+
+ nodetypes = path_node.pop("sodipodi:nodetypes", None)
+ path_id = path_node.pop("id", str(id(path_node)))
+ transform = path_node.pop("transform", None)
+ path_node.pop("style")
+
+ # Pass along all remaining attributes to the group
+ for attrib_name, attrib_value in path_node.attrib.items():
+ group.set(attrib_name, attrib_value)
+
+ group.set("id", path_id)
+
+ # Next split apart the style attribute
+ style_group = {}
+ style_fill = {"stroke": "none", "fill": "#000000"}
+ style_stroke = {"fill": "none", "stroke": "none"}
+
+ for key in style.keys():
+ if key.startswith("fill"):
+ style_fill[key] = style[key]
+ elif key.startswith("stroke"):
+ style_stroke[key] = style[key]
+ elif key.startswith("marker"):
+ style_stroke[key] = style[key]
+ elif key.startswith("filter"):
+ style_group[key] = style[key]
+ else:
+ style_fill[key] = style[key]
+ style_stroke[key] = style[key]
+
+ if len(style_group) != 0:
+ group.set("style", str(inkex.Style(style_group)))
+
+ fill.set("style", str(inkex.Style(style_fill)))
+ stroke.set("style", str(inkex.Style(style_stroke)))
+
+ # Finalize the two paths
+ fill.set("d", d)
+ stroke.set("d", d)
+ if nodetypes is not None:
+ fill.set("sodipodi:nodetypes", nodetypes)
+ stroke.set("sodipodi:nodetypes", nodetypes)
+ fill.set("id", path_id + "-fill")
+ stroke.set("id", path_id + "-stroke")
+ if transform is not None:
+ fill.set("transform", transform)
+ stroke.set("transform", transform)
+
+ # Replace the original node with the group
+ path_node.getparent().replace(path_node, group)
+
+ return [fill, stroke]
+
+
+### Object related
+
+
+def propagate_attribs(
+ node, parent_style={}, parent_transform=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+):
+ """Propagate style and transform to remove inheritance"""
+
+ # Don't enter non-graphical portions of the document
+ if not isinstance(node, (ShapeElement, SvgDocumentElement)):
+ return
+
+ # Compose the transformations
+ if isinstance(node, SvgDocumentElement) and node.get("viewBox"):
+ vx, vy, vw, vh = [get_dimension(x) for x in node.get_viewbox()]
+ dw = get_dimension(node.get("width", vw))
+ dh = get_dimension(node.get("height", vh))
+ this_transform = Transform(translate=(-vx, -vy), scale=(dw / vw, dh / vh))
+ del node.attrib["viewBox"]
+ else:
+ this_transform = Transform(parent_transform)
+
+ this_transform @= node.transform
+
+ # Compose the style attribs
+ this_style = dict(inkex.Style.parse_str(node.get("style", "")))
+ remaining_style = {} # Style attributes that are not propagated
+
+ non_propagated = ["filter"] # Filters should remain on the topmost ancestor
+ 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 = parent_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, Group, Anchor, Switch)):
+ # Leave only non-propagating style attributes
+ if remaining_style:
+ node.style = remaining_style
+ else:
+ if "style" in node.keys():
+ del node.attrib["style"]
+
+ # Remove the transform attribute
+ if "transform" in node.keys():
+ del node.attrib["transform"]
+
+ # Continue propagating on subelements
+ for child in node.iterchildren():
+ propagate_attribs(child, this_style, this_transform)
+ else:
+ # This element is not a container
+
+ # Merge remaining_style into this_style
+ this_style.update(remaining_style)
+
+ # Set the element's style and transform attribs
+ node.style = this_style
+ node.transform = this_transform
+
+
+### Style related
+
+
+def get_dimension(s="1024"):
+ """Convert an SVG length string from arbitrary units to pixels"""
+ return inkex.units.convert_unit(s, "px")
+
+
+###### Main Class #########################################
+class SynfigPrep(TempDirMixin, inkex.EffectExtension):
+ def effect(self):
+ """Transform document in preparation for exporting it into the Synfig format"""
+
+ self.preprocess()
+
+ # Remove inheritance of attributes
+ propagate_attribs(self.document.getroot())
+
+ # Fuse multiple subpaths in fills
+ for node in self.document.getroot().xpath("//svg:path"):
+ if node.get("d", "").lower().count("m") > 1:
+ # There are multiple subpaths
+ fill = split_fill_and_stroke(node)[0]
+ if fill is not None:
+ fill.path = fuse_subpaths(fill.path)
+
+ def preprocess(self):
+
+ actions = [
+ "unlock-all",
+ # Flow roots contain rectangles inside them, so they need to be
+ # converted to paths separately from other shapes
+ "select-by-element:flowRoot",
+ "object-to-path",
+ "select-clear",
+ ]
+
+ # Now convert all non-paths to paths
+ elements = ["rect", "circle", "ellipse", "line", "polyline", "polygon", "text"]
+ actions += ["select-by-element:" + i for i in elements]
+ actions += ["object-to-path", "select-clear"]
+ # unlink clones
+ actions += ["select-by-element:use", "object-unlink-clones"]
+ # save and overwrite
+ actions += ["export-overwrite", "export-do"]
+
+ infile = os.path.join(self.tempdir, "input.svg")
+ write_svg(self.document, infile)
+ try:
+ inkscape(infile, actions=";".join(actions))
+ except ProgramRunError as err:
+ inkex.errormsg(_("An error occurred during document preparation"))
+ inkex.errormsg(err.stderr.decode("utf-8"))
+
+ with open(infile, "r") as stream:
+ self.document = load_svg(stream)
+
+ return self.document
+
+
+if __name__ == "__main__":
+ SynfigPrep().run()