synfig_output.py revision 8fe37a993fbb0c0a84a7ab4a77cd83c87162c2da
"""
An Inkscape extension for exporting Synfig files (.sif)
Copyright (C) 2011 Nikita Kitaev
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
import inkex
import cubicsuperpath
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"""
"""
<canvas
version="0.5"
width="%f"
height="%f"
xres="2834.645752"
yres="2834.645752"
view-box="0 0 0 0"
>
<name>%s</name>
</canvas>
)
### Properties
def get_root_canvas(self):
return self.root_canvas
def get_root_tree(self):
def _update_viewbox(self):
"""Update the viewbox to match document width and height"""
attr_viewbox = "%f %f %f %f" % (
)
def get_height(self):
### Public utility functions
"""Generate a new GUID"""
### Coordinate system conversions
"""Convert distance from SVG to Synfig units"""
"""Convert distance from Synfig to SVG units"""
"""Convert SVG coordinate [x, y] to Synfig units"""
x = vector[0]
return [x, y]
"""Convert Synfig coordinate [x, y] to SVG units"""
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
return
# Otherwise recursively iterate over the list
for x in l:
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
return
# Otherwise recursively iterate over the list
for x in l:
def bline_coor_svg2sif(self, b):
"""Convert a BLine from SVG to Synfig coordinate units"""
def bline_coor_sif2svg(self, b):
"""Convert a BLine from Synfig to SVG coordinate units"""
### XML Builders -- private
### used to create XML elements in the Synfig document
"""Build an empty layer"""
if canvas is None:
else:
if active:
else:
if version == "auto":
return layer
"""Calculate radius of a tangent given two points"""
# Synfig tangents are scaled by a factor of 3
"""Calculate angle (in radians) of a tangent given two points"""
ag = 0
ag = 0
"""Add a parameter node to a layer"""
if layer is None:
else:
#Automatically detect param_type
if param_type == "auto":
if layer is not None:
else:
if param_type == "real":
elif param_type == "integer":
elif param_type == "vector":
elif param_type == "color":
elif param_type == "gradient":
# Value is a dictionary of color stops
# see get_gradient()
elif param_type == "bool":
if value:
else:
elif param_type == "time":
elif param_type == "bline":
# value is a bline (dictionary type), see path_to_bline_list
else:
if vertex[3]:
split = "true"
else:
split = "false"
elif param_type == "canvas":
# "value" is a list of layers
if value is not None:
else:
if guid:
else:
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
"""
else:
else:
param_guid = None
if param_value is not None:
return layer
"""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"
assert layer_type, "Layer does not have a type"
if param_type == "auto":
# Remove existing parameters with this name
existing = []
raise AssertionError, "Found multiple parameters with the same name"
else:
"""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)
"""
self.set_param(layer, param_name, params[param_name], guid=guids[param_name], modify_linked=modify_linked)
else:
"""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
"""
assert layer_type, "Layer does not have a type"
if param_type == "auto":
if param_type == "real":
elif param_type == "integer":
else:
raise Exception, "Getting this type of parameter not yet implemented"
### Global defs, and related
# SVG Filters
"""Register a filter"""
# 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 != []:
elif link != "":
else:
raise MalformedSVGError, "Gradient has neither stops nor link"
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 != []:
elif link != "":
else:
raise MalformedSVGError, "Gradient has neither stops nor link"
"""
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
}
"""
return None
# If the gradient has no link, we are done
return gradient
# If the gradient does have a link, find the color stops recursively
raise MalformedSVGError, "Linked gradient ID not found"
del gradient["link"]
# Update the gradient in our listing
# (so recursive lookup only happens once)
return gradient
"""Transform gradient to a list of parameters to pass to a Synfig layer"""
# Create a copy of the gradient
# 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"]:
if pos == 1.0:
else:
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":
if g["type"] == "radial":
# 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)
"""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
"""
"size" : [x, y]
})
if is_end:
else:
"""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
if is_end:
return ret
else:
"""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
return [layer]
"""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
# Otherwise, set their amount
return layers
"""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
"""
try:
return ret
except UnsupportedException:
# If the filter is not supported, ignore it.
return layers
"""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
return [layer]
"""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
return layers
} )
if is_end:
else:
###### Utility Functions ##################################
### Path related
"""
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
# Append (more than) enough c's to the nodetypes
if nodetypes is None:
nt = ""
else:
nt += "c"
# Create bline list
# borrows code from cubicsuperpath.py
# bline_list := [bline, bline, ...]
# bline := {
# "points":[vertex, vertex, ...],
# }
bline_list = []
subpathstart = []
last = []
lastctrl = []
for s in path:
elif cmd == "M":
# Add previous point to subpath
if last:
# Start a new subpath
# Save coordinates of this point
subpathstart = params[:]
elif cmd == 'L':
elif cmd == 'C':
elif cmd == 'Q':
elif cmd == 'A':
elif cmd == "Z":
# If the path "loops" after only one point
# e.g. "M 0 0 Z"
elif last == subpathstart:
# If we are back to the original position
# merge our tangent into the first point
else:
# Otherwise draw a line to the starting point
# Clear the variables (no more points need to be added)
last = []
lastctrl = []
# Loop the subpath
# Append final superpoint, if needed
if last:
# Apply the transformation
for bline in bline_list:
return bline_list
### Style related
#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
if s is None:
return {}
else:
return [1, 1, 1, 0]
else:
c = (0, 0, 0)
# Convert color scales and adjust gamma
for opacity in opacity_attribs:
return color
ret = 1.0
for opacity in opacity_attribs:
return ret
else:
width = 1
###### Main Class #########################################
class SynfigExport(SynfigPrep):
# Prepare the document for exporting
else:
layers = []
root_canvas = d.get_root_canvas()
"""Convert an SVG node to a list of Synfig layers"""
# Parse tags that don't draw any layers
return []
return []
return []
# An unsupported element
return []
layers = []
# Treat anchor and switch as a group
if opacity != 1.0:
return layers
if link == "":
d.add_linear_gradient(gradient_id, [x1, y1], [x2, y2], mtx, stops=stops, spread_method=spread_method)
else:
if link == "":
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)
stops = {}
else:
raise MalformedSVGError, "Child of gradient is not a stop"
return stops
# A filter is just like an operator (the op_* functions),
# except that it's created here
"SourceGraphic" : layers }
encapsulate_result = not is_end
# "SourceAlpha", "BackgroundImage",
# "BackgroundAlpha", "FillPaint", "StrokePaint"
# are not supported
raise UnsupportedException
l_out = []
else:
y = x
if x == 0 and y == 0:
else:
x = d.distance_svg2sif(x)
y = d.distance_svg2sif(y)
# Note: Blend methods are not an exact match
# because SVG uses alpha channel in places where
# Synfig does not
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"
raise UnsupportedException
else:
else:
# This filter element is currently unsupported
raise UnsupportedException
# Output the layers
# Set the default for the next filter element
# Return the output from the last element
return d.op_encapsulate(refs[None])
else:
return refs[None]
"""Convert an SVG path node to a list of Synfig layers"""
layers = []
bline_guid = d.new_guid()
# Set the color to black, so we can later overlay
# the shape with a gradient or pattern
else:
"bline": bline,
"color": color,
}, guids={
"bline":bline_guid
} )
# Set the color to black, so we can later overlay
# the shape with a gradient or pattern
else:
"bline": bline,
"color": color,
}, guids={
"bline":bline_guid
} )
return layers
"""Return a list Synfig layers that represent the gradient with the given id"""
if gradient is None:
# Patterns and other URLs not supported
return [None]
if __name__ == '__main__':
try:
e = SynfigExport()
except MalformedSVGError, e:
errormsg(e)
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99