1
0
Fork 0
inkscape/share/extensions/synfig_output.py
Daniel Baumann 02d935e272
Adding upstream version 1.4.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 23:40:13 +02:00

1507 lines
49 KiB
Python
Executable file

#!/usr/bin/env python3
# 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,
OutputExtension,
)
import synfig_fileformat as sif
from synfig_prepare import *
# ##### Utility Classes ####################################
class UnsupportedException(Exception):
"""When part of an element is not supported, this exception is raised to invalidate the whole element"""
pass
class SynfigDocument(object):
"""A synfig document, with commands for adding layers and layer parameters"""
def __init__(self, width=1024, height=768, name="Synfig Animation 1"):
self.root_canvas = etree.fromstring(
"""
<canvas
version="0.5"
width="{:f}"
height="{:f}"
xres="2834.645752"
yres="2834.645752"
view-box="0 0 0 0"
>
<name>{}</name>
</canvas>
""".format(width, height, name)
)
self._update_viewbox()
self.gradients = {}
self.filters = {}
# ## Properties
def get_root_canvas(self):
return self.root_canvas
def get_root_tree(self):
return self.root_canvas.getroottree()
def _update_viewbox(self):
"""Update the viewbox to match document width and height"""
attr_viewbox = "{:f} {:f} {:f} {:f}".format(
-self.width / 2.0 / sif.kux,
self.height / 2.0 / sif.kux,
self.width / 2.0 / sif.kux,
-self.height / 2.0 / sif.kux,
)
self.root_canvas.set("view-box", attr_viewbox)
def get_width(self):
return float(self.root_canvas.get("width", "0"))
def set_width(self, value):
self.root_canvas.set("width", str(value))
self._update_viewbox()
def get_height(self):
return float(self.root_canvas.get("height", "0"))
def set_height(self, value):
self.root_canvas.set("height", str(value))
self._update_viewbox()
def get_name(self):
return self.root_canvas.get("name", "")
def set_name(self, value):
self.root_canvas.set("name", value)
self._update_viewbox()
width = property(get_width, set_width)
height = property(get_height, set_height)
name = property(get_name, set_name)
# ## Public utility functions
def new_guid(self):
"""Generate a new GUID"""
return uuid.uuid4().hex
# ## Coordinate system conversions
def distance_svg2sif(self, distance):
"""Convert distance from SVG to Synfig units"""
return distance / sif.kux
def distance_sif2svg(self, distance):
"""Convert distance from Synfig to SVG units"""
return distance * sif.kux
def coor_svg2sif(self, vector):
"""Convert SVG coordinate [x, y] to Synfig units"""
x = vector[0]
y = self.height - vector[1]
x -= self.width / 2.0
y -= self.height / 2.0
x /= sif.kux
y /= sif.kux
return [x, y]
def coor_sif2svg(self, vector):
"""Convert Synfig coordinate [x, y] to SVG units"""
x = vector[0] * sif.kux + self.width / 2.0
y = vector[1] * sif.kux + self.height / 2.0
y = self.height - y
assert (
self.coor_svg2sif([x, y]) == vector
), "sif to svg coordinate conversion error"
return [x, y]
def list_coor_svg2sif(self, l):
"""Scan a list for coordinate pairs and convert them to Synfig units"""
# If list has two numerical elements,
# treat it as a coordinate pair
if type(l) == list and len(l) == 2:
if type(l[0]) == int or type(l[0]) == float:
if type(l[1]) == int or type(l[1]) == float:
l_sif = self.coor_svg2sif(l)
l[0] = l_sif[0]
l[1] = l_sif[1]
return
# Otherwise recursively iterate over the list
for x in l:
if type(x) == list:
self.list_coor_svg2sif(x)
def list_coor_sif2svg(self, l):
"""Scan a list for coordinate pairs and convert them to SVG units"""
# If list has two numerical elements,
# treat it as a coordinate pair
if type(l) == list and len(l) == 2:
if type(l[0]) == int or type(l[0]) == float:
if type(l[1]) == int or type(l[1]) == float:
l_sif = self.coor_sif2svg(l)
l[0] = l_sif[0]
l[1] = l_sif[1]
return
# Otherwise recursively iterate over the list
for x in l:
if type(x) == list:
self.list_coor_sif2svg(x)
def bline_coor_svg2sif(self, b):
"""Convert a BLine from SVG to Synfig coordinate units"""
self.list_coor_svg2sif(b["points"])
def bline_coor_sif2svg(self, b):
"""Convert a BLine from Synfig to SVG coordinate units"""
self.list_coor_sif2svg(b["points"])
# ## XML Builders -- private
# ## used to create XML elements in the Synfig document
def build_layer(self, layer_type, desc, canvas=None, active=True, version="auto"):
"""Build an empty layer"""
if canvas is None:
layer = self.root_canvas.makeelement("layer")
else:
layer = etree.SubElement(canvas, "layer")
layer.set("type", layer_type)
layer.set("desc", desc)
if active:
layer.set("active", "true")
else:
layer.set("active", "false")
if version == "auto":
version = sif.defaultLayerVersion(layer_type)
if type(version) == float:
version = str(version)
layer.set("version", version)
return layer
def _calc_radius(self, p1x, p1y, p2x, p2y):
"""Calculate radius of a tangent given two points"""
# Synfig tangents are scaled by a factor of 3
return sif.tangent_scale * math.sqrt((p2x - p1x) ** 2 + (p2y - p1y) ** 2)
def _calc_angle(self, p1x, p1y, p2x, p2y):
"""Calculate angle (in radians) of a tangent given two points"""
dx = p2x - p1x
dy = p2y - p1y
if dx > 0 and dy > 0:
ag = math.pi + math.atan(dy / dx)
elif dx > 0 > dy:
ag = math.pi + math.atan(dy / dx)
elif dx < 0 and dy < 0:
ag = math.atan(dy / dx)
elif dx < 0 < dy:
ag = 2 * math.pi + math.atan(dy / dx)
elif dx == 0 and dy > 0:
ag = -1 * math.pi / 2
elif dx == 0 and dy < 0:
ag = math.pi / 2
elif dx == 0 and dy == 0:
ag = 0
elif dx < 0 and dy == 0:
ag = 0
elif dx > 0 and dy == 0:
ag = math.pi
return (ag * 180) / math.pi
def build_param(self, layer, name, value, param_type="auto", guid=None):
"""Add a parameter node to a layer"""
if layer is None:
param = self.root_canvas.makeelement("param")
else:
param = etree.SubElement(layer, "param")
param.set("name", name)
# Automatically detect param_type
if param_type == "auto":
if layer is not None:
layer_type = layer.get("type")
param_type = sif.paramType(layer_type, name)
else:
param_type = sif.paramType(None, name, value)
if param_type == "real":
el = etree.SubElement(param, "real")
el.set("value", str(float(value)))
elif param_type == "integer":
el = etree.SubElement(param, "integer")
el.set("value", str(int(value)))
elif param_type == "vector":
el = etree.SubElement(param, "vector")
x = etree.SubElement(el, "x")
x.text = str(float(value[0]))
y = etree.SubElement(el, "y")
y.text = str(float(value[1]))
elif param_type == "color":
el = etree.SubElement(param, "color")
r = etree.SubElement(el, "r")
r.text = str(float(value[0]))
g = etree.SubElement(el, "g")
g.text = str(float(value[1]))
b = etree.SubElement(el, "b")
b.text = str(float(value[2]))
a = etree.SubElement(el, "a")
a.text = str(float(value[3])) if len(value) > 3 else "1.0"
elif param_type == "gradient":
el = etree.SubElement(param, "gradient")
# Value is a dictionary of color stops
# see get_gradient()
for pos in value.keys():
color = etree.SubElement(el, "color")
color.set("pos", str(float(pos)))
c = value[pos]
r = etree.SubElement(color, "r")
r.text = str(float(c[0]))
g = etree.SubElement(color, "g")
g.text = str(float(c[1]))
b = etree.SubElement(color, "b")
b.text = str(float(c[2]))
a = etree.SubElement(color, "a")
a.text = str(float(c[3])) if len(c) > 3 else "1.0"
elif param_type == "bool":
el = etree.SubElement(param, "bool")
if value:
el.set("value", "true")
else:
el.set("value", "false")
elif param_type == "time":
el = etree.SubElement(param, "time")
if type(value) == int:
el.set("value", "{:d}s".format(value))
elif type(value) == float:
el.set("value", "{:f}s".format(value))
elif type(value) == str:
el.set("value", value)
elif param_type == "bline":
el = etree.SubElement(param, "bline")
el.set("type", "bline_point")
# value is a bline (dictionary type), see path_to_bline_list
if value["loop"]:
el.set("loop", "true")
else:
el.set("loop", "false")
for vertex in value["points"]:
x = float(vertex[1][0])
y = float(vertex[1][1])
tg1x = float(vertex[0][0])
tg1y = float(vertex[0][1])
tg2x = float(vertex[2][0])
tg2y = float(vertex[2][1])
tg1_radius = self._calc_radius(x, y, tg1x, tg1y)
tg1_angle = self._calc_angle(x, y, tg1x, tg1y)
tg2_radius = self._calc_radius(x, y, tg2x, tg2y)
tg2_angle = self._calc_angle(x, y, tg2x, tg2y) - 180.0
if vertex[3]:
split = "true"
else:
split = "false"
entry = etree.SubElement(el, "entry")
composite = etree.SubElement(entry, "composite")
composite.set("type", "bline_point")
point = etree.SubElement(composite, "point")
vector = etree.SubElement(point, "vector")
etree.SubElement(vector, "x").text = str(x)
etree.SubElement(vector, "y").text = str(y)
width = etree.SubElement(composite, "width")
etree.SubElement(width, "real").set("value", "1.0")
origin = etree.SubElement(composite, "origin")
etree.SubElement(origin, "real").set("value", "0.5")
split_el = etree.SubElement(composite, "split")
etree.SubElement(split_el, "bool").set("value", split)
t1 = etree.SubElement(composite, "t1")
t2 = etree.SubElement(composite, "t2")
t1_rc = etree.SubElement(t1, "radial_composite")
t1_rc.set("type", "vector")
t2_rc = etree.SubElement(t2, "radial_composite")
t2_rc.set("type", "vector")
t1_r = etree.SubElement(t1_rc, "radius")
t2_r = etree.SubElement(t2_rc, "radius")
t1_radius = etree.SubElement(t1_r, "real")
t2_radius = etree.SubElement(t2_r, "real")
t1_radius.set("value", str(tg1_radius))
t2_radius.set("value", str(tg2_radius))
t1_t = etree.SubElement(t1_rc, "theta")
t2_t = etree.SubElement(t2_rc, "theta")
t1_angle = etree.SubElement(t1_t, "angle")
t2_angle = etree.SubElement(t2_t, "angle")
t1_angle.set("value", str(tg1_angle))
t2_angle.set("value", str(tg2_angle))
elif param_type == "canvas":
el = etree.SubElement(param, "canvas")
el.set("xres", "10.0")
el.set("yres", "10.0")
# "value" is a list of layers
if value is not None:
for layer in value:
el.append(layer)
else:
raise AssertionError("Unsupported param type {}".format(param_type))
if guid:
el.set("guid", guid)
else:
el.set("guid", self.new_guid())
return param
# ## Public layer API
# ## Should be used by outside functions to create layers and set layer parameters
def create_layer(
self,
layer_type,
desc,
params={},
guids={},
canvas=None,
active=True,
version="auto",
):
"""Create a new layer
Keyword arguments:
layer_type -- layer type string used internally by Synfig
desc -- layer description
params -- a dictionary of parameter names and their values
guids -- a dictionary of parameter types and their guids (optional)
active -- set to False to create a hidden layer
"""
layer = self.build_layer(layer_type, desc, canvas, active, version)
default_layer_params = sif.defaultLayerParams(layer_type)
for param_name in default_layer_params.keys():
param_type = default_layer_params[param_name][0]
if param_name in params.keys():
param_value = params[param_name]
else:
param_value = default_layer_params[param_name][1]
if param_name in guids.keys():
param_guid = guids[param_name]
else:
param_guid = None
if param_value is not None:
self.build_param(
layer, param_name, param_value, param_type, guid=param_guid
)
return layer
def set_param(
self, layer, name, value, param_type="auto", guid=None, modify_linked=False
):
"""Set a layer parameter
Keyword arguments:
layer -- the layer to set the parameter for
name -- parameter name
value -- parameter value
param_type -- parameter type (default "auto")
guid -- guid of the parameter value
"""
if modify_linked:
raise AssertionError("Modifying linked parameters is not supported")
layer_type = layer.get("type")
assert layer_type, "Layer does not have a type"
if param_type == "auto":
param_type = sif.paramType(layer_type, name)
# Remove existing parameters with this name
existing = []
for param in layer.iterchildren():
if param.get("name") == name:
existing.append(param)
if len(existing) == 0:
self.build_param(layer, name, value, param_type, guid)
elif len(existing) > 1:
raise AssertionError("Found multiple parameters with the same name")
else:
new_param = self.build_param(None, name, value, param_type, guid)
layer.replace(existing[0], new_param)
def set_params(self, layer, params={}, guids={}, modify_linked=False):
"""Set layer parameters
Keyword arguments:
layer -- the layer to set the parameter for
params -- a dictionary of parameter names and their values
guids -- a dictionary of parameter types and their guids (optional)
"""
for param_name in params.keys():
if param_name in guids.keys():
self.set_param(
layer,
param_name,
params[param_name],
guid=guids[param_name],
modify_linked=modify_linked,
)
else:
self.set_param(
layer, param_name, params[param_name], modify_linked=modify_linked
)
def get_param(self, layer, name, param_type="auto"):
"""Get the value of a layer parameter
Keyword arguments:
layer -- the layer to get the parameter from
name -- param name
param_type -- parameter type (default "auto")
NOT FULLY IMPLEMENTED
"""
layer_type = layer.get("type")
assert layer_type, "Layer does not have a type"
if param_type == "auto":
param_type = sif.paramType(layer_type, name)
for param in layer.iterchildren():
if param.get("name") == name:
if param_type == "real":
return float(param[0].get("value", "0"))
elif param_type == "integer":
return int(param[0].get("integer", "0"))
else:
raise Exception(
"Getting this type of parameter not yet implemented"
)
# ## Global defs, and related
# SVG Filters
def add_filter(self, filter_id, f):
"""Register a filter"""
self.filters[filter_id] = f
# SVG Gradients
def add_linear_gradient(
self,
gradient_id,
p1,
p2,
mtx=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
stops=[],
link="",
spread_method="pad",
):
"""Register a linear gradient definition"""
gradient = {
"type": "linear",
"p1": p1,
"p2": p2,
"mtx": mtx,
"spreadMethod": spread_method,
}
if stops:
gradient["stops"] = stops
gradient["stops_guid"] = self.new_guid()
elif link != "":
gradient["link"] = link
else:
raise MalformedSVGError("Gradient has neither stops nor link")
self.gradients[gradient_id] = gradient
def add_radial_gradient(
self,
gradient_id,
center,
radius,
focus,
mtx=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
stops=[],
link="",
spread_method="pad",
):
"""Register a radial gradient definition"""
gradient = {
"type": "radial",
"center": center,
"radius": radius,
"focus": focus,
"mtx": mtx,
"spreadMethod": spread_method,
}
if stops:
gradient["stops"] = stops
gradient["stops_guid"] = self.new_guid()
elif link != "":
gradient["link"] = link
else:
raise MalformedSVGError("Gradient has neither stops nor link")
self.gradients[gradient_id] = gradient
def get_gradient(self, gradient_id):
"""
Return a gradient with a given id
Linear gradient format:
{
"type" : "linear",
"p1" : [x, y],
"p2" : [x, y],
"mtx" : mtx,
"stops" : color stops,
"stops_guid": color stops guid,
"spreadMethod": "pad", "reflect", or "repeat"
}
Radial gradient format:
{
"type" : "radial",
"center" : [x, y],
"radius" : r,
"focus" : [x, y],
"mtx" : mtx,
"stops" : color stops,
"stops_guid": color stops guid,
"spreadMethod": "pad", "reflect", or "repeat"
}
Color stops format
{
0.0 : color ([r,g,b,a] or [r,g,b]) at start,
[a number] : color at that position,
1.0 : color at end
}
"""
if gradient_id not in self.gradients.keys():
return None
gradient = self.gradients[gradient_id]
# If the gradient has no link, we are done
if "link" not in gradient.keys() or gradient["link"] == "":
return gradient
# If the gradient does have a link, find the color stops recursively
if gradient["link"] not in self.gradients.keys():
raise MalformedSVGError("Linked gradient ID not found")
linked_gradient = self.get_gradient(gradient["link"])
gradient["stops"] = linked_gradient["stops"]
gradient["stops_guid"] = linked_gradient["stops_guid"]
del gradient["link"]
# Update the gradient in our listing
# (so recursive lookup only happens once)
self.gradients[gradient_id] = gradient
return gradient
def gradient_to_params(self, gradient):
"""Transform gradient to a list of parameters to pass to a Synfig layer"""
# Create a copy of the gradient
g = gradient.copy()
# Set synfig-only attribs
if g["spreadMethod"] == "repeat":
g["loop"] = True
elif g["spreadMethod"] == "reflect":
g["loop"] = True
# Reflect the gradient
# Original: 0.0 [A . B . C] 1.0
# New: 0.0 [A . B . C . B . A] 1.0
# (with gradient size doubled)
new_stops = {}
# reflect the stops
for pos in g["stops"]:
val = g["stops"][pos]
if pos == 1.0:
new_stops[pos / 2.0] = val
else:
new_stops[pos / 2.0] = val
new_stops[1 - pos / 2.0] = val
g["stops"] = new_stops
# double the gradient size
if g["type"] == "linear":
g["p2"] = [
g["p1"][0] + 2.0 * (g["p2"][0] - g["p1"][0]),
g["p1"][1] + 2.0 * (g["p2"][1] - g["p1"][1]),
]
if g["type"] == "radial":
g["radius"] *= 2.0
# Rename "stops" to "gradient"
g["gradient"] = g["stops"]
# Convert coordinates
if g["type"] == "linear":
g["p1"] = self.coor_svg2sif(g["p1"])
g["p2"] = self.coor_svg2sif(g["p2"])
if g["type"] == "radial":
g["center"] = self.coor_svg2sif(g["center"])
g["radius"] = self.distance_svg2sif(g["radius"])
# Delete extra attribs
removed_attribs = [
"type",
"stops",
"stops_guid",
"mtx",
"focus",
"spreadMethod",
]
for x in removed_attribs:
if x in g.keys():
del g[x]
return g
# ## Public operations API
# Operations act on a series of layers, and (optionally) on a series of named parameters
# The "is_end" attribute should be set to true when the layers are at the end of a canvas
# (i.e. when adding transform layers on top of them does not require encapsulation)
def op_blur(self, layers, x, y, name="Blur", is_end=False):
"""Gaussian blur the given layers by the given x and y amounts
Keyword arguments:
layers -- list of layers
x -- x-amount of blur
y -- x-amount of blur
is_end -- set to True if layers are at the end of a canvas
Returns: list of layers
"""
blur = self.create_layer(
"blur",
name,
params={"blend_method": sif.blend_methods["straight"], "size": [x, y]},
)
if is_end:
return layers + [blur]
else:
return self.op_encapsulate(layers + [blur])
def op_color(self, layers, overlay, is_end=False):
"""Apply a color overlay to the given layers
Should be used to apply a gradient or pattern to a shape
Keyword arguments:
layers -- list of layers
overlay -- color layer to apply
is_end -- set to True if layers are at the end of a canvas
Returns: list of layers
"""
if not layers:
return layers
if overlay is None:
return layers
overlay_enc = self.op_encapsulate([overlay])
self.set_param(
overlay_enc[0], "blend_method", sif.blend_methods["straight onto"]
)
ret = layers + overlay_enc
if is_end:
return ret
else:
return self.op_encapsulate(ret)
def op_encapsulate(self, layers, name="Inline Canvas", is_end=False):
"""Encapsulate the given layers
Keyword arguments:
layers -- list of layers
name -- Name of the PasteCanvas layer that is created
is_end -- set to True if layers are at the end of a canvas
Returns: list of one layer
"""
if not layers:
return layers
layer = self.create_layer("PasteCanvas", name, params={"canvas": layers})
return [layer]
def op_fade(self, layers, opacity, is_end=False):
"""Increase the opacity of the given layers by a certain amount
Keyword arguments:
layers -- list of layers
opacity -- the opacity to apply (float between 0.0 to 1.0)
name -- name of the Transform layer that is added
is_end -- set to True if layers are at the end of a canvas
Returns: list of layers
"""
# If there is blending involved, first encapsulate the layers
for layer in layers:
if self.get_param(layer, "blend_method") != sif.blend_methods["composite"]:
return self.op_fade(self.op_encapsulate(layers), opacity, is_end)
# Otherwise, set their amount
for layer in layers:
amount = self.get_param(layer, "amount")
self.set_param(layer, "amount", amount * opacity)
return layers
def op_filter(self, layers, filter_id, is_end=False):
"""Apply a filter to the given layers
Keyword arguments:
layers -- list of layers
filter_id -- id of the filter
is_end -- set to True if layers are at the end of a canvas
Returns: list of layers
"""
if filter_id not in self.filters.keys():
raise MalformedSVGError("Filter {} not found".format(filter_id))
try:
ret = self.filters[filter_id](self, layers, is_end)
assert type(ret) == list
return ret
except UnsupportedException:
# If the filter is not supported, ignore it.
return layers
def op_set_blend(self, layers, blend_method, is_end=False):
"""Set the blend method of the given group of layers
If more than one layer is supplied, they will be encapsulated.
Keyword arguments:
layers -- list of layers
blend_method -- blend method to give the layers
is_end -- set to True if layers are at the end of a canvas
Returns: list of layers
"""
if not layers:
return layers
if blend_method == "composite":
return layers
layer = layers[0]
if len(layers) > 1 or self.get_param(layers[0], "amount") != 1.0:
layer = self.op_encapsulate(layers)[0]
layer = deepcopy(layer)
self.set_param(layer, "blend_method", sif.blend_methods[blend_method])
return [layer]
def op_transform(self, layers, mtx, name="Transform", is_end=False):
"""Apply a matrix transformation to the given layers
Keyword arguments:
layers -- list of layers
mtx -- transformation matrix
name -- name of the Transform layer that is added
is_end -- set to True if layers are at the end of a canvas
Returns: list of layers
"""
if not layers:
return layers
if mtx is None or mtx == [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]:
return layers
src_tl = [100, 100]
src_br = [200, 200]
dest_tl = [100, 100]
dest_tr = [200, 100]
dest_br = [200, 200]
dest_bl = [100, 200]
dest_tl = Transform(mtx).apply_to_point(dest_tl)
dest_tr = Transform(mtx).apply_to_point(dest_tr)
dest_br = Transform(mtx).apply_to_point(dest_br)
dest_bl = Transform(mtx).apply_to_point(dest_bl)
warp = self.create_layer(
"warp",
name,
params={
"src_tl": self.coor_svg2sif(src_tl),
"src_br": self.coor_svg2sif(src_br),
"dest_tl": self.coor_svg2sif(dest_tl),
"dest_tr": self.coor_svg2sif(dest_tr),
"dest_br": self.coor_svg2sif(dest_br),
"dest_bl": self.coor_svg2sif(dest_bl),
},
)
if is_end:
return layers + [warp]
else:
return self.op_encapsulate(layers + [warp])
# ##### Utility Functions ##################################
# ## Path related
def path_to_bline_list(path_d, nodetypes=None, mtx=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]):
"""
Convert a path to a BLine List
bline_list format:
Vertex:
[[tg1x, tg1y], [x,y], [tg2x, tg2y], split = T/F]
Vertex list:
[ vertex, vertex, vertex, ...]
Bline:
{
"points" : vertex_list,
"loop" : True / False
}
"""
# Exit on empty paths
if not path_d:
return []
# Parse the path
path = Path(path_d).to_arrays()
# Append (more than) enough c's to the nodetypes
if nodetypes is None:
nt = ""
else:
nt = nodetypes
for _ in range(len(path)):
nt += "c"
# Create bline list
# borrows code from cubicsuperpath.py
# bline_list := [bline, bline, ...]
# bline := {
# "points":[vertex, vertex, ...],
# "loop":True/False,
# }
bline_list = []
subpathstart = []
last = []
lastctrl = []
lastsplit = True
for s in path:
cmd, params = s
if cmd != "M" and bline_list == []:
raise MalformedSVGError(
"Bad path data: path doesn't start with moveto, {}, {}".format(s, path)
)
elif cmd == "M":
# Add previous point to subpath
if last:
bline_list[-1]["points"].append(
[lastctrl[:], last[:], last[:], lastsplit]
)
# Start a new subpath
bline_list.append({"nodetypes": "", "loop": False, "points": []})
# Save coordinates of this point
subpathstart = params[:]
last = params[:]
lastctrl = params[:]
lastsplit = False if nt[0] == "z" else True
nt = nt[1:]
elif cmd in "LHV":
bline_list[-1]["points"].append([lastctrl[:], last[:], last[:], lastsplit])
if cmd == "H":
last = [params[0], last[1]]
lastctrl = [params[0], last[1]]
elif cmd == "V":
last = [last[0], params[0]]
lastctrl = [last[0], params[0]]
else:
last = params[:]
lastctrl = params[:]
lastsplit = False if nt[0] == "z" else True
nt = nt[1:]
elif cmd == "C":
bline_list[-1]["points"].append(
[lastctrl[:], last[:], params[:2], lastsplit]
)
last = params[-2:]
lastctrl = params[2:4]
lastsplit = False if nt[0] == "z" else True
nt = nt[1:]
elif cmd == "Q":
q0 = last[:]
q1 = params[0:2]
q2 = params[2:4]
x0 = q0[0]
x1 = 1.0 / 3 * q0[0] + 2.0 / 3 * q1[0]
x2 = 2.0 / 3 * q1[0] + 1.0 / 3 * q2[0]
x3 = q2[0]
y0 = q0[1]
y1 = 1.0 / 3 * q0[1] + 2.0 / 3 * q1[1]
y2 = 2.0 / 3 * q1[1] + 1.0 / 3 * q2[1]
y3 = q2[1]
bline_list[-1]["points"].append(
[lastctrl[:], [x0, y0], [x1, y1], lastsplit]
)
last = [x3, y3]
lastctrl = [x2, y2]
lastsplit = False if nt[0] == "z" else True
nt = nt[1:]
elif cmd == "A":
from inkex.paths import arc_to_path
arcp = arc_to_path(last[:], params[:])
arcp[0][0] = lastctrl[:]
last = arcp[-1][1]
lastctrl = arcp[-1][0]
lastsplit = False if nt[0] == "z" else True
nt = nt[1:]
for el in arcp[:-1]:
el.append(True)
bline_list[-1]["points"].append(el)
elif cmd == "Z":
if len(bline_list[-1]["points"]) == 0:
# If the path "loops" after only one point
# e.g. "M 0 0 Z"
bline_list[-1]["points"].append([lastctrl[:], last[:], last[:], False])
elif last == subpathstart:
# If we are back to the original position
# merge our tangent into the first point
bline_list[-1]["points"][0][0] = lastctrl[:]
else:
# Otherwise draw a line to the starting point
bline_list[-1]["points"].append(
[lastctrl[:], last[:], last[:], lastsplit]
)
# Clear the variables (no more points need to be added)
last = []
lastctrl = []
lastsplit = True
# Loop the subpath
bline_list[-1]["loop"] = True
# Append final superpoint, if needed
if last:
bline_list[-1]["points"].append([lastctrl[:], last[:], last[:], lastsplit])
# Apply the transformation
if mtx != [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]:
for bline in bline_list:
for vertex in bline["points"]:
for point in vertex:
if not isinstance(point, bool):
pnt = Transform(mtx).apply_to_point(point)
point[0], point[1] = pnt[0], pnt[1]
return bline_list
# ## Style related
def extract_color(style, color_attrib, *opacity_attribs):
if color_attrib in style.keys():
if style[color_attrib] == "none":
return [1, 1, 1, 0]
c = 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(OutputExtension):
def preprocess(self):
"""Transform document in preparation for exporting it into the Synfig format"""
# Convert objects to path
super().preprocess()
# Remove inheritance of attributes
propagate_attribs(self.document.getroot())
# Fuse multiple subpaths in fills
for node in self.document.getroot().xpath("//svg:path"):
if node.get("d", "").lower().count("m") > 1:
# There are multiple subpaths
fill = split_fill_and_stroke(node)[0]
if fill is not None:
fill.path = fuse_subpaths(fill.path)
def effect(self):
# Prepare the document for exporting
self.preprocess()
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()