summaryrefslogtreecommitdiffstats
path: root/share
diff options
context:
space:
mode:
authorNikita Kitaev <nikitakit@gmail.com>2011-11-25 19:00:24 +0000
committerNikita Kitaev <nikitakit@gmail.com>2011-11-25 19:00:24 +0000
commitd989dd75e290c2d7e2164a1fa3f066186a10946a (patch)
tree6bf87c82b9ed533b62d47f23c9068122db5e3414 /share
parentdxf input. improved support for multispline (Bug 892496) (diff)
downloadinkscape-d989dd75e290c2d7e2164a1fa3f066186a10946a.tar.gz
inkscape-d989dd75e290c2d7e2164a1fa3f066186a10946a.zip
Add Synfig Animation Studio (*.sif) file output extension
(bzr r10750)
Diffstat (limited to 'share')
-rw-r--r--share/extensions/Makefile.am4
-rwxr-xr-xshare/extensions/synfig_fileformat.py246
-rw-r--r--share/extensions/synfig_output.inx20
-rwxr-xr-xshare/extensions/synfig_output.py1348
-rwxr-xr-xshare/extensions/synfig_prepare.py501
5 files changed, 2119 insertions, 0 deletions
diff --git a/share/extensions/Makefile.am b/share/extensions/Makefile.am
index a6815d932..e86852895 100644
--- a/share/extensions/Makefile.am
+++ b/share/extensions/Makefile.am
@@ -147,6 +147,9 @@ extensions = \
svgcalendar.py \
svg_regex.py \
svg_and_media_zip_output.py \
+ synfig_fileformat.py \
+ synfig_output.py \
+ synfig_prepare.py \
text_extract.py \
svg_transform.py \
text_uppercase.py \
@@ -323,6 +326,7 @@ modules = \
svg2xaml.inx \
svg_and_media_zip_output.inx \
svgcalendar.inx \
+ synfig_output.inx \
text_extract.inx \
text_uppercase.inx\
text_lowercase.inx \
diff --git a/share/extensions/synfig_fileformat.py b/share/extensions/synfig_fileformat.py
new file mode 100755
index 000000000..6c93e6b87
--- /dev/null
+++ b/share/extensions/synfig_fileformat.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python
+"""
+synfig_fileformat.py
+Synfig file format utilities
+
+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
+"""
+
+###### Constants ##########################################
+kux = 60.0 # Number of SVG units (pixels) per Synfig "unit"
+gamma = 2.2
+tangent_scale = 3.0 # Synfig tangents are scaled by a factor of 3
+
+
+
+###### Layer parameters, types, and default values ########
+layers = {}
+
+# Layer_Composite is the parent of most layers
+default_composite = {
+ "z_depth": ["real", 0.0],
+ "amount": ["real", 1.0],
+ "blend_method": ["integer", 0],
+ }
+
+layers["PasteCanvas"] = default_composite.copy()
+layers["PasteCanvas"].update({
+ "origin": ["vector", [0.0, 0.0]],
+ "canvas": ["canvas", None],
+ "zoom": ["real", 0.0],
+ "time_offset": ["time", "0s"],
+ "children_lock": ["bool", False],
+ "focus": ["vector", [0.0, 0.0]]
+ })
+
+
+## Layers in mod_geometry
+
+layers["circle"] = default_composite.copy()
+layers["circle"].update({
+ "color": ["color", [0,0,0,1]],
+ "radius": ["real", 1.0],
+ "feather": ["real", 0.0],
+ "origin": ["vector", [0.0, 0.0]],
+ "invert": ["bool", False],
+ "falloff": ["integer", 2]
+ })
+
+layers["rectangle"] = default_composite.copy()
+layers["rectangle"].update({
+ "color": ["color", [0,0,0,1]],
+ "point1": ["vector", [0,0]],
+ "point2": ["vector", [1,1]],
+ "expand": ["real", 0.0],
+ "invert": ["bool", False]
+ })
+
+default_shape = default_composite.copy()
+default_shape.update({
+ "color": ["color", [0,0,0,1]],
+ "origin": ["vector", [0.0, 0.0]],
+ "invert": ["bool", False],
+ "antialias": ["bool", True],
+ "feather": ["real", 0.0],
+ "blurtype": ["integer", 1],
+ "winding_style": ["integer", 0]
+ })
+
+layers["region"] = default_shape.copy()
+layers["region"].update({
+ "bline": ["bline", None]
+ })
+
+layers["outline"] = default_shape.copy()
+layers["outline"].update({
+ "bline": ["bline", None],
+ "round_tip[0]": ["bool", True],
+ "round_tip[1]": ["bool", True],
+ "sharp_cusps": ["bool", True],
+ "width": ["real", 1.0],
+ "loopyness": ["real", 1.0],
+ "expand": ["real", 0.0],
+ "homogeneous_width": ["bool", True]
+ })
+
+
+## Layers in mod_gradient
+
+layers["linear_gradient"] = default_composite.copy()
+layers["linear_gradient"].update({
+ "p1": ["vector", [0,0]],
+ "p2": ["vector", [1,1]],
+ "gradient": ["gradient", {0.0:[0,0,0,1], 1.0:[1,1,1,1]} ],
+ "loop": ["bool", False],
+ "zigzag": ["bool", False]
+ })
+
+layers["radial_gradient"] = default_composite.copy()
+layers["radial_gradient"].update({
+ "gradient": ["gradient", {0.0:[0,0,0,1], 1.0:[1,1,1,1]} ],
+ "center": ["vector", [0,0]],
+ "radius": ["real", 1.0],
+ "loop": ["bool", False],
+ "zigzag": ["bool", False]
+ })
+
+
+## Layers in lyr_std
+
+layers["import"] = default_composite.copy()
+layers["import"].update({
+ "tl": ["vector", [-1,1]],
+ "br": ["vector", [1,-1]],
+ "c": ["integer", 1],
+ "gamma_adjust": ["real", 1.0],
+ "filename": ["string", ""], # <string>foo</string>
+ "time_offset": ["time", "0s"]
+ })
+
+# transforms are not blending
+layers["warp"] = {
+ "src_tl": ["vector", [-1,1]],
+ "src_br": ["vector", [1,-1]],
+ "dest_tl": ["vector", [-1,1]],
+ "dest_tr": ["vector", [1,1]],
+ "dest_br": ["vector", [1,-1]],
+ "dest_bl": ["vector", [-1,-1]],
+ "clip": ["bool", False],
+ "horizon": ["real", 4.0]
+ }
+
+layers["rotate"] = {
+ "origin": ["vector", [0.0, 0.0]],
+ "amount": ["angle", 0] # <angle value=.../>
+ }
+
+layers["translate"] = {
+ "origin": ["vector", [0.0, 0.0]]
+ }
+
+## Layers in mod_filter
+layers["blur"] = default_composite.copy()
+layers["blur"].update({
+ "size": ["vector", [1,1]],
+ "type": ["integer", 3] # 1 is fast gaussian, 3 is regular
+ })
+
+
+###### Layer versions #####################################
+layer_versions = {
+ "outline" : "0.2",
+ "rectangle" : "0.2",
+ "linear_gradient" : "0.0",
+ "blur" : "0.2",
+ None : "0.1" # default
+ }
+
+###### Blend Methods ######################################
+blend_method_names = {
+ 0 : "composite",
+ 1 : "straight",
+ 13 : "onto",
+ 21 : "straight onto",
+ 12 : "behind",
+ 16 : "screen",
+ 20 : "overlay",
+ 17 : "hand light",
+ 6 : "multiply",
+ 7 : "divide",
+ 4 : "add",
+ 5 : "subtract",
+ 18 : "difference",
+ 2 : "brighten",
+ 3 : "darken",
+ 8 : "color",
+ 9 : "hue",
+ 10 : "saturation",
+ 11 : "luminance",
+ 14 : "alpha brighten", #deprecated
+ 15 : "alpha darken", #deprecated
+ 19 : "alpha over" #deprecated
+ }
+
+blend_methods = dict((v, k) for (k, v) in blend_method_names.iteritems())
+
+###### Functions ##########################################
+def paramType(layer, param, value=None):
+ if layer in layers.keys():
+ layer_params = layers[layer]
+ if param in layer_params.keys():
+ return layer_params[param][0]
+ else:
+ raise Exception, "Invalid parameter type for layer"
+ else:
+ # Unknown layer, try to determine parameter type based on value
+ if value is None:
+ raise Exception, "No information for given layer"
+ if type(value) == int:
+ return "integer"
+ elif type(value) == float:
+ return "real"
+ elif type(value) == bool:
+ return "bool"
+ elif type(value) == dict:
+ if "points" in value.keys():
+ return "bline"
+ elif 0.0 in value.keys():
+ return "gradient"
+ else:
+ raise Exception, "Could not automatically determine parameter type"
+ elif type(value) == list:
+ if len(value) == 2:
+ return "vector"
+ elif len(value) == 3 or len(value) == 4:
+ return "color"
+ else:
+ # The first two could also be canvases
+ return "canvas"
+ elif type(value) == str:
+ return "string"
+
+def defaultLayerVersion(layer):
+ if layer in layer_versions.keys():
+ return layer_versions[layer]
+ else:
+ return layer_versions[None]
+
+def defaultLayerParams(layer):
+ if layer in layers.keys():
+ return layers[layer].copy()
+ else:
+ return {}
diff --git a/share/extensions/synfig_output.inx b/share/extensions/synfig_output.inx
new file mode 100644
index 000000000..e947567b7
--- /dev/null
+++ b/share/extensions/synfig_output.inx
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <_name>Synfig Output</_name>
+ <id>org.inkscape.output.sif</id>
+ <dependency type="executable" location="extensions">synfig_fileformat.py</dependency>
+ <dependency type="executable" location="extensions">synfig_output.py</dependency>
+ <dependency type="executable" location="extensions">synfig_prepare.py</dependency>
+ <dependency type="executable" location="extensions">inkex.py</dependency>
+ <output>
+ <extension>.sif</extension>
+ <mimetype>image/sif</mimetype>
+ <_filetypename>Synfig Animation (*.sif)</_filetypename>
+ <_filetypetooltip>Synfig Animation written using the sif-file exporter extension</_filetypetooltip>
+ <dataloss>true</dataloss>
+ </output>
+ <script>
+ <command reldir="extensions"
+ interpreter="python">synfig_output.py</command>
+ </script>
+</inkscape-extension>
diff --git a/share/extensions/synfig_output.py b/share/extensions/synfig_output.py
new file mode 100755
index 000000000..bcd1eeaf3
--- /dev/null
+++ b/share/extensions/synfig_output.py
@@ -0,0 +1,1348 @@
+#!/usr/bin/env python
+"""
+synfig_output.py
+An Inkscape extension for exporting Synfig files (.sif)
+
+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
+"""
+import sys
+import math
+import uuid
+from copy import deepcopy
+
+import inkex
+from inkex import NSS, addNS, etree, errormsg
+import simplepath, simplestyle, simpletransform
+import cubicsuperpath
+
+from synfig_prepare import SynfigPrep, MalformedSVGError, get_dimension
+import synfig_fileformat as sif
+
+###### 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>%s</name>
+</canvas>
+""" % (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" % (
+ -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 and dy < 0:
+ ag = math.pi + math.atan(dy/dx)
+ elif dx < 0 and dy < 0:
+ ag = math.atan(dy/dx)
+ elif dx < 0 and dy > 0:
+ 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", "%ds" % value)
+ elif type(value) == float:
+ el.set("value", "%fs" % 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"] == True:
+ 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 %s" % (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*g["radius"]
+
+ # 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 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 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 %s not found" % 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 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 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]
+
+ simpletransform.applyTransformToPoint(mtx, dest_tl)
+ simpletransform.applyTransformToPoint(mtx, dest_tr)
+ simpletransform.applyTransformToPoint(mtx, dest_br)
+ simpletransform.applyTransformToPoint(mtx, 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 = simplepath.parsePath(path_d)
+
+ # 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, %s, %s" % (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 == 'L':
+ bline_list[-1]["points"].append([lastctrl[:], last[:], last[:], lastsplit])
+ 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./3*q0[0]+2./3*q1[0]
+ x2 = 2./3*q1[0]+1./3*q2[0]
+ x3 = q2[0]
+ y0 = q0[1]
+ y1 = 1./3*q0[1]+2./3*q1[1]
+ y2 = 2./3*q1[1]+1./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':
+ arcp = cubicsuperpath.ArcToPath(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 pt in vertex:
+ if type(pt) != bool:
+ simpletransform.applyTransformToPoint(mtx, pt)
+
+ return bline_list
+
+### Style related
+
+def extract_style(node, style_attrib="style"):
+ #return simplestyle.parseStyle(node.get("style"))
+
+ # Work around a simplestyle bug in older verions of Inkscape
+ # that leaves spaces at the beginning and end of values
+ s = node.get(style_attrib)
+ if s is None:
+ return {}
+ else:
+ return dict([[x.strip() for x in i.split(":")] for i in s.split(";") if len(i)])
+
+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 = simplestyle.parseColor(style[color_attrib])
+ 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] = 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 = 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.xpath("svg:title", namespaces=NSS)
+ if len(title) == 1:
+ name = title[0].text
+ else:
+ name = svg.get(addNS("docname", "sodipodi"), "Synfig Animation 1")
+
+ d = SynfigDocument(width, height, name)
+
+ layers = []
+ for node in svg.iterchildren():
+ layers += self.convert_node(node, d)
+
+ root_canvas = d.get_root_canvas()
+ for layer in layers:
+ root_canvas.append(layer)
+
+ d.get_root_tree().write(sys.stdout)
+
+ 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 node.tag == addNS("namedview", "sodipodi"):
+ return []
+ elif node.tag == addNS("defs", "svg"):
+ self.parse_defs(node, d)
+ return []
+ elif node.tag == addNS("metadata", "svg"):
+ return []
+ elif node.tag not in [
+ addNS("g", "svg"),
+ addNS("a", "svg"),
+ addNS("switch", "svg"),
+ addNS("path", "svg")]:
+ # An unsupported element
+ return []
+
+ layers = []
+ if node.tag == addNS("g", "svg"):
+ for subnode in node:
+ layers += self.convert_node(subnode, d)
+ if node.get(addNS("groupmode", "inkscape")) == "layer":
+ name = node.get(addNS("label", "inkscape"), "Inline Canvas")
+ layers = d.op_encapsulate(layers, name=name)
+
+ elif (node.tag == addNS("a", "svg")
+ or node.tag == addNS("switch", "svg")):
+ # Treat anchor and switch as a group
+ for subnode in node:
+ layers += self.convert_node(subnode, d)
+ elif node.tag == addNS("path", "svg"):
+ layers = self.convert_path(node, d)
+
+ style = extract_style(node)
+ 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 child.tag == addNS("linearGradient", "svg"):
+ self.parse_gradient(child, d)
+ elif child.tag == addNS("radialGradient", "svg"):
+ self.parse_gradient(child, d)
+ elif child.tag == addNS("filter", "svg"):
+ self.parse_filter(child, d)
+
+ def parse_gradient(self, node, d):
+ if node.tag == addNS("linearGradient", "svg"):
+ 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 = simpletransform.parseTransform(node.get("gradientTransform"))
+
+ link = node.get(addNS("href", "xlink"), "#")[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 == addNS("radialGradient", "svg"):
+ 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 = simpletransform.parseTransform(node.get("gradientTransform"))
+
+ link = node.get(addNS("href", "xlink"), "#")[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 == addNS("stop", "svg"):
+ offset = float(stop.get("offset"))
+ style = extract_style(stop)
+ 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, #default
+ "SourceGraphic" : layers }
+ 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 == addNS("feGaussianBlur", "svg"):
+ 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 == addNS("feBlend", "svg"):
+ # 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 = extract_style(node)
+ mtx = simpletransform.parseTransform(node.get("transform"))
+
+ blines = path_to_bline_list(node.get("d"), node.get(addNS("nodetypes", "sodipodi")), 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"]} )
+
+ return d.op_transform([layer], simpletransform.composeTransform(mtx, gradient["mtx"]))
+
+
+if __name__ == '__main__':
+ try:
+ e = SynfigExport()
+ e.affect(output=False)
+ except MalformedSVGError, e:
+ errormsg(e)
+
+# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99
diff --git a/share/extensions/synfig_prepare.py b/share/extensions/synfig_prepare.py
new file mode 100755
index 000000000..e06efde9b
--- /dev/null
+++ b/share/extensions/synfig_prepare.py
@@ -0,0 +1,501 @@
+#!/usr/bin/env python
+"""
+synfig_prepare.py
+Simplifies SVG files in preparation for sif export.
+
+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
+"""
+
+import os, tempfile
+
+import inkex
+from inkex import NSS, addNS, etree, errormsg
+import simplepath, simplestyle, simpletransform
+
+###### 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)
+
+try:
+ from subprocess import Popen, PIPE
+ bsubprocess = True
+except:
+ bsubprocess = False
+
+class InkscapeActionGroup(object):
+ """A class for calling Inkscape to perform operations on a document"""
+ def __init__(self, svg_document=None):
+ self.command = ""
+ self.init_args = ""
+ self.has_selection = False
+ self.has_action = False
+ self.svg_document = svg_document
+
+ def set_svg_document(self, svg_document):
+ """Set the SVG document that Inkscape will operate on"""
+ self.svg_document = svg_document
+
+ def set_init_args(self, cmd):
+ """Set the initial arguments to Inkscape subprocess
+
+ Can be used to pass additional arguments to Inkscape, or an initializer
+ command (e.g. unlock all objects before proceeding).
+ """
+ self.init_args = cmd
+
+ def clear(self):
+ """Clear all actions"""
+ self.command = ""
+ self.has_action = False
+ self.has_selection = False
+
+ def verb(self, verb):
+ """Run an Inkscape verb
+
+ For a list of verbs, run `inkscape --verb-list`
+ """
+ if self.has_selection:
+ self.command += "--verb=%s " % (verb)
+
+ if not self.has_action:
+ self.has_action = True
+
+ def select_id(self, object_id):
+ """Select object with given id"""
+ self.command += "--select='%s' " % (object_id)
+ if not self.has_selection:
+ self.has_selection = True
+
+ def select_node(self, node):
+ """Select the object represented by the SVG node
+
+ Selection will fail if node has no id attribute
+ """
+ node_id = node.get("id", None)
+ if node_id is None:
+ raise MalformedSVGError, "Node has no id"
+ self.select_id(node_id)
+
+ def select_nodes(self, nodes):
+ """Select objects represented by SVG nodes
+
+ Selection will fail if any node has no id attribute
+ """
+ for node in nodes:
+ self.select_node(node)
+
+ def select_xpath(self, xpath, namespaces=NSS):
+ """Select objects matching a given XPath expression
+
+ Selection will fail if any matching node has no id attribute
+ """
+ nodes = self.svg_document.xpath(xpath, namespaces=namespaces)
+
+ self.select_nodes(nodes)
+
+ def deselect(self):
+ """Deselect all objects"""
+ if self.has_selection:
+ self.verb("EditDeselect")
+ self.has_selection = False
+
+ def run_file(self, filename):
+ """Run the actions on a specific file"""
+ if not self.has_action:
+ return
+
+ cmd = self.init_args + " " + self.command + "--verb=FileSave --verb=FileQuit"
+ if bsubprocess:
+ p = Popen('inkscape "%s" %s' % (filename, cmd), shell=True, stdout=PIPE, stderr=PIPE)
+ rc = p.wait()
+ f = p.stdout
+ err = p.stderr
+ else:
+ _, f, err = os.popen3( "inkscape %s %s" % ( filename, cmd ) )
+
+ f.close()
+ err.close()
+
+ def run_document(self):
+ """Run the actions on the svg xml tree"""
+ if not self.has_action:
+ return self.svg_document
+
+ # First save the document
+ svgfile = tempfile.mktemp(".svg")
+ self.svg_document.write(svgfile)
+
+ # Run the action on the document
+ self.run_file(svgfile)
+
+ # Open the resulting file
+ stream = open(svgfile, 'r')
+ new_svg_doc = etree.parse(stream)
+ stream.close()
+
+ # Clean up.
+ try:
+ os.remove(svgfile)
+ except Exception:
+ pass
+
+ # Set the current SVG document
+ self.svg_document = new_svg_doc
+
+ # Return the new document
+ return new_svg_doc
+
+class SynfigExportActionGroup(InkscapeActionGroup):
+ """An action group with stock commands designed for Synfig exporting"""
+ def __init__(self, svg_document=None):
+ InkscapeActionGroup.__init__(self, svg_document)
+ self.set_init_args("--verb=UnlockAllInAllLayers")
+ self.objects_to_paths()
+ self.unlink_clones()
+
+ def objects_to_paths(self):
+ """Convert unsupported objects to paths"""
+ # Flow roots contain rectangles inside them, so they need to be
+ # converted to paths separately from other shapes
+ self.select_xpath("//svg:flowRoot", namespaces=NSS)
+ self.verb("ObjectToPath")
+ self.deselect()
+
+ non_paths = [
+ "svg:rect",
+ "svg:circle",
+ "svg:ellipse",
+ "svg:line",
+ "svg:polyline",
+ "svg:polygon",
+ "svg:text"
+ ]
+
+ # Build an xpath command to select these nodes
+ xpath_cmd = " | ".join(["//" + np for np in non_paths])
+
+ # Select all of these elements
+ # Note: already selected elements are not deselected
+ self.select_xpath(xpath_cmd, namespaces=NSS)
+
+ # Convert them to paths
+ self.verb("ObjectToPath")
+ self.deselect()
+
+ def unlink_clones(self):
+ """Unlink clones (remove <svg:use> elements)"""
+ self.select_xpath("//svg:use", namespaces=NSS)
+ self.verb("EditUnlinkClone")
+ self.deselect()
+
+###### Utility Functions ##################################
+
+### Path related
+
+def fuse_subpaths(path_node):
+ """Fuse subpaths of a path. Should only be used on unstroked paths"""
+ path_d = path_node.get("d", None)
+ path = simplepath.parsePath(path_d)
+
+ if len(path) == 0:
+ return
+
+ i = 0
+ initial_point = [ path[i][1][-2], path[i][1][-1] ]
+ return_stack = []
+ while i < len(path):
+ # Remove any terminators: they are redundant
+ if path[i][0] == "Z":
+ path.remove(["Z", []])
+ continue
+
+ # Skip all elements that do not begin a new path
+ if i == 0 or path[i][0] != "M":
+ i += 1
+ continue
+
+ # This element begins a new path - it should be a moveto
+ assert(path[i][0] == 'M')
+
+ # Swap it for a lineto
+ path[i][0] = 'L'
+
+ # If the old subpath has not been closed yet, close it
+ if path[i-1][1][-2] != initial_point[0] or path[i-1][1][-2] != initial_point[1]:
+ path.insert(i, ['L', initial_point])
+ i += 1
+
+ # Set the initial point of this subpath
+ initial_point = [ path[i-1][1][-2], path[i-1][1][-1] ]
+
+ # Append this point to the return stack
+ return_stack.append(initial_point)
+ #end while
+
+ # Now pop the entire return stack
+ while return_stack != []:
+ el = ['L', return_stack.pop()]
+ path.insert(i, el)
+ i += 1
+
+
+ path_d = simplepath.formatPath(path)
+ path_node.set("d", path_d)
+
+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 = simplestyle.parseStyle(path_node.get("style", ""))
+
+ # If there is only stroke or only fill, don't split anything
+ if "fill" in style.keys() and style["fill"] == "none":
+ if "stroke" not in style.keys() 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 = path_node.makeelement(addNS("g", "svg"))
+ fill = etree.SubElement(group, addNS("path", "svg"))
+ stroke = etree.SubElement(group, addNS("path", "svg"))
+
+ attribs = path_node.attrib
+
+ if "d" in attribs.keys():
+ d = attribs["d"]
+ del attribs["d"]
+ else:
+ raise AssertionError, "Cannot split stroke and fill of non-path element"
+
+ if addNS("nodetypes", "sodipodi") in attribs.keys():
+ nodetypes = attribs[addNS("nodetypes", "sodipodi")]
+ del attribs[addNS("nodetypes", "sodipodi")]
+ else:
+ nodetypes = None
+
+ if "id" in attribs.keys():
+ path_id = attribs["id"]
+ del attribs["id"]
+ else:
+ path_id = str(id(path_node))
+
+ if "style" in attribs.keys():
+ del attribs["style"]
+
+ if "transform" in attribs.keys():
+ transform = attribs["transform"]
+ del attribs["transform"]
+ else:
+ transform = None
+
+ # Pass along all remaining attributes to the group
+ for attrib_name in attribs.keys():
+ group.set(attrib_name, attribs[attrib_name])
+
+ 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", simplestyle.formatStyle(style_group))
+
+ fill.set("style", simplestyle.formatStyle(style_fill))
+ stroke.set("style", simplestyle.formatStyle(style_stroke))
+
+ # Finalize the two paths
+ fill.set("d", d)
+ stroke.set("d", d)
+ if nodetypes is not None:
+ fill.set(addNS("nodetypes", "sodipodi"), nodetypes)
+ stroke.set(addNS("nodetypes", "sodipodi"), 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 (node.tag == addNS("namedview", "sodipodi")
+ or node.tag == addNS("defs", "svg")
+ or node.tag == addNS("metadata", "svg")
+ or node.tag == addNS("foreignObject", "svg")):
+ return
+
+
+ # Compose the transformations
+ if node.tag == addNS("svg", "svg") and node.get("viewBox"):
+ vx, vy, vw, vh = [get_dimension(x) for x in node.get("viewBox").split()]
+ dw = get_dimension(node.get("width", vw))
+ dh = get_dimension(node.get("height", vh))
+ t = "translate(%f, %f) scale(%f, %f)" % (-vx, -vy, dw/vw, dh/vh)
+ this_transform = simpletransform.parseTransform(t, parent_transform)
+ this_transform = simpletransform.parseTransform(node.get("transform"), this_transform)
+ del node.attrib["viewBox"]
+ else:
+ this_transform = simpletransform.parseTransform(node.get("transform"), parent_transform)
+
+ # Compose the style attribs
+ this_style = simplestyle.parseStyle(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 (node.tag == addNS("svg", "svg")
+ or node.tag == addNS("g", "svg")
+ or node.tag == addNS("a", "svg")
+ or node.tag == addNS("switch", "svg")):
+ # Leave only non-propagating style attributes
+ if len(remaining_style) == 0:
+ if "style" in node.keys():
+ del node.attrib["style"]
+ else:
+ node.set("style", simplestyle.formatStyle(remaining_style))
+
+ # Remove the transform attribute
+ if "transform" in node.keys():
+ del node.attrib["transform"]
+
+ # Continue propagating on subelements
+ for c in node.iterchildren():
+ propagate_attribs(c, 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.set("style", simplestyle.formatStyle(this_style))
+ node.set("transform", simpletransform.formatTransform(this_transform))
+
+### Style related
+
+def get_dimension(s="1024"):
+ """Convert an SVG length string from arbitrary units to pixels"""
+ if s == "":
+ return 0
+ try:
+ last = int(s[-1])
+ except:
+ last = None
+
+ if type(last) == int:
+ return float(s)
+ elif s[-1] == "%":
+ return 1024
+ elif s[-2:] == "px":
+ return float(s[:-2])
+ elif s[-2:] == "pt":
+ return float(s[:-2])*1.25
+ elif s[-2:] == "em":
+ return float(s[:-2])*16
+ elif s[-2:] == "mm":
+ return float(s[:-2])*3.54
+ elif s[-2:] == "pc":
+ return float(s[:-2])*15
+ elif s[-2:] == "cm":
+ return float(s[:-2])*35.43
+ elif s[-2:] == "in":
+ return float(s[:-2])*90
+ else:
+ return 1024
+
+###### Main Class #########################################
+class SynfigPrep(inkex.Effect):
+ def effect(self):
+ """Transform document in preparation for exporting it into the Synfig format"""
+
+ a = SynfigExportActionGroup(self.document)
+ self.document = a.run_document()
+
+ # Remove inheritance of attributes
+ propagate_attribs(self.document.getroot())
+
+ # Fuse multiple subpaths in fills
+ for node in self.document.xpath('//svg:path', namespaces=NSS):
+ if node.get("d", "").lower().count("m") > 1:
+ # There are multiple subpaths
+ fill = split_fill_and_stroke(node)[0]
+ if fill is not None:
+ fuse_subpaths(fill)
+
+if __name__ == '__main__':
+ try:
+ e = SynfigPrep()
+ e.affect()
+ except MalformedSVGError, e:
+ errormsg(e)
+
+
+# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99