summaryrefslogtreecommitdiffstats
path: root/share/extensions/synfig_output.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xshare/extensions/synfig_output.py1485
1 files changed, 1485 insertions, 0 deletions
diff --git a/share/extensions/synfig_output.py b/share/extensions/synfig_output.py
new file mode 100755
index 0000000..327c013
--- /dev/null
+++ b/share/extensions/synfig_output.py
@@ -0,0 +1,1485 @@
+#!/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
+#
+"""
+An Inkscape extension for exporting Synfig files (.sif)
+"""
+import math
+import uuid
+from copy import deepcopy
+
+from lxml import etree
+
+import inkex
+from inkex import (
+ Group,
+ Layer,
+ Anchor,
+ Switch,
+ PathElement,
+ Metadata,
+ NamedView,
+ Gradient,
+ SvgDocumentElement,
+ Path,
+ Transform,
+)
+
+import synfig_fileformat as sif
+from synfig_prepare import MalformedSVGError, SynfigPrep, get_dimension
+
+
+# ##### Utility Classes ####################################
+class UnsupportedException(Exception):
+ """When part of an element is not supported, this exception is raised to invalidate the whole element"""
+
+ pass
+
+
+class SynfigDocument(object):
+ """A synfig document, with commands for adding layers and layer parameters"""
+
+ def __init__(self, width=1024, height=768, name="Synfig Animation 1"):
+ self.root_canvas = etree.fromstring(
+ """
+ <canvas
+ version="0.5"
+ width="{:f}"
+ height="{:f}"
+ xres="2834.645752"
+ yres="2834.645752"
+ view-box="0 0 0 0"
+ >
+ <name>{}</name>
+ </canvas>
+ """.format(
+ width, height, name
+ )
+ )
+
+ self._update_viewbox()
+
+ self.gradients = {}
+ self.filters = {}
+
+ # ## Properties
+
+ def get_root_canvas(self):
+ return self.root_canvas
+
+ def get_root_tree(self):
+ return self.root_canvas.getroottree()
+
+ def _update_viewbox(self):
+ """Update the viewbox to match document width and height"""
+ attr_viewbox = "{:f} {:f} {:f} {:f}".format(
+ -self.width / 2.0 / sif.kux,
+ self.height / 2.0 / sif.kux,
+ self.width / 2.0 / sif.kux,
+ -self.height / 2.0 / sif.kux,
+ )
+ self.root_canvas.set("view-box", attr_viewbox)
+
+ def get_width(self):
+ return float(self.root_canvas.get("width", "0"))
+
+ def set_width(self, value):
+ self.root_canvas.set("width", str(value))
+ self._update_viewbox()
+
+ def get_height(self):
+ return float(self.root_canvas.get("height", "0"))
+
+ def set_height(self, value):
+ self.root_canvas.set("height", str(value))
+ self._update_viewbox()
+
+ def get_name(self):
+ return self.root_canvas.get("name", "")
+
+ def set_name(self, value):
+ self.root_canvas.set("name", value)
+ self._update_viewbox()
+
+ width = property(get_width, set_width)
+ height = property(get_height, set_height)
+ name = property(get_name, set_name)
+
+ # ## Public utility functions
+
+ def new_guid(self):
+ """Generate a new GUID"""
+ return uuid.uuid4().hex
+
+ # ## Coordinate system conversions
+
+ def distance_svg2sif(self, distance):
+ """Convert distance from SVG to Synfig units"""
+ return distance / sif.kux
+
+ def distance_sif2svg(self, distance):
+ """Convert distance from Synfig to SVG units"""
+ return distance * sif.kux
+
+ def coor_svg2sif(self, vector):
+ """Convert SVG coordinate [x, y] to Synfig units"""
+ x = vector[0]
+ y = self.height - vector[1]
+
+ x -= self.width / 2.0
+ y -= self.height / 2.0
+ x /= sif.kux
+ y /= sif.kux
+
+ return [x, y]
+
+ def coor_sif2svg(self, vector):
+ """Convert Synfig coordinate [x, y] to SVG units"""
+ x = vector[0] * sif.kux + self.width / 2.0
+ y = vector[1] * sif.kux + self.height / 2.0
+
+ y = self.height - y
+
+ assert (
+ self.coor_svg2sif([x, y]) == vector
+ ), "sif to svg coordinate conversion error"
+
+ return [x, y]
+
+ def list_coor_svg2sif(self, l):
+ """Scan a list for coordinate pairs and convert them to Synfig units"""
+ # If list has two numerical elements,
+ # treat it as a coordinate pair
+ if type(l) == list and len(l) == 2:
+ if type(l[0]) == int or type(l[0]) == float:
+ if type(l[1]) == int or type(l[1]) == float:
+ l_sif = self.coor_svg2sif(l)
+ l[0] = l_sif[0]
+ l[1] = l_sif[1]
+ return
+
+ # Otherwise recursively iterate over the list
+ for x in l:
+ if type(x) == list:
+ self.list_coor_svg2sif(x)
+
+ def list_coor_sif2svg(self, l):
+ """Scan a list for coordinate pairs and convert them to SVG units"""
+ # If list has two numerical elements,
+ # treat it as a coordinate pair
+ if type(l) == list and len(l) == 2:
+ if type(l[0]) == int or type(l[0]) == float:
+ if type(l[1]) == int or type(l[1]) == float:
+ l_sif = self.coor_sif2svg(l)
+ l[0] = l_sif[0]
+ l[1] = l_sif[1]
+ return
+
+ # Otherwise recursively iterate over the list
+ for x in l:
+ if type(x) == list:
+ self.list_coor_sif2svg(x)
+
+ def bline_coor_svg2sif(self, b):
+ """Convert a BLine from SVG to Synfig coordinate units"""
+ self.list_coor_svg2sif(b["points"])
+
+ def bline_coor_sif2svg(self, b):
+ """Convert a BLine from Synfig to SVG coordinate units"""
+ self.list_coor_sif2svg(b["points"])
+
+ # ## XML Builders -- private
+ # ## used to create XML elements in the Synfig document
+
+ def build_layer(self, layer_type, desc, canvas=None, active=True, version="auto"):
+ """Build an empty layer"""
+ if canvas is None:
+ layer = self.root_canvas.makeelement("layer")
+ else:
+ layer = etree.SubElement(canvas, "layer")
+
+ layer.set("type", layer_type)
+ layer.set("desc", desc)
+ if active:
+ layer.set("active", "true")
+ else:
+ layer.set("active", "false")
+
+ if version == "auto":
+ version = sif.defaultLayerVersion(layer_type)
+
+ if type(version) == float:
+ version = str(version)
+
+ layer.set("version", version)
+
+ return layer
+
+ def _calc_radius(self, p1x, p1y, p2x, p2y):
+ """Calculate radius of a tangent given two points"""
+ # Synfig tangents are scaled by a factor of 3
+ return sif.tangent_scale * math.sqrt((p2x - p1x) ** 2 + (p2y - p1y) ** 2)
+
+ def _calc_angle(self, p1x, p1y, p2x, p2y):
+ """Calculate angle (in radians) of a tangent given two points"""
+ dx = p2x - p1x
+ dy = p2y - p1y
+ if dx > 0 and dy > 0:
+ ag = math.pi + math.atan(dy / dx)
+ elif dx > 0 > dy:
+ ag = math.pi + math.atan(dy / dx)
+ elif dx < 0 and dy < 0:
+ ag = math.atan(dy / dx)
+ elif dx < 0 < dy:
+ ag = 2 * math.pi + math.atan(dy / dx)
+ elif dx == 0 and dy > 0:
+ ag = -1 * math.pi / 2
+ elif dx == 0 and dy < 0:
+ ag = math.pi / 2
+ elif dx == 0 and dy == 0:
+ ag = 0
+ elif dx < 0 and dy == 0:
+ ag = 0
+ elif dx > 0 and dy == 0:
+ ag = math.pi
+
+ return (ag * 180) / math.pi
+
+ def build_param(self, layer, name, value, param_type="auto", guid=None):
+ """Add a parameter node to a layer"""
+ if layer is None:
+ param = self.root_canvas.makeelement("param")
+ else:
+ param = etree.SubElement(layer, "param")
+ param.set("name", name)
+
+ # Automatically detect param_type
+ if param_type == "auto":
+ if layer is not None:
+ layer_type = layer.get("type")
+ param_type = sif.paramType(layer_type, name)
+ else:
+ param_type = sif.paramType(None, name, value)
+
+ if param_type == "real":
+ el = etree.SubElement(param, "real")
+ el.set("value", str(float(value)))
+ elif param_type == "integer":
+ el = etree.SubElement(param, "integer")
+ el.set("value", str(int(value)))
+ elif param_type == "vector":
+ el = etree.SubElement(param, "vector")
+ x = etree.SubElement(el, "x")
+ x.text = str(float(value[0]))
+ y = etree.SubElement(el, "y")
+ y.text = str(float(value[1]))
+ elif param_type == "color":
+ el = etree.SubElement(param, "color")
+ r = etree.SubElement(el, "r")
+ r.text = str(float(value[0]))
+ g = etree.SubElement(el, "g")
+ g.text = str(float(value[1]))
+ b = etree.SubElement(el, "b")
+ b.text = str(float(value[2]))
+ a = etree.SubElement(el, "a")
+ a.text = str(float(value[3])) if len(value) > 3 else "1.0"
+ elif param_type == "gradient":
+ el = etree.SubElement(param, "gradient")
+ # Value is a dictionary of color stops
+ # see get_gradient()
+ for pos in value.keys():
+ color = etree.SubElement(el, "color")
+ color.set("pos", str(float(pos)))
+
+ c = value[pos]
+
+ r = etree.SubElement(color, "r")
+ r.text = str(float(c[0]))
+ g = etree.SubElement(color, "g")
+ g.text = str(float(c[1]))
+ b = etree.SubElement(color, "b")
+ b.text = str(float(c[2]))
+ a = etree.SubElement(color, "a")
+ a.text = str(float(c[3])) if len(c) > 3 else "1.0"
+ elif param_type == "bool":
+ el = etree.SubElement(param, "bool")
+ if value:
+ el.set("value", "true")
+ else:
+ el.set("value", "false")
+ elif param_type == "time":
+ el = etree.SubElement(param, "time")
+ if type(value) == int:
+ el.set("value", "{:d}s".format(value))
+ elif type(value) == float:
+ el.set("value", "{:f}s".format(value))
+ elif type(value) == str:
+ el.set("value", value)
+ elif param_type == "bline":
+ el = etree.SubElement(param, "bline")
+ el.set("type", "bline_point")
+
+ # value is a bline (dictionary type), see path_to_bline_list
+ if value["loop"]:
+ el.set("loop", "true")
+ else:
+ el.set("loop", "false")
+
+ for vertex in value["points"]:
+ x = float(vertex[1][0])
+ y = float(vertex[1][1])
+
+ tg1x = float(vertex[0][0])
+ tg1y = float(vertex[0][1])
+
+ tg2x = float(vertex[2][0])
+ tg2y = float(vertex[2][1])
+
+ tg1_radius = self._calc_radius(x, y, tg1x, tg1y)
+ tg1_angle = self._calc_angle(x, y, tg1x, tg1y)
+
+ tg2_radius = self._calc_radius(x, y, tg2x, tg2y)
+ tg2_angle = self._calc_angle(x, y, tg2x, tg2y) - 180.0
+
+ if vertex[3]:
+ split = "true"
+ else:
+ split = "false"
+
+ entry = etree.SubElement(el, "entry")
+ composite = etree.SubElement(entry, "composite")
+ composite.set("type", "bline_point")
+
+ point = etree.SubElement(composite, "point")
+ vector = etree.SubElement(point, "vector")
+ etree.SubElement(vector, "x").text = str(x)
+ etree.SubElement(vector, "y").text = str(y)
+
+ width = etree.SubElement(composite, "width")
+ etree.SubElement(width, "real").set("value", "1.0")
+
+ origin = etree.SubElement(composite, "origin")
+ etree.SubElement(origin, "real").set("value", "0.5")
+
+ split_el = etree.SubElement(composite, "split")
+ etree.SubElement(split_el, "bool").set("value", split)
+
+ t1 = etree.SubElement(composite, "t1")
+ t2 = etree.SubElement(composite, "t2")
+
+ t1_rc = etree.SubElement(t1, "radial_composite")
+ t1_rc.set("type", "vector")
+
+ t2_rc = etree.SubElement(t2, "radial_composite")
+ t2_rc.set("type", "vector")
+
+ t1_r = etree.SubElement(t1_rc, "radius")
+ t2_r = etree.SubElement(t2_rc, "radius")
+ t1_radius = etree.SubElement(t1_r, "real")
+ t2_radius = etree.SubElement(t2_r, "real")
+ t1_radius.set("value", str(tg1_radius))
+ t2_radius.set("value", str(tg2_radius))
+
+ t1_t = etree.SubElement(t1_rc, "theta")
+ t2_t = etree.SubElement(t2_rc, "theta")
+ t1_angle = etree.SubElement(t1_t, "angle")
+ t2_angle = etree.SubElement(t2_t, "angle")
+ t1_angle.set("value", str(tg1_angle))
+ t2_angle.set("value", str(tg2_angle))
+ elif param_type == "canvas":
+ el = etree.SubElement(param, "canvas")
+ el.set("xres", "10.0")
+ el.set("yres", "10.0")
+
+ # "value" is a list of layers
+ if value is not None:
+ for layer in value:
+ el.append(layer)
+ else:
+ raise AssertionError("Unsupported param type {}".format(param_type))
+
+ if guid:
+ el.set("guid", guid)
+ else:
+ el.set("guid", self.new_guid())
+
+ return param
+
+ # ## Public layer API
+ # ## Should be used by outside functions to create layers and set layer parameters
+
+ def create_layer(
+ self,
+ layer_type,
+ desc,
+ params={},
+ guids={},
+ canvas=None,
+ active=True,
+ version="auto",
+ ):
+ """Create a new layer
+
+ Keyword arguments:
+ layer_type -- layer type string used internally by Synfig
+ desc -- layer description
+ params -- a dictionary of parameter names and their values
+ guids -- a dictionary of parameter types and their guids (optional)
+ active -- set to False to create a hidden layer
+ """
+ layer = self.build_layer(layer_type, desc, canvas, active, version)
+ default_layer_params = sif.defaultLayerParams(layer_type)
+
+ for param_name in default_layer_params.keys():
+ param_type = default_layer_params[param_name][0]
+ if param_name in params.keys():
+ param_value = params[param_name]
+ else:
+ param_value = default_layer_params[param_name][1]
+
+ if param_name in guids.keys():
+ param_guid = guids[param_name]
+ else:
+ param_guid = None
+
+ if param_value is not None:
+ self.build_param(
+ layer, param_name, param_value, param_type, guid=param_guid
+ )
+
+ return layer
+
+ def set_param(
+ self, layer, name, value, param_type="auto", guid=None, modify_linked=False
+ ):
+ """Set a layer parameter
+
+ Keyword arguments:
+ layer -- the layer to set the parameter for
+ name -- parameter name
+ value -- parameter value
+ param_type -- parameter type (default "auto")
+ guid -- guid of the parameter value
+ """
+ if modify_linked:
+ raise AssertionError("Modifying linked parameters is not supported")
+
+ layer_type = layer.get("type")
+ assert layer_type, "Layer does not have a type"
+
+ if param_type == "auto":
+ param_type = sif.paramType(layer_type, name)
+
+ # Remove existing parameters with this name
+ existing = []
+ for param in layer.iterchildren():
+ if param.get("name") == name:
+ existing.append(param)
+
+ if len(existing) == 0:
+ self.build_param(layer, name, value, param_type, guid)
+ elif len(existing) > 1:
+ raise AssertionError("Found multiple parameters with the same name")
+ else:
+ new_param = self.build_param(None, name, value, param_type, guid)
+ layer.replace(existing[0], new_param)
+
+ def set_params(self, layer, params={}, guids={}, modify_linked=False):
+ """Set layer parameters
+
+ Keyword arguments:
+ layer -- the layer to set the parameter for
+ params -- a dictionary of parameter names and their values
+ guids -- a dictionary of parameter types and their guids (optional)
+ """
+ for param_name in params.keys():
+ if param_name in guids.keys():
+ self.set_param(
+ layer,
+ param_name,
+ params[param_name],
+ guid=guids[param_name],
+ modify_linked=modify_linked,
+ )
+ else:
+ self.set_param(
+ layer, param_name, params[param_name], modify_linked=modify_linked
+ )
+
+ def get_param(self, layer, name, param_type="auto"):
+ """Get the value of a layer parameter
+
+ Keyword arguments:
+ layer -- the layer to get the parameter from
+ name -- param name
+ param_type -- parameter type (default "auto")
+
+ NOT FULLY IMPLEMENTED
+ """
+ layer_type = layer.get("type")
+ assert layer_type, "Layer does not have a type"
+
+ if param_type == "auto":
+ param_type = sif.paramType(layer_type, name)
+
+ for param in layer.iterchildren():
+ if param.get("name") == name:
+ if param_type == "real":
+ return float(param[0].get("value", "0"))
+ elif param_type == "integer":
+ return int(param[0].get("integer", "0"))
+ else:
+ raise Exception(
+ "Getting this type of parameter not yet implemented"
+ )
+
+ # ## Global defs, and related
+
+ # SVG Filters
+ def add_filter(self, filter_id, f):
+ """Register a filter"""
+ self.filters[filter_id] = f
+
+ # SVG Gradients
+ def add_linear_gradient(
+ self,
+ gradient_id,
+ p1,
+ p2,
+ mtx=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
+ stops=[],
+ link="",
+ spread_method="pad",
+ ):
+ """Register a linear gradient definition"""
+ gradient = {
+ "type": "linear",
+ "p1": p1,
+ "p2": p2,
+ "mtx": mtx,
+ "spreadMethod": spread_method,
+ }
+ if stops:
+ gradient["stops"] = stops
+ gradient["stops_guid"] = self.new_guid()
+ elif link != "":
+ gradient["link"] = link
+ else:
+ raise MalformedSVGError("Gradient has neither stops nor link")
+ self.gradients[gradient_id] = gradient
+
+ def add_radial_gradient(
+ self,
+ gradient_id,
+ center,
+ radius,
+ focus,
+ mtx=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
+ stops=[],
+ link="",
+ spread_method="pad",
+ ):
+ """Register a radial gradient definition"""
+ gradient = {
+ "type": "radial",
+ "center": center,
+ "radius": radius,
+ "focus": focus,
+ "mtx": mtx,
+ "spreadMethod": spread_method,
+ }
+ if stops:
+ gradient["stops"] = stops
+ gradient["stops_guid"] = self.new_guid()
+ elif link != "":
+ gradient["link"] = link
+ else:
+ raise MalformedSVGError("Gradient has neither stops nor link")
+ self.gradients[gradient_id] = gradient
+
+ def get_gradient(self, gradient_id):
+ """
+ Return a gradient with a given id
+
+ Linear gradient format:
+ {
+ "type" : "linear",
+ "p1" : [x, y],
+ "p2" : [x, y],
+ "mtx" : mtx,
+ "stops" : color stops,
+ "stops_guid": color stops guid,
+ "spreadMethod": "pad", "reflect", or "repeat"
+ }
+
+ Radial gradient format:
+ {
+ "type" : "radial",
+ "center" : [x, y],
+ "radius" : r,
+ "focus" : [x, y],
+ "mtx" : mtx,
+ "stops" : color stops,
+ "stops_guid": color stops guid,
+ "spreadMethod": "pad", "reflect", or "repeat"
+ }
+
+ Color stops format
+ {
+ 0.0 : color ([r,g,b,a] or [r,g,b]) at start,
+ [a number] : color at that position,
+ 1.0 : color at end
+ }
+ """
+
+ if gradient_id not in self.gradients.keys():
+ return None
+
+ gradient = self.gradients[gradient_id]
+
+ # If the gradient has no link, we are done
+ if "link" not in gradient.keys() or gradient["link"] == "":
+ return gradient
+
+ # If the gradient does have a link, find the color stops recursively
+ if gradient["link"] not in self.gradients.keys():
+ raise MalformedSVGError("Linked gradient ID not found")
+
+ linked_gradient = self.get_gradient(gradient["link"])
+ gradient["stops"] = linked_gradient["stops"]
+ gradient["stops_guid"] = linked_gradient["stops_guid"]
+ del gradient["link"]
+
+ # Update the gradient in our listing
+ # (so recursive lookup only happens once)
+ self.gradients[gradient_id] = gradient
+
+ return gradient
+
+ def gradient_to_params(self, gradient):
+ """Transform gradient to a list of parameters to pass to a Synfig layer"""
+ # Create a copy of the gradient
+ g = gradient.copy()
+
+ # Set synfig-only attribs
+ if g["spreadMethod"] == "repeat":
+ g["loop"] = True
+ elif g["spreadMethod"] == "reflect":
+ g["loop"] = True
+ # Reflect the gradient
+ # Original: 0.0 [A . B . C] 1.0
+ # New: 0.0 [A . B . C . B . A] 1.0
+ # (with gradient size doubled)
+ new_stops = {}
+
+ # reflect the stops
+ for pos in g["stops"]:
+ val = g["stops"][pos]
+ if pos == 1.0:
+ new_stops[pos / 2.0] = val
+ else:
+ new_stops[pos / 2.0] = val
+ new_stops[1 - pos / 2.0] = val
+ g["stops"] = new_stops
+
+ # double the gradient size
+ if g["type"] == "linear":
+ g["p2"] = [
+ g["p1"][0] + 2.0 * (g["p2"][0] - g["p1"][0]),
+ g["p1"][1] + 2.0 * (g["p2"][1] - g["p1"][1]),
+ ]
+ if g["type"] == "radial":
+ g["radius"] *= 2.0
+
+ # Rename "stops" to "gradient"
+ g["gradient"] = g["stops"]
+
+ # Convert coordinates
+ if g["type"] == "linear":
+ g["p1"] = self.coor_svg2sif(g["p1"])
+ g["p2"] = self.coor_svg2sif(g["p2"])
+
+ if g["type"] == "radial":
+ g["center"] = self.coor_svg2sif(g["center"])
+ g["radius"] = self.distance_svg2sif(g["radius"])
+
+ # Delete extra attribs
+ removed_attribs = [
+ "type",
+ "stops",
+ "stops_guid",
+ "mtx",
+ "focus",
+ "spreadMethod",
+ ]
+ for x in removed_attribs:
+ if x in g.keys():
+ del g[x]
+ return g
+
+ # ## Public operations API
+ # Operations act on a series of layers, and (optionally) on a series of named parameters
+ # The "is_end" attribute should be set to true when the layers are at the end of a canvas
+ # (i.e. when adding transform layers on top of them does not require encapsulation)
+
+ def op_blur(self, layers, x, y, name="Blur", is_end=False):
+ """Gaussian blur the given layers by the given x and y amounts
+
+ Keyword arguments:
+ layers -- list of layers
+ x -- x-amount of blur
+ y -- x-amount of blur
+ is_end -- set to True if layers are at the end of a canvas
+
+ Returns: list of layers
+ """
+ blur = self.create_layer(
+ "blur",
+ name,
+ params={"blend_method": sif.blend_methods["straight"], "size": [x, y]},
+ )
+
+ if is_end:
+ return layers + [blur]
+ else:
+ return self.op_encapsulate(layers + [blur])
+
+ def op_color(self, layers, overlay, is_end=False):
+ """Apply a color overlay to the given layers
+
+ Should be used to apply a gradient or pattern to a shape
+
+ Keyword arguments:
+ layers -- list of layers
+ overlay -- color layer to apply
+ is_end -- set to True if layers are at the end of a canvas
+
+ Returns: list of layers
+ """
+ if not layers:
+ return layers
+ if overlay is None:
+ return layers
+
+ overlay_enc = self.op_encapsulate([overlay])
+ self.set_param(
+ overlay_enc[0], "blend_method", sif.blend_methods["straight onto"]
+ )
+ ret = layers + overlay_enc
+
+ if is_end:
+ return ret
+ else:
+ return self.op_encapsulate(ret)
+
+ def op_encapsulate(self, layers, name="Inline Canvas", is_end=False):
+ """Encapsulate the given layers
+
+ Keyword arguments:
+ layers -- list of layers
+ name -- Name of the PasteCanvas layer that is created
+ is_end -- set to True if layers are at the end of a canvas
+
+ Returns: list of one layer
+ """
+
+ if not layers:
+ return layers
+
+ layer = self.create_layer("PasteCanvas", name, params={"canvas": layers})
+ return [layer]
+
+ def op_fade(self, layers, opacity, is_end=False):
+ """Increase the opacity of the given layers by a certain amount
+
+ Keyword arguments:
+ layers -- list of layers
+ opacity -- the opacity to apply (float between 0.0 to 1.0)
+ name -- name of the Transform layer that is added
+ is_end -- set to True if layers are at the end of a canvas
+
+ Returns: list of layers
+ """
+ # If there is blending involved, first encapsulate the layers
+ for layer in layers:
+ if self.get_param(layer, "blend_method") != sif.blend_methods["composite"]:
+ return self.op_fade(self.op_encapsulate(layers), opacity, is_end)
+
+ # Otherwise, set their amount
+ for layer in layers:
+ amount = self.get_param(layer, "amount")
+ self.set_param(layer, "amount", amount * opacity)
+
+ return layers
+
+ def op_filter(self, layers, filter_id, is_end=False):
+ """Apply a filter to the given layers
+
+ Keyword arguments:
+ layers -- list of layers
+ filter_id -- id of the filter
+ is_end -- set to True if layers are at the end of a canvas
+
+ Returns: list of layers
+ """
+ if filter_id not in self.filters.keys():
+ raise MalformedSVGError("Filter {} not found".format(filter_id))
+
+ try:
+ ret = self.filters[filter_id](self, layers, is_end)
+ assert type(ret) == list
+ return ret
+ except UnsupportedException:
+ # If the filter is not supported, ignore it.
+ return layers
+
+ def op_set_blend(self, layers, blend_method, is_end=False):
+ """Set the blend method of the given group of layers
+
+ If more than one layer is supplied, they will be encapsulated.
+
+ Keyword arguments:
+ layers -- list of layers
+ blend_method -- blend method to give the layers
+ is_end -- set to True if layers are at the end of a canvas
+
+ Returns: list of layers
+ """
+ if not layers:
+ return layers
+ if blend_method == "composite":
+ return layers
+
+ layer = layers[0]
+ if len(layers) > 1 or self.get_param(layers[0], "amount") != 1.0:
+ layer = self.op_encapsulate(layers)[0]
+
+ layer = deepcopy(layer)
+
+ self.set_param(layer, "blend_method", sif.blend_methods[blend_method])
+
+ return [layer]
+
+ def op_transform(self, layers, mtx, name="Transform", is_end=False):
+ """Apply a matrix transformation to the given layers
+
+ Keyword arguments:
+ layers -- list of layers
+ mtx -- transformation matrix
+ name -- name of the Transform layer that is added
+ is_end -- set to True if layers are at the end of a canvas
+
+ Returns: list of layers
+ """
+ if not layers:
+ return layers
+ if mtx is None or mtx == [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]:
+ return layers
+
+ src_tl = [100, 100]
+ src_br = [200, 200]
+
+ dest_tl = [100, 100]
+ dest_tr = [200, 100]
+ dest_br = [200, 200]
+ dest_bl = [100, 200]
+
+ dest_tl = Transform(mtx).apply_to_point(dest_tl)
+ dest_tr = Transform(mtx).apply_to_point(dest_tr)
+ dest_br = Transform(mtx).apply_to_point(dest_br)
+ dest_bl = Transform(mtx).apply_to_point(dest_bl)
+
+ warp = self.create_layer(
+ "warp",
+ name,
+ params={
+ "src_tl": self.coor_svg2sif(src_tl),
+ "src_br": self.coor_svg2sif(src_br),
+ "dest_tl": self.coor_svg2sif(dest_tl),
+ "dest_tr": self.coor_svg2sif(dest_tr),
+ "dest_br": self.coor_svg2sif(dest_br),
+ "dest_bl": self.coor_svg2sif(dest_bl),
+ },
+ )
+
+ if is_end:
+ return layers + [warp]
+ else:
+ return self.op_encapsulate(layers + [warp])
+
+
+# ##### Utility Functions ##################################
+
+# ## Path related
+
+
+def path_to_bline_list(path_d, nodetypes=None, mtx=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
+ """
+ Convert a path to a BLine List
+
+ bline_list format:
+
+ Vertex:
+ [[tg1x, tg1y], [x,y], [tg2x, tg2y], split = T/F]
+ Vertex list:
+ [ vertex, vertex, vertex, ...]
+ Bline:
+ {
+ "points" : vertex_list,
+ "loop" : True / False
+ }
+ """
+
+ # Exit on empty paths
+ if not path_d:
+ return []
+
+ # Parse the path
+ path = Path(path_d).to_arrays()
+
+ # Append (more than) enough c's to the nodetypes
+ if nodetypes is None:
+ nt = ""
+ else:
+ nt = nodetypes
+
+ for _ in range(len(path)):
+ nt += "c"
+
+ # Create bline list
+ # borrows code from cubicsuperpath.py
+
+ # bline_list := [bline, bline, ...]
+ # bline := {
+ # "points":[vertex, vertex, ...],
+ # "loop":True/False,
+ # }
+
+ bline_list = []
+
+ subpathstart = []
+ last = []
+ lastctrl = []
+ lastsplit = True
+
+ for s in path:
+ cmd, params = s
+ if cmd != "M" and bline_list == []:
+ raise MalformedSVGError(
+ "Bad path data: path doesn't start with moveto, {}, {}".format(s, path)
+ )
+ elif cmd == "M":
+ # Add previous point to subpath
+ if last:
+ bline_list[-1]["points"].append(
+ [lastctrl[:], last[:], last[:], lastsplit]
+ )
+ # Start a new subpath
+ bline_list.append({"nodetypes": "", "loop": False, "points": []})
+ # Save coordinates of this point
+ subpathstart = params[:]
+ last = params[:]
+ lastctrl = params[:]
+ lastsplit = False if nt[0] == "z" else True
+ nt = nt[1:]
+ elif cmd in "LHV":
+ bline_list[-1]["points"].append([lastctrl[:], last[:], last[:], lastsplit])
+ if cmd == "H":
+ last = [params[0], last[1]]
+ lastctrl = [params[0], last[1]]
+ elif cmd == "V":
+ last = [last[0], params[0]]
+ lastctrl = [last[0], params[0]]
+ else:
+ last = params[:]
+ lastctrl = params[:]
+ lastsplit = False if nt[0] == "z" else True
+ nt = nt[1:]
+ elif cmd == "C":
+ bline_list[-1]["points"].append(
+ [lastctrl[:], last[:], params[:2], lastsplit]
+ )
+ last = params[-2:]
+ lastctrl = params[2:4]
+ lastsplit = False if nt[0] == "z" else True
+ nt = nt[1:]
+ elif cmd == "Q":
+ q0 = last[:]
+ q1 = params[0:2]
+ q2 = params[2:4]
+ x0 = q0[0]
+ x1 = 1.0 / 3 * q0[0] + 2.0 / 3 * q1[0]
+ x2 = 2.0 / 3 * q1[0] + 1.0 / 3 * q2[0]
+ x3 = q2[0]
+ y0 = q0[1]
+ y1 = 1.0 / 3 * q0[1] + 2.0 / 3 * q1[1]
+ y2 = 2.0 / 3 * q1[1] + 1.0 / 3 * q2[1]
+ y3 = q2[1]
+ bline_list[-1]["points"].append(
+ [lastctrl[:], [x0, y0], [x1, y1], lastsplit]
+ )
+ last = [x3, y3]
+ lastctrl = [x2, y2]
+ lastsplit = False if nt[0] == "z" else True
+ nt = nt[1:]
+ elif cmd == "A":
+ from inkex.paths import arc_to_path
+
+ arcp = arc_to_path(last[:], params[:])
+ arcp[0][0] = lastctrl[:]
+ last = arcp[-1][1]
+ lastctrl = arcp[-1][0]
+ lastsplit = False if nt[0] == "z" else True
+ nt = nt[1:]
+ for el in arcp[:-1]:
+ el.append(True)
+ bline_list[-1]["points"].append(el)
+ elif cmd == "Z":
+ if len(bline_list[-1]["points"]) == 0:
+ # If the path "loops" after only one point
+ # e.g. "M 0 0 Z"
+ bline_list[-1]["points"].append([lastctrl[:], last[:], last[:], False])
+ elif last == subpathstart:
+ # If we are back to the original position
+ # merge our tangent into the first point
+ bline_list[-1]["points"][0][0] = lastctrl[:]
+ else:
+ # Otherwise draw a line to the starting point
+ bline_list[-1]["points"].append(
+ [lastctrl[:], last[:], last[:], lastsplit]
+ )
+
+ # Clear the variables (no more points need to be added)
+ last = []
+ lastctrl = []
+ lastsplit = True
+
+ # Loop the subpath
+ bline_list[-1]["loop"] = True
+ # Append final superpoint, if needed
+ if last:
+ bline_list[-1]["points"].append([lastctrl[:], last[:], last[:], lastsplit])
+
+ # Apply the transformation
+ if mtx != [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]:
+ for bline in bline_list:
+ for vertex in bline["points"]:
+ for point in vertex:
+ if not isinstance(point, bool):
+ pnt = Transform(mtx).apply_to_point(point)
+ point[0], point[1] = pnt[0], pnt[1]
+
+ return bline_list
+
+
+# ## Style related
+
+
+def extract_color(style, color_attrib, *opacity_attribs):
+ if color_attrib in style.keys():
+ if style[color_attrib] == "none":
+ return [1, 1, 1, 0]
+ c = inkex.Color(style[color_attrib]).to_rgb()
+ else:
+ c = (0, 0, 0)
+
+ # Convert color scales and adjust gamma
+ color = [
+ pow(c[0] / 255.0, sif.gamma),
+ pow(c[1] / 255.0, sif.gamma),
+ pow(c[2] / 255.0, sif.gamma),
+ 1.0,
+ ]
+
+ for opacity in opacity_attribs:
+ if opacity in style.keys():
+ color[3] *= float(style[opacity])
+ return color
+
+
+def extract_opacity(style, *opacity_attribs):
+ ret = 1.0
+ for opacity in opacity_attribs:
+ if opacity in style.keys():
+ ret *= float(style[opacity])
+ return ret
+
+
+def extract_width(style, width_attrib, mtx):
+ if width_attrib in style.keys():
+ width = get_dimension(style[width_attrib])
+ else:
+ width = 1
+
+ area_scale_factor = mtx[0][0] * mtx[1][1] - mtx[0][1] * mtx[1][0]
+ linear_scale_factor = math.sqrt(abs(area_scale_factor))
+
+ return width * linear_scale_factor / sif.kux
+
+
+# ##### Main Class #########################################
+class SynfigExport(SynfigPrep):
+ def __init__(self):
+ SynfigPrep.__init__(self)
+
+ def effect(self):
+ # Prepare the document for exporting
+ SynfigPrep.effect(self)
+ svg = self.document.getroot()
+ width = get_dimension(svg.get("width", 1024))
+ height = get_dimension(svg.get("height", 768))
+
+ title = svg.getElement("svg:title")
+ if title is not None:
+ name = title.text
+ else:
+ name = svg.get("sodipodi:docname", "Synfig Animation 1")
+
+ doc = SynfigDocument(width, height, name)
+
+ layers = []
+ for node in svg.iterchildren():
+ layers += self.convert_node(node, doc)
+
+ root_canvas = doc.get_root_canvas()
+ for layer in layers:
+ root_canvas.append(layer)
+
+ self.synfig_document = doc.get_root_tree()
+
+ def save(self, stream):
+ self.synfig_document.write(stream)
+
+ def convert_node(self, node, d):
+ """Convert an SVG node to a list of Synfig layers"""
+ # Parse tags that don't draw any layers
+ if isinstance(node, SvgDocumentElement):
+ self.parse_defs(node, d)
+ return []
+ elif not isinstance(
+ node, (Group, Anchor, Switch, PathElement, Metadata, NamedView)
+ ):
+ # An unsupported element
+ return []
+
+ layers = []
+ if isinstance(node, Group):
+ for subnode in node:
+ layers += self.convert_node(subnode, d)
+ if isinstance(node, Layer):
+ name = node.label or "Inline Canvas"
+ layers = d.op_encapsulate(layers, name=name)
+
+ elif isinstance(node, (Anchor, Switch)):
+ # Treat anchor and switch as a group
+ for subnode in node:
+ layers += self.convert_node(subnode, d)
+ elif isinstance(node, PathElement):
+ layers = self.convert_path(node, d)
+
+ style = node.style
+ if "filter" in style.keys() and style["filter"].startswith("url"):
+ filter_id = style["filter"][5:].split(")")[0]
+ layers = d.op_filter(layers, filter_id)
+
+ opacity = extract_opacity(style, "opacity")
+ if opacity != 1.0:
+ layers = d.op_fade(layers, opacity)
+
+ return layers
+
+ def parse_defs(self, node, d):
+ for child in node.iterchildren():
+ if isinstance(child, Gradient):
+ self.parse_gradient(child, d)
+ elif child.TAG == "filter":
+ self.parse_filter(child, d)
+
+ def parse_gradient(self, node, d):
+ if node.TAG == "linearGradient":
+ gradient_id = node.get("id", str(id(node)))
+ x1 = float(node.get("x1", "0.0"))
+ x2 = float(node.get("x2", "0.0"))
+ y1 = float(node.get("y1", "0.0"))
+ y2 = float(node.get("y2", "0.0"))
+
+ mtx = node.gradientTransform.matrix
+
+ link = node.get("xlink:href", "#")[1:]
+ spread_method = node.get("spreadMethod", "pad")
+ if link == "":
+ stops = self.parse_stops(node, d)
+ d.add_linear_gradient(
+ gradient_id,
+ [x1, y1],
+ [x2, y2],
+ mtx,
+ stops=stops,
+ spread_method=spread_method,
+ )
+ else:
+ d.add_linear_gradient(
+ gradient_id,
+ [x1, y1],
+ [x2, y2],
+ mtx,
+ link=link,
+ spread_method=spread_method,
+ )
+ elif node.TAG == "radialGradient":
+ gradient_id = node.get("id", str(id(node)))
+ cx = float(node.get("cx", "0.0"))
+ cy = float(node.get("cy", "0.0"))
+ r = float(node.get("r", "0.0"))
+ fx = float(node.get("fx", "0.0"))
+ fy = float(node.get("fy", "0.0"))
+
+ mtx = node.gradientTransform.matrix
+
+ link = node.get("xlink:href", "#")[1:]
+ spread_method = node.get("spreadMethod", "pad")
+ if link == "":
+ stops = self.parse_stops(node, d)
+ d.add_radial_gradient(
+ gradient_id,
+ [cx, cy],
+ r,
+ [fx, fy],
+ mtx,
+ stops=stops,
+ spread_method=spread_method,
+ )
+ else:
+ d.add_radial_gradient(
+ gradient_id,
+ [cx, cy],
+ r,
+ [fx, fy],
+ mtx,
+ link=link,
+ spread_method=spread_method,
+ )
+
+ def parse_stops(self, node, d):
+ stops = {}
+ for stop in node.iterchildren():
+ if stop.TAG == "stop":
+ offset = float(stop.get("offset"))
+ style = stop.style
+ stops[offset] = extract_color(style, "stop-color", "stop-opacity")
+ else:
+ raise MalformedSVGError("Child of gradient is not a stop")
+
+ return stops
+
+ def parse_filter(self, node, d):
+ filter_id = node.get("id", str(id(node)))
+
+ # A filter is just like an operator (the op_* functions),
+ # except that it's created here
+ def the_filter(d, layers, is_end=False):
+ refs = {None: layers, "SourceGraphic": layers} # default
+ encapsulate_result = not is_end
+
+ for child in node.iterchildren():
+ if child.get("in") not in refs:
+ # "SourceAlpha", "BackgroundImage",
+ # "BackgroundAlpha", "FillPaint", "StrokePaint"
+ # are not supported
+ raise UnsupportedException
+ l_in = refs[child.get("in")]
+ l_out = []
+ if child.TAG == "feGaussianBlur":
+ std_dev = child.get("stdDeviation", "0")
+ std_dev = std_dev.replace(",", " ").split()
+ x = float(std_dev[0])
+ if len(std_dev) > 1:
+ y = float(std_dev[1])
+ else:
+ y = x
+
+ if x == 0 and y == 0:
+ l_out = l_in
+ else:
+ x = d.distance_svg2sif(x)
+ y = d.distance_svg2sif(y)
+ l_out = d.op_blur(l_in, x, y, is_end=True)
+ elif child.TAG == "feBlend":
+ # Note: Blend methods are not an exact match
+ # because SVG uses alpha channel in places where
+ # Synfig does not
+ mode = child.get("mode", "normal")
+ if mode == "normal":
+ blend_method = "composite"
+ elif mode == "multiply":
+ blend_method = "multiply"
+ elif mode == "screen":
+ blend_method = "screen"
+ elif mode == "darken":
+ blend_method = "darken"
+ elif mode == "lighten":
+ blend_method = "brighten"
+ else:
+ raise MalformedSVGError("Invalid blend method")
+
+ if child.get("in2") == "BackgroundImage":
+ encapsulate_result = False
+ l_out = d.op_set_blend(l_in, blend_method) + d.op_set_blend(
+ l_in, "behind"
+ )
+ elif child.get("in2") not in refs:
+ raise UnsupportedException
+ else:
+ l_in2 = refs[child.get("in2")]
+ l_out = l_in2 + d.op_set_blend(l_in, blend_method)
+
+ else:
+ # This filter element is currently unsupported
+ raise UnsupportedException
+
+ # Output the layers
+ if child.get("result"):
+ refs[child.get("result")] = l_out
+
+ # Set the default for the next filter element
+ refs[None] = l_out
+
+ # Return the output from the last element
+ if len(refs[None]) > 1 and encapsulate_result:
+ return d.op_encapsulate(refs[None])
+ else:
+ return refs[None]
+
+ d.add_filter(filter_id, the_filter)
+
+ def convert_path(self, node, d):
+ """Convert an SVG path node to a list of Synfig layers"""
+ layers = []
+
+ node_id = node.get("id", str(id(node)))
+ style = node.style
+
+ mtx = node.transform.matrix
+ blines = path_to_bline_list(node.get("d"), node.get("sodipodi:nodetypes"), mtx)
+ for bline in blines:
+ d.bline_coor_svg2sif(bline)
+ bline_guid = d.new_guid()
+
+ if style.setdefault("fill", "#000000") != "none":
+ if style["fill"].startswith("url"):
+ # Set the color to black, so we can later overlay
+ # the shape with a gradient or pattern
+ color = [0, 0, 0, 1]
+ else:
+ color = extract_color(style, "fill", "fill-opacity")
+
+ layer = d.create_layer(
+ "region",
+ node_id,
+ {
+ "bline": bline,
+ "color": color,
+ "winding_style": 1
+ if style.setdefault("fill-rule", "nonzero") == "evenodd"
+ else 0,
+ },
+ guids={"bline": bline_guid},
+ )
+
+ if style["fill"].startswith("url"):
+ color_layer = self.convert_url(
+ style["fill"][5:].split(")")[0], mtx, d
+ )[0]
+ layer = d.op_color([layer], overlay=color_layer)[0]
+ layer = d.op_fade([layer], extract_opacity(style, "fill-opacity"))[
+ 0
+ ]
+
+ layers.append(layer)
+
+ if style.setdefault("stroke", "none") != "none":
+ if style["stroke"].startswith("url"):
+ # Set the color to black, so we can later overlay
+ # the shape with a gradient or pattern
+ color = [0, 0, 0, 1]
+ else:
+ color = extract_color(style, "stroke", "stroke-opacity")
+
+ layer = d.create_layer(
+ "outline",
+ node_id,
+ {
+ "bline": bline,
+ "color": color,
+ "width": extract_width(style, "stroke-width", mtx),
+ "sharp_cusps": True
+ if style.setdefault("stroke-linejoin", "miter") == "miter"
+ else False,
+ "round_tip[0]": False
+ if style.setdefault("stroke-linecap", "butt") == "butt"
+ else True,
+ "round_tip[1]": False
+ if style.setdefault("stroke-linecap", "butt") == "butt"
+ else True,
+ },
+ guids={"bline": bline_guid},
+ )
+
+ if style["stroke"].startswith("url"):
+ color_layer = self.convert_url(
+ style["stroke"][5:].split(")")[0], mtx, d
+ )[0]
+ layer = d.op_color([layer], overlay=color_layer)[0]
+ layer = d.op_fade(
+ [layer], extract_opacity(style, "stroke-opacity")
+ )[0]
+
+ layers.append(layer)
+
+ return layers
+
+ def convert_url(self, url_id, mtx, d):
+ """Return a list Synfig layers that represent the gradient with the given id"""
+ gradient = d.get_gradient(url_id)
+ if gradient is None:
+ # Patterns and other URLs not supported
+ return [None]
+
+ if gradient["type"] == "linear":
+ layer = d.create_layer(
+ "linear_gradient",
+ url_id,
+ d.gradient_to_params(gradient),
+ guids={"gradient": gradient["stops_guid"]},
+ )
+
+ if gradient["type"] == "radial":
+ layer = d.create_layer(
+ "radial_gradient",
+ url_id,
+ d.gradient_to_params(gradient),
+ guids={"gradient": gradient["stops_guid"]},
+ )
+
+ trm = Transform(mtx) * Transform(gradient["mtx"])
+ return d.op_transform([layer], trm.matrix)
+
+
+if __name__ == "__main__":
+ SynfigExport().run()