From cca66b9ec4e494c1d919bff0f71a820d8afab1fa Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 20:24:48 +0200 Subject: Adding upstream version 1.2.2. Signed-off-by: Daniel Baumann --- share/extensions/synfig_output.py | 1485 +++++++++++++++++++++++++++++++++++++ 1 file changed, 1485 insertions(+) create mode 100755 share/extensions/synfig_output.py (limited to 'share/extensions/synfig_output.py') 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( + """ + + {} + + """.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() -- cgit v1.2.3