Initial Commit

This commit is contained in:
OmniLottie
2026-03-01 21:36:54 +08:00
commit a386c803e1
199 changed files with 42253 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
from . import builder, importer
from .importer import parse_sif_file
from .builder import to_sif
__all__ = ["builder", "importer", "parse_sif_file", "to_sif"]
Binary file not shown.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
from .sif.nodes import *
from . import ast
from .ast_impl.base import SifKeyframe, Interpolation
+2
View File
@@ -0,0 +1,2 @@
from .ast_impl.nodes import *
from .ast_impl.base import *
+120
View File
@@ -0,0 +1,120 @@
from xml.dom import minidom
import enum
from lottie.parsers.sif.sif.core import TypeDescriptor, ObjectRegistry, SifNodeMeta, FrameTime
from lottie.parsers.sif.xml.utils import xml_child_elements, xml_first_element_child
class SifAstNode:
_subclasses = None
_tag = None
@staticmethod
def from_dom(xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
if xml.tagName == param.typename:
return SifValue.from_dom(xml, param, registry)
xmltype = xml.getAttribute("type")
if xmltype != param.typename and xmltype != "weighted_" + param.typename:
raise ValueError("Invalid type %s (should be %s)" % (xmltype, param.typename))
return SifAstNode.ast_node_types()[xml.tagName].from_dom(xml, param, registry)
@staticmethod
def ast_node_types():
if SifAstNode._subclasses is None:
from . import nodes
SifAstNode._subclasses = {}
SifAstNode._gather_ast_types(SifAstNode)
return SifAstNode._subclasses
@staticmethod
def _gather_ast_types(cls):
for subcls in cls.__subclasses__():
if subcls._tag:
SifAstNode._subclasses[subcls._tag] = subcls
SifAstNode._gather_ast_types(subcls)
def _prepare_to_dom(self, dom: minidom.Document, param: TypeDescriptor):
element = dom.createElement(self._tag)
element.setAttribute("type", param.typename)
return element
class SifValue(SifAstNode):
def __init__(self, value=None):
self.value = value
def __repr__(self):
return "<%s %r>" % (self.__class__.__name__, self.value)
@classmethod
def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
return SifValue(param.value_from_xml_element(xml, registry))
def to_dom(self, dom: minidom.Document, param: TypeDescriptor):
return param.value_to_xml_element(self.value, dom)
class Interpolation(enum.Enum):
Auto = "auto"
Linear = "linear"
Clamped = "clamped"
Ease = "halt"
Constant = "constant"
class SifKeyframe:
def __init__(self, value, time: FrameTime, before=Interpolation.Clamped, after=Interpolation.Clamped):
self.value = value
self.time = time
self.before = before
self.after = after
@classmethod
def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
return cls(
param.value_from_xml_element(xml_first_element_child(xml), registry),
FrameTime.parse_string(xml.getAttribute("time"), registry),
Interpolation(xml.getAttribute("before")),
Interpolation(xml.getAttribute("after"))
)
def to_dom(self, dom: minidom.Document, param: TypeDescriptor):
element = dom.createElement("waypoint")
element.setAttribute("time", str(self.time))
element.setAttribute("before", self.before.value)
element.setAttribute("after", self.after.value)
element.appendChild(param.value_to_xml_element(self.value, dom))
return element
def __repr__(self):
return "<SifKeyframe %s %s>" % (self.time, self.value)
class SifAnimated(SifAstNode):
_tag = "animated"
def __init__(self):
self.keyframes = []
@classmethod
def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
obj = SifAnimated()
for waypoint in xml_child_elements(xml, "waypoint"):
obj.keyframes.append(SifKeyframe.from_dom(waypoint, param, registry))
return obj
def to_dom(self, dom: minidom.Document, param: TypeDescriptor):
element = self._prepare_to_dom(dom, param)
for kf in self.keyframes:
element.appendChild(kf.to_dom(dom, param))
return element
def add_keyframe(self, *args, **kwargs):
if not kwargs and len(args) == 1 and isinstance(args[0], SifKeyframe):
keyframe = args[0]
else:
keyframe = SifKeyframe(*args, **kwargs)
self.keyframes.append(keyframe)
return keyframe
+321
View File
@@ -0,0 +1,321 @@
from xml.dom import minidom
import enum
from lottie.nvector import NVector
from lottie.parsers.sif.ast_impl.base import SifAstNode, TypeDescriptor, ObjectRegistry
from lottie.parsers.sif.xml.animatable import XmlAnimatable
from lottie.parsers.sif.xml.wrappers import XmlBoneReference, XmlSifElement, XmlList
from lottie.parsers.sif.sif.nodes import Segment, WeightedVector, Bline
from lottie.parsers.sif.sif.enums import Smooth
from lottie.parsers.sif.sif.core import SifNodeMeta, FrameTime
class SifAstComplex(SifAstNode, metaclass=SifNodeMeta):
_nodes = []
def __init__(self, **kw):
for node in self._nodes:
node.initialize_object(kw, self)
@classmethod
def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
outcls = cls.get_class_from_dom(xml, param, registry)
instance = outcls()
for node in outcls._nodes:
node.from_xml(instance, xml, registry)
return instance
@classmethod
def get_class_from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
return cls
def to_dom(self, dom: minidom.Document, param: TypeDescriptor):
element = self._prepare_to_dom(dom, param)
for node in self._nodes:
node.to_xml(self, element, dom, param)
return element
class SifAstBoneLink(SifAstComplex):
_tag = "bone_link"
_nodes = [
XmlBoneReference("bone"),
XmlAnimatable("base_value", "vector", NVector(0, 0)),
XmlAnimatable("translate", "bool", True),
XmlAnimatable("rotate", "bool", True),
XmlAnimatable("skew", "bool", True),
XmlAnimatable("scale_x", "bool", True),
XmlAnimatable("scale_y", "bool", True),
]
class SifAstBoneInfluence(SifAstComplex):
_tag = "boneinfluence"
_nodes = [
# TODO bone_weight_list
XmlAnimatable("link", "vector", NVector(0, 0)),
]
class SifSegCalcTangent(SifAstComplex):
_tag = "segcalctangent"
_nodes = [
XmlSifElement("segment", Segment),
XmlAnimatable("amount", "real", .5),
]
class SifSegCalcVertex(SifAstComplex):
_tag = "segcalcvertex"
_nodes = [
XmlSifElement("segment", Segment),
XmlAnimatable("amount", "real", .5),
]
class WeightedAverage(SifAstComplex):
_tag = "weighted_average"
_nodes = [
XmlList(WeightedVector, "vectors", "entry"),
]
def _prepare_to_dom(self, dom: minidom.Document, param: TypeDescriptor):
element = dom.createElement(self._tag)
element.setAttribute("type", "weighted_vector")
return element
class SifAdd(SifAstComplex):
_tag = "add"
_nodes = [
XmlAnimatable("lhs", "_recurse"),
XmlAnimatable("rhs", "_recurse"),
XmlAnimatable("scalar", "real", 1.),
]
class SifAnimatedFile(SifAstComplex):
_tag = "animated_file"
_nodes = [
XmlAnimatable("filename", "string"),
]
class Accuracy(enum.Enum):
Rough = 0
Normal = 1
Fine = 2
Extreme = 3
class DerivativeOrder:
FirstDerivative = 0
SecondDerivative = 1
class SifDerivative(SifAstComplex):
_tag = "derivative"
_nodes = [
XmlAnimatable("link", "_recurse"),
XmlAnimatable("interval", "real", 0.01),
XmlAnimatable("accuracy", "integer", Accuracy.Normal, Accuracy),
XmlAnimatable("order", "integer", DerivativeOrder.FirstDerivative, DerivativeOrder),
]
class SifDynamic(SifAstComplex):
_tag = "dynamic"
_nodes = [
XmlAnimatable("tip_static", "vector", NVector(0, 0)),
XmlAnimatable("origin", "vector", NVector(0, 0)),
XmlAnimatable("force", "vector", NVector(0, 0)),
XmlAnimatable("torque", "real", 0.),
XmlAnimatable("damping", "real", 0.4),
XmlAnimatable("friction", "real", 0.4),
XmlAnimatable("spring", "real", 30.),
XmlAnimatable("torsion", "real", 30.),
XmlAnimatable("mass", "real", 0.3),
XmlAnimatable("inertia", "real", 0.3),
XmlAnimatable("spring_rigid", "bool", False),
XmlAnimatable("torsion_rigid", "bool", False),
XmlAnimatable("origin_drags_tip", "bool", True),
]
class SifGreyed(SifAstComplex):
_tag = "greyed"
_nodes = [
XmlAnimatable("link", "_recurse"),
]
class SifLinear(SifAstComplex):
_tag = "linear"
_nodes = [
XmlAnimatable("slope", "vector", NVector(0, 0)),
XmlAnimatable("offset", "vector", NVector(0, 0)),
]
class SifRadialComposite(SifAstComplex):
_tag = "radial_composite"
_nodes = [
XmlAnimatable("radius", "real", 0.),
XmlAnimatable("theta", "angle", 0.),
]
class SifComposite(SifAstComplex):
_tag = "composite"
@classmethod
def get_class_from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
type = xml.getAttribute("type")
if type == "vector":
return SifVectorComposite
return None
def _prepare_to_dom(self, dom: minidom.Document, param: TypeDescriptor):
element = dom.createElement("composite")
element.setAttribute("type", param.typename)
return element
class SifVectorComposite(SifComposite):
_type = "vector"
_nodes = [
XmlAnimatable("x", "real", 0.),
XmlAnimatable("y", "real", 0.),
]
class SifRandom(SifAstComplex):
_tag = "random"
_nodes = [
XmlAnimatable("link", "_recurse"),
XmlAnimatable("radius", "real", 0.),
XmlAnimatable("seed", "integer", 0),
XmlAnimatable("speed", "real", 1.),
XmlAnimatable("smooth", "integer", Smooth.Cubic, Smooth),
XmlAnimatable("loop", "real", 0.),
]
class SifReference(SifAstComplex):
_tag = "link"
_nodes = [
XmlAnimatable("reference", "_recurse"),
]
class SifScale(SifAstComplex):
_tag = "scale"
_nodes = [
XmlAnimatable("link", "_recurse"),
XmlAnimatable("scalar", "real", 1.),
]
class SifStep(SifAstComplex):
_tag = "step"
_nodes = [
XmlAnimatable("link", "_recurse"),
XmlAnimatable("duration", "time", FrameTime(1, FrameTime.Unit.Seconds)),
XmlAnimatable("start_time", "time", FrameTime(0, FrameTime.Unit.Seconds)),
XmlAnimatable("intersection", "real", 0.5),
]
class SifSubtract(SifAstComplex):
_tag = "subtract"
_nodes = [
XmlAnimatable("lhs", "_recurse"),
XmlAnimatable("rhs", "_recurse"),
XmlAnimatable("scalar", "real", 1.),
]
class SifSwitch(SifAstComplex):
_tag = "switch"
_nodes = [
XmlAnimatable("link_off", "_recurse"),
XmlAnimatable("link_on", "_recurse"),
XmlAnimatable("switch", "bool", False),
]
class SifTimedSwap(SifAstComplex):
_tag = "timed_swap"
_nodes = [
XmlAnimatable("before", "_recurse"),
XmlAnimatable("after", "_recurse"),
XmlAnimatable("time", "time", FrameTime(0, FrameTime.Unit.Seconds)),
XmlAnimatable("length", "time", FrameTime(0, FrameTime.Unit.Seconds)),
]
class SifTimeLoop(SifAstComplex):
_tag = "timeloop"
_nodes = [
XmlAnimatable("link", "_recurse"),
XmlAnimatable("link_time", "time", FrameTime(0, FrameTime.Unit.Seconds)),
XmlAnimatable("local_time", "time", FrameTime(0, FrameTime.Unit.Seconds)),
XmlAnimatable("duration", "time", FrameTime(0, FrameTime.Unit.Seconds)),
]
class SifPower(SifAstComplex):
_tag = "power"
_nodes = [
XmlAnimatable("base", "real", 1.),
XmlAnimatable("power", "real", 1.),
XmlAnimatable("epsilon", "real", 0.000001),
XmlAnimatable("infinite", "real", 999999.),
]
class SifBlineCalcTangent(SifAstComplex):
_tag = "blinecalctangent"
_nodes = [
XmlSifElement("bline", Bline),
XmlAnimatable("loop", "bool", False),
XmlAnimatable("amount", "real", 0.5),
XmlAnimatable("offset", "angle", 0.),
XmlAnimatable("scale", "real", 1.),
XmlAnimatable("fixed_length", "bool", False),
XmlAnimatable("homogeneous", "bool", False),
]
class SifBlineCalcVertex(SifAstComplex):
_tag = "blinecalcvertex"
_nodes = [
XmlSifElement("bline", Bline),
XmlAnimatable("loop", "bool", False),
XmlAnimatable("amount", "real", 0.5),
XmlAnimatable("homogeneous", "bool", False),
]
+411
View File
@@ -0,0 +1,411 @@
import math
from xml.dom import minidom
from ... import objects
from ...nvector import NVector
from ...utils import restructure
from . import api, ast
blend_modes = {
objects.BlendMode.Normal: api.BlendMethod.Composite,
objects.BlendMode.Multiply: api.BlendMethod.Multiply,
objects.BlendMode.Screen: api.BlendMethod.Screen,
objects.BlendMode.Overlay: api.BlendMethod.Overlay,
objects.BlendMode.Darken: api.BlendMethod.Darken,
objects.BlendMode.Lighten: api.BlendMethod.Lighten,
objects.BlendMode.HardLight: api.BlendMethod.HardLight,
objects.BlendMode.Difference: api.BlendMethod.Difference,
objects.BlendMode.Hue: api.BlendMethod.Hue,
objects.BlendMode.Saturation: api.BlendMethod.Saturation,
objects.BlendMode.Color: api.BlendMethod.Color,
objects.BlendMode.Luminosity: api.BlendMethod.Luminosity,
objects.BlendMode.Exclusion: api.BlendMethod.Difference,
objects.BlendMode.SoftLight: api.BlendMethod.Multiply,
objects.BlendMode.ColorDodge: api.BlendMethod.Composite,
objects.BlendMode.ColorBurn: api.BlendMethod.Composite,
}
class SifBuilder(restructure.AbstractBuilder):
def __init__(self, gamma=1.0):
"""
@todo Add gamma option to lottie_convert.py
"""
super().__init__()
self.canvas = api.Canvas()
self.canvas.version = "1.2"
self.canvas.gamma_r = self.canvas.gamma_g = self.canvas.gamma_b = gamma
self.autoid = objects.base.Index()
def _on_animation(self, animation: objects.Animation):
if animation.name:
self.canvas.name = animation.name
self.canvas.width = animation.width
self.canvas.height = animation.height
self.canvas.xres = animation.width
self.canvas.yres = animation.height
self.canvas.view_box = NVector(0, 0, animation.width, animation.height)
self.canvas.fps = animation.frame_rate
self.canvas.begin_time = api.FrameTime.frame(animation.in_point)
self.canvas.end_time = api.FrameTime.frame(animation.out_point)
self.canvas.antialias = True
return self.canvas
def _on_precomp(self, id, dom_parent, layers):
g = dom_parent.add_layer(api.GroupLayer())
g.desc = id
for layer_builder in layers:
self.process_layer(layer_builder, g)
def _on_layer(self, layer_builder, dom_parent):
layer = self.layer_from_lottie(api.GroupLayer, layer_builder.lottie, dom_parent)
if not layer_builder.lottie.name:
layer.desc = layer_builder.lottie.__class__.__name__
bm = getattr(layer_builder.lottie, "blend_mode", None)
if bm is None:
bm = objects.BlendMode.Normal
layer.blend_method = blend_modes[bm]
layer.time_drilation = getattr(layer_builder.lottie, "stretch", 1) or 1
in_point = getattr(layer_builder.lottie, "in_point", 0)
layer.time_offset.value = api.FrameTime.frame(in_point)
#layer.canvas.end_time = api.FrameTime.frame(out_point)
return layer
def layer_from_lottie(self, type, lottie, dom_parent):
g = dom_parent.add_layer(type())
if lottie.name:
g.desc = lottie.name
g.active = not lottie.hidden
transf = getattr(lottie, "transform", None)
if transf:
self.set_transform(g, transf)
if isinstance(lottie, objects.NullLayer):
g.amount.value = 1
return g
def _get_scale(self, transform):
def func(keyframe):
t = keyframe.time if keyframe else 0
scale_x, scale_y = transform.scale.get_value(t)[:2]
scale_x /= 100
scale_y /= 100
skew = transform.skew.get_value(t) if transform.skew else 0
c = math.cos(skew * math.pi / 180)
if c != 0:
scale_y *= 1 / c
return NVector(scale_x, scale_y)
return func
def set_transform(self, group, transform):
composite = group.transformation
if transform.position:
composite.offset = self.process_vector(transform.position)
if transform.scale:
keyframes = self._merge_keyframes([transform.scale, transform.skew])
composite.scale = self.process_vector_ext(keyframes, self._get_scale(transform))
composite.skew_angle = self.process_scalar(transform.skew or objects.Value(0))
if transform.rotation:
composite.angle = self.process_scalar(transform.rotation)
if transform.opacity:
group.amount = self.process_scalar(transform.opacity, 1/100)
if transform.anchor_point:
group.origin = self.process_vector(transform.anchor_point)
# TODO get z_depth from position
composite.z_depth = 0
def process_vector(self, multidim):
def getter(keyframe):
if keyframe is None:
v = multidim.value
else:
v = keyframe.start
return NVector(v[0], v[1])
return self.process_vector_ext(multidim.keyframes, getter)
def process_vector_ext(self, kframes, getter):
if kframes is not None:
wrap = ast.SifAnimated()
for i in range(len(kframes)):
keyframe = kframes[i]
waypoint = wrap.add_keyframe(getter(keyframe), api.FrameTime.frame(keyframe.time))
if i > 0:
prev = kframes[i-1]
if prev.jump:
waypoint.before = api.Interpolation.Constant
elif prev.in_value and prev.in_value.x < 1:
waypoint.before = api.Interpolation.Ease
else:
waypoint.before = api.Interpolation.Linear
else:
waypoint.before = api.Interpolation.Linear
if keyframe.jump:
waypoint.after = api.Interpolation.Constant
elif keyframe.out_value and keyframe.out_value.x > 0:
waypoint.after = api.Interpolation.Ease
else:
waypoint.after = api.Interpolation.Linear
else:
wrap = api.SifValue(getter(None))
return wrap
def process_scalar(self, value, mult=None):
def getter(keyframe):
if keyframe is None:
v = value.value
else:
v = keyframe.start[0]
if mult is not None:
v *= mult
return v
return self.process_vector_ext(value.keyframes, getter)
def _on_shape(self, shape, group, dom_parent):
layers = []
if not hasattr(shape, "to_bezier"):
return []
if group.stroke:
sif_shape = self.build_path(api.OutlineLayer, shape.to_bezier(), dom_parent, shape)
self.apply_group_stroke(sif_shape, group.stroke)
layers.append(sif_shape)
if group.fill:
sif_shape = self.build_path(api.RegionLayer, shape.to_bezier(), dom_parent, shape)
layers.append(sif_shape)
self.apply_group_fill(sif_shape, group.fill)
return layers
def _merge_keyframes(self, props):
keyframes = {}
for prop in props:
if prop is not None and prop.animated:
keyframes.update({kf.time: kf for kf in prop.keyframes})
return list(sorted(keyframes.values(), key=lambda kf: kf.time)) or None
def apply_origin(self, sif_shape, lottie_shape):
if hasattr(lottie_shape, "position"):
sif_shape.origin.value = lottie_shape.position.get_value()
else:
sif_shape.origin.value = lottie_shape.bounding_box().center()
def apply_group_fill(self, sif_shape, fill):
## @todo gradients?
if hasattr(fill, "colors"):
return
def getter(keyframe):
if keyframe is None:
v = fill.color.value
else:
v = keyframe.start
return self.canvas.make_color(*v)
sif_shape.color = self.process_vector_ext(fill.color.keyframes, getter)
def get_op(keyframe):
if keyframe is None:
v = fill.opacity.value
else:
v = keyframe.start[0]
v /= 100
return v
sif_shape.amount = self.process_vector_ext(fill.opacity.keyframes, get_op)
def apply_group_stroke(self, sif_shape, stroke):
self.apply_group_fill(sif_shape, stroke)
sif_shape.sharp_cusps.value = stroke.line_join == objects.LineJoin.Miter
round_cap = stroke.line_cap == objects.LineCap.Round
sif_shape.round_tip_0.value = round_cap
sif_shape.round_tip_1.value = round_cap
sif_shape.width = self.process_scalar(stroke.width, 0.5)
def build_path(self, type, path, dom_parent, lottie_shape):
layer = self.layer_from_lottie(type, lottie_shape, dom_parent)
self.apply_origin(layer, lottie_shape)
startbez = path.shape.get_value()
layer.bline.loop = startbez.closed
nverts = len(startbez.vertices)
for point in range(nverts):
self.bezier_point(path, point, layer.bline, layer.origin.value)
return layer
def bezier_point(self, lottie_path, point_index, sif_parent, offset):
composite = api.BlinePoint()
def get_point(keyframe):
if keyframe is None:
bezier = lottie_path.shape.value
else:
bezier = keyframe.start
if not bezier:
#elem.parentNode.parentNode.removeChild(elem.parentNode)
return
vert = bezier.vertices[point_index]
return NVector(vert[0], vert[1]) - offset
composite.point = self.process_vector_ext(lottie_path.shape.keyframes, get_point)
composite.split.value = True
composite.split_radius.value = True
composite.split_angle.value = True
def get_tangent(keyframe):
if keyframe is None:
bezier = lottie_path.shape.value
else:
bezier = keyframe.start
if not bezier:
#elem.parentNode.parentNode.removeChild(elem.parentNode)
return
inp = getattr(bezier, which_point)[point_index]
return NVector(inp.x, inp.y) * 3 * mult
mult = -1
which_point = "in_tangents"
composite.t1 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
mult = 1
which_point = "out_tangents"
composite.t2 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
sif_parent.points.append(composite)
def _on_shapegroup(self, shape_group, dom_parent):
if shape_group.empty():
return
layer = self.layer_from_lottie(api.GroupLayer, shape_group.lottie, dom_parent)
self.shapegroup_process_children(shape_group, layer)
def _modifier_inner_group(self, modifier, shapegroup, dom_parent):
layer = dom_parent.add_layer(api.GroupLayer())
self.shapegroup_process_child(modifier.child, shapegroup, layer)
return layer
def _on_shape_modifier(self, modifier, shapegroup, dom_parent):
layer = dom_parent.add_layer(api.GroupLayer())
if modifier.lottie.name:
layer.desc = modifier.lottie.name
inner = self._modifier_inner_group(modifier, shapegroup, layer)
if isinstance(modifier.lottie, objects.Repeater):
self.build_repeater(modifier.lottie, inner, layer)
def _build_repeater_defs(self, shape, name_id):
dup = api.Duplicate()
dup.id = name_id
self.canvas.defs.append(dup)
self.canvas.register_as(dup, name_id)
def getter(keyframe):
if keyframe is None:
v = shape.copies.value
else:
v = keyframe.start[0]
return v - 1
setattr(dup, "from", self.process_vector_ext(shape.copies.keyframes, getter))
dup.to.value = 0
dup.step.value = -1
return dup
def _build_repeater_transform_scale_component(self, shape, name_id, comp, scalecomposite):
power = ast.SifPower()
setattr(scalecomposite, "xy"[comp], power)
def getter(keyframe):
if keyframe is None:
v = shape.transform.scale.value
else:
v = keyframe.start
v = v[comp] / 100
return v
power.base = self.process_vector_ext(shape.transform.scale.keyframes, getter)
# HACK work around an issue in Synfig
power.power = ast.SifAdd()
power.power.lhs.value = api.ValueReference(name_id)
power.power.rhs.value = 0.000001
def _build_repeater_transform(self, shape, inner, name_id):
offset_id = name_id + "_origin"
origin = api.ExportedValue(offset_id, self.process_vector(shape.transform.anchor_point), "vector")
self.canvas.defs.append(origin)
self.canvas.register_as(origin, offset_id)
inner.origin = origin
composite = inner.transformation
composite.offset = ast.SifAdd()
composite.offset.rhs.value = api.ValueReference(offset_id)
composite.offset.lhs = ast.SifScale()
composite.offset.lhs.scalar.value = api.ValueReference(name_id)
composite.offset.lhs.link = self.process_vector(shape.transform.position)
composite.angle = ast.SifScale()
composite.angle.scalar.value = api.ValueReference(name_id)
composite.angle.link = self.process_scalar(shape.transform.rotation)
composite.scale = ast.SifVectorComposite()
self._build_repeater_transform_scale_component(shape, name_id, 0, composite.scale)
self._build_repeater_transform_scale_component(shape, name_id, 1, composite.scale)
def _build_repeater_amount(self, shape, inner, name_id):
inner.amount = ast.SifSubtract()
inner.amount.lhs = self.process_scalar(shape.transform.start_opacity, 0.01)
inner.amount.rhs = ast.SifScale()
inner.amount.rhs.scalar.value = api.ValueReference(name_id)
def getter(keyframe):
if keyframe is None:
t = 0
end = shape.transform.end_opacity.value
else:
t = keyframe.time
end = keyframe.start[0]
start = shape.transform.start_opacity.get_value(t)
n = shape.copies.get_value(t)
v = (start - end) / (n - 1) / 100 if n > 0 else 0
return v
inner.amount.rhs.link = self.process_vector_ext(shape.transform.end_opacity.keyframes, getter)
def build_repeater(self, shape, inner, dom_parent):
name_id = "duplicate_%s" % next(self.autoid)
dup = self._build_repeater_defs(shape, name_id)
self._build_repeater_transform(shape, inner, name_id)
self._build_repeater_amount(shape, inner, name_id)
inner.desc = "Transformation for " + (dom_parent.desc or "duplicate")
# duplicate layer
duplicate = dom_parent.add_layer(api.DuplicateLayer())
duplicate.index = dup
duplicate.desc = shape.name
def to_sif(animation):
builder = SifBuilder()
builder.process(animation)
return builder.canvas
+528
View File
@@ -0,0 +1,528 @@
import math
from ... import objects
from ...objects import easing
from . import api, ast
from ... import NVector, PolarVector
try:
from ...utils import font
has_font = True
except ImportError:
has_font = False
def convert(canvas: api.Canvas):
return Converter().convert(canvas)
class Converter:
def __init__(self):
pass
def _animated(self, sifval):
return isinstance(sifval, ast.SifAnimated)
def convert(self, canvas: api.Canvas):
self.canvas = canvas
self.animation = objects.Animation(
self._time(canvas.end_time),
canvas.fps
)
self.animation.in_point = self._time(canvas.begin_time)
self.animation.width = canvas.width
self.animation.height = canvas.height
self.view_p1 = NVector(canvas.view_box[0], canvas.view_box[1])
self.view_p2 = NVector(canvas.view_box[2], canvas.view_box[3])
self.target_size = NVector(canvas.width, canvas.height)
self.shape_layer = self.animation.add_layer(objects.ShapeLayer())
self.gamma = NVector(canvas.gamma_r, canvas.gamma_g, canvas.gamma_b)
self._process_layers(canvas.layers, self.shape_layer)
return self.animation
def _time(self, t: api.FrameTime):
return self.canvas.time_to_frames(t)
def _process_layers(self, layers, parent):
old_gamma = self.gamma
for layer in reversed(layers):
if not layer.active:
continue
elif isinstance(layer, api.GroupLayerBase):
parent.add_shape(self._convert_group(layer))
elif isinstance(layer, api.RectangleLayer):
parent.add_shape(self._convert_fill(layer, self._convert_rect))
elif isinstance(layer, api.CircleLayer):
parent.add_shape(self._convert_fill(layer, self._convert_circle))
elif isinstance(layer, api.StarLayer):
parent.add_shape(self._convert_fill(layer, self._convert_star))
elif isinstance(layer, api.PolygonLayer):
parent.add_shape(self._convert_fill(layer, self._convert_polygon))
elif isinstance(layer, api.RegionLayer):
parent.add_shape(self._convert_fill(layer, self._convert_bline))
elif isinstance(layer, api.AbstractOutline):
parent.add_shape(self._convert_outline(layer, self._convert_bline))
elif isinstance(layer, api.GradientLayer):
parent.add_shape(self._convert_gradient(layer, parent))
elif isinstance(layer, api.TransformDown):
shape = self._convert_transform_down(layer)
parent.add_shape(shape)
parent = shape
elif isinstance(layer, api.TextLayer):
if has_font:
parent.add_shape(self._convert_fill(layer, self._convert_text))
elif isinstance(layer, api.ColorCorrectLayer):
self.gamma = self.gamma * NVector(layer.gamma.value, layer.gamma.value, layer.gamma.value)
self.gamma = old_gamma
def _convert_group(self, layer: api.GroupLayer):
shape = objects.Group()
self._set_name(shape, layer)
shape.transform.anchor_point = self._adjust_coords(self._convert_vector(layer.origin))
self._convert_transform(layer.transformation, shape.transform)
self._process_layers(layer.layers, shape)
shape.transform.opacity = self._adjust_animated(
self._convert_scalar(layer.amount),
lambda x: x*100
)
return shape
def _convert_transform(self, sif_transform: api.AbstractTransform, lottie_transform: objects.Transform):
if isinstance(sif_transform, api.BoneLinkTransform):
base_transform = sif_transform.base_value
else:
base_transform = sif_transform
position = self._adjust_coords(self._convert_vector(base_transform.offset))
rotation = self._adjust_angle(self._convert_scalar(base_transform.angle))
scale = self._adjust_animated(
self._convert_vector(base_transform.scale),
lambda x: x * 100
)
lottie_transform.skew_axis = self._adjust_angle(self._convert_scalar(base_transform.skew_angle))
if isinstance(sif_transform, api.BoneLinkTransform):
lottie_transform.position = position
lottie_transform.rotation = rotation
lottie_transform.scale = scale
#bone = sif_transform.bone
#b_pos = self._adjust_coords(self._convert_vector(bone.origin))
#old_anchor = lottie_transform.anchor_point
#if sif_transform.translate:
#self._mix_animations_into(
#[position, b_pos, old_anchor],
#lottie_transform.position,
#lambda base_p, bone_p, anchor: (anchor-self.target_size/2)/2+self.target_size/2
#)
#else:
#lottie_transform.position = position
#lottie_transform.anchor_point = b_pos
#lottie_transform.anchor_point.value += NVector(100,0)
#if sif_transform.rotate:
#b_rot = self._convert_scalar(bone.angle)
#self._mix_animations_into([rotation, b_rot], lottie_transform.rotation, lambda a, b: a-b)
#else:
#lottie_transform.rotation = rotation
#if sif_transform.scale_y:
#b_scale = self._convert_scalar(bone.scalelx)
#self._mix_animations_into(
#scale, b_scale, lottie_transform.scale,
#lambda a, b: NVector(a.x, a.y * b)
#)
#else:
#lottie_transform.scale = scale
else:
lottie_transform.position = position
lottie_transform.rotation = rotation
lottie_transform.scale = scale
def _mix_animations_into(self, animations, output, mix):
if not any(x.animated for x in animations):
output.value = mix(*(x.value for x in animations))
else:
for vals in self._mix_animations(*animations):
time = vals.pop(0)
output.add_keyframe(time, mix(*vals))
def _convert_fill(self, layer, converter):
shape = objects.Group()
self._set_name(shape, layer)
shape.add_shape(converter(layer))
if layer.invert.value:
shape.add_shape(objects.Rect(self.target_size/2, self.target_size))
fill = objects.Fill()
fill.color = self._convert_color(layer.color)
fill.opacity = self._adjust_animated(
self._convert_scalar(layer.amount),
lambda x: x * 100
)
shape.add_shape(fill)
return shape
def _convert_linecap(self, lc: api.LineCap):
if lc == api.LineCap.Rounded:
return objects.LineCap.Round
if lc == api.LineCap.Squared:
return objects.LineCap.Square
return objects.LineCap.Butt
def _convert_cusp(self, lc: api.CuspStyle):
if lc == api.CuspStyle.Miter:
return objects.LineJoin.Miter
if lc == api.CuspStyle.Bevel:
return objects.LineJoin.Bevel
return objects.LineJoin.Round
def _convert_outline(self, layer: api.AbstractOutline, converter):
shape = objects.Group()
self._set_name(shape, layer)
shape.add_shape(converter(layer))
stroke = objects.Stroke()
stroke.color = self._convert_color(layer.color)
stroke.line_cap = self._convert_linecap(layer.start_tip)
stroke.line_join = self._convert_cusp(layer.cusp_type)
stroke.width = self._adjust_scalar(self._convert_scalar(layer.width))
shape.add_shape(stroke)
return shape
def _convert_rect(self, layer: api.RectangleLayer):
rect = objects.Rect()
p1 = self._adjust_coords(self._convert_vector(layer.point1))
p2 = self._adjust_coords(self._convert_vector(layer.point2))
if p1.animated or p2.animated:
for time, p1v, p2v in self._mix_animations(p1, p2):
rect.position.add_keyframe(time, (p1v + p2v) / 2)
rect.size.add_keyframe(time, abs(p2v - p1v))
pass
else:
rect.position.value = (p1.value + p2.value) / 2
rect.size.value = abs(p2.value - p1.value)
rect.rounded = self._adjust_scalar(self._convert_scalar(layer.bevel))
return rect
def _convert_circle(self, layer: api.CircleLayer):
shape = objects.Ellipse()
shape.position = self._adjust_coords(self._convert_vector(layer.origin))
radius = self._adjust_scalar(self._convert_scalar(layer.radius))
shape.size = self._adjust_add_dimension(radius, lambda x: NVector(x, x) * 2)
return shape
def _convert_star(self, layer: api.StarLayer):
shape = objects.Star()
shape.position = self._adjust_coords(self._convert_vector(layer.origin))
shape.inner_radius = self._adjust_scalar(self._convert_scalar(layer.radius2))
shape.outer_radius = self._adjust_scalar(self._convert_scalar(layer.radius1))
shape.rotation = self._adjust_animated(
self._convert_scalar(layer.angle),
lambda x: 90-x
)
shape.points = self._convert_scalar(layer.points)
if layer.regular_polygon.value:
shape.star_type = objects.StarType.Polygon
return shape
def _mix_animations(self, *animatable):
times = set()
for v in animatable:
self._force_animated(v)
for kf in v.keyframes:
times.add(kf.time)
for time in sorted(times):
yield [time] + [v.get_value(time) for v in animatable]
def _force_animated(self, lottieval):
if not lottieval.animated:
v = lottieval.value
lottieval.add_keyframe(0, v)
lottieval.add_keyframe(self.animation.out_point, v)
def _convert_easing_part(self, interp: api.Interpolation):
if interp == api.Interpolation.Linear:
return easing.Linear()
return easing.Sigmoid()
def _convert_easing(self, start: api.Interpolation, end: api.Interpolation):
if api.Interpolation.Constant in (start, end):
return easing.Jump()
if start == end:
return self._convert_easing_part(start)
return easing.Split(self._convert_easing_part(start), self._convert_easing_part(end))
def _convert_animatable(self, v: ast.SifAstNode, lot: objects.properties.AnimatableMixin):
if self._animated(v):
if len(v.keyframes) == 1:
lot.value = self._convert_ast_value(v.keyframes[0].value)
else:
for i, kf in enumerate(v.keyframes):
if i+1 < len(v.keyframes):
start = kf.after
end = v.keyframes[i+1].before
ease = self._convert_easing(start, end)
else:
ease = easing.Linear()
lot.add_keyframe(self._time(kf.time), self._convert_ast_value(kf.value), ease)
else:
lot.value = self._convert_ast_value(v)
return lot
def _convert_ast_value(self, v):
if isinstance(v, ast.SifRadialComposite):
return self._polar(v.radius.value, v.theta.value, 1)
elif isinstance(v, ast.SifValue):
return v.value
elif isinstance(v, ast.SifVectorComposite):
return NVector(v.x.value, v.y.value)
else:
return v
def _converted_vector_values(self, v):
if isinstance(v, ast.SifRadialComposite):
return [self._convert_scalar(v.radius), self._convert_scalar(v.theta)]
return self._convert_vector(v)
def _convert_color(self, v: ast.SifAstNode):
return self._adjust_animated(
self._convert_animatable(v, objects.ColorValue()),
self._color_gamma
)
def _convert_vector(self, v: ast.SifAstNode):
return self._convert_animatable(v, objects.MultiDimensional())
def _convert_scalar(self, v: ast.SifAstNode):
return self._convert_animatable(v, objects.Value())
def _color_gamma(self, color):
color = color.clone()
for i in range(3):
color[i] = color[i] ** (1/self.gamma[i])
return color
def _adjust_animated(self, lottieval, transform):
if lottieval.animated:
for kf in lottieval.keyframes:
if kf.start is not None:
kf.start = transform(kf.start)
if kf.end is not None:
kf.end = transform(kf.end)
else:
lottieval.value = transform(lottieval.value)
return lottieval
def _adjust_scalar(self, lottieval: objects.Value):
return self._adjust_animated(lottieval, self._scalar_mult)
def _adjust_angle(self, lottieval: objects.Value):
return self._adjust_animated(lottieval, lambda x: -x)
def _adjust_add_dimension(self, lottieval, transform):
to_val = objects.MultiDimensional()
to_val.animated = lottieval.animated
if lottieval.animated:
to_val.keyframes = []
for kf in lottieval.keyframes:
if kf.start is not None:
kf.start = transform(kf.start[0])
if kf.end is not None:
kf.end = transform(kf.end[0])
to_val.keyframes.append(kf)
else:
to_val.value = transform(lottieval.value)
return to_val
def _scalar_mult(self, x):
return x * 60
def _adjust_coords(self, lottieval: objects.MultiDimensional):
return self._adjust_animated(lottieval, self._coord)
def _coord(self, val: NVector):
return NVector(
self.target_size.x * (val.x / (self.view_p2.x - self.view_p1.x) + 0.5),
self.target_size.y * (val.y / (self.view_p2.y - self.view_p1.y) + 0.5),
)
def _convert_polygon(self, layer: api.PolygonLayer):
lot = objects.Path()
animatables = [self._convert_vector(layer.origin)] + [
self._convert_vector(p)
for p in layer.points
]
animated = any(x.animated for x in animatables)
if not animated:
lot.shape.value = self._polygon([x.value for x in animatables[1:]], animatables[0].value)
else:
for values in self._mix_animations(*animatables):
time = values[0]
origin = values[1]
points = values[2:]
lot.shape.add_keyframe(time, self._polygon(points, origin))
return lot
def _polygon(self, points, origin):
bezier = objects.Bezier()
bezier.closed = True
for point in points:
bezier.add_point(self._coord(point+origin))
return bezier
def _convert_bline(self, layer: api.AbstractOutline):
lot = objects.Path()
closed = layer.bline.loop
animatables = [
self._convert_vector(layer.origin)
]
for p in layer.bline.points:
animatables += [
self._convert_vector(p.point),
self._convert_scalar(p.t1.radius) if hasattr(p.t1, "radius") else objects.Value(0),
self._convert_scalar(p.t1.theta) if hasattr(p.t1, "radius") else objects.Value(0),
self._convert_scalar(p.t2.radius) if hasattr(p.t2, "radius") else objects.Value(0),
self._convert_scalar(p.t2.theta) if hasattr(p.t2, "radius") else objects.Value(0)
]
animated = any(x.animated for x in animatables)
if not animated:
lot.shape.value = self._bezier(
closed, [x.value for x in animatables[1:]], animatables[0].value, layer.bline.points
)
else:
for values in self._mix_animations(*animatables):
time = values[0]
origin = values[1]
values = values[2:]
lot.shape.add_keyframe(time, self._bezier(closed, values, origin, layer.bline.points))
return lot
def _bezier(self, closed, values, origin, points):
chunk_size = 5
bezier = objects.Bezier()
bezier.closed = closed
for i in range(0, len(values), chunk_size):
point, r1, a1, r2, a2 = values[i:i+chunk_size]
sifvert = point+origin
vert = self._coord(sifvert)
if not points[i//chunk_size].split_radius.value:
r2 = r1
if not points[i//chunk_size].split_angle.value:
a2 = a1
t1 = self._coord(sifvert + self._polar(r1, a1, 1)) - vert
t2 = self._coord(sifvert + self._polar(r2, a2, 2)) - vert
bezier.add_point(vert, t1, t2)
return bezier
def _polar(self, radius, angle, dir):
offset_angle = 0
if dir == 1:
offset_angle += 180
return PolarVector(radius/3, (angle+offset_angle) * math.pi / 180)
def _convert_transform_down(self, tl: api.TransformDown):
group = objects.Group()
self._set_name(group, tl)
if isinstance(tl, api.TranslateLayer):
group.transform.anchor_point.value = self.target_size / 2
group.transform.position = self._adjust_coords(self._convert_vector(tl.origin))
elif isinstance(tl, api.RotateLayer):
group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.origin))
group.transform.position = group.transform.anchor_point.clone()
group.transform.rotation = self._adjust_angle(self._convert_scalar(tl.amount))
elif isinstance(tl, api.ScaleLayer):
group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.center))
group.transform.position = group.transform.anchor_point.clone()
group.transform.scale = self._adjust_add_dimension(
self._convert_scalar(tl.amount),
self._zoom_to_scale
)
return group
def _zoom_to_scale(self, value):
zoom = math.e ** value * 100
return NVector(zoom, zoom)
def _set_name(self, lottie, sif):
lottie.name = sif.desc if sif.desc is not None else sif.__class__.__name__
def _convert_gradient(self, layer: api.GradientLayer, parent):
group = objects.Group()
parent_shapes = parent.shapes
parent.shapes = []
if isinstance(parent, objects.Group):
parent.shapes.append(parent_shapes[-1])
self._gradient_gather_shapes(parent_shapes, group)
gradient = objects.GradientFill()
self._set_name(gradient, layer)
group.add_shape(gradient)
gradient.colors = self._convert_gradient_stops(layer.gradient)
gradient.opacity = self._adjust_animated(
self._convert_scalar(layer.amount),
lambda x: x * 100
)
if isinstance(layer, api.LinearGradient):
gradient.start_point = self._adjust_coords(self._convert_vector(layer.p1))
gradient.end_point = self._adjust_coords(self._convert_vector(layer.p2))
gradient.gradient_type = objects.GradientType.Linear
elif isinstance(layer, api.RadialGradient):
gradient.gradient_type = objects.GradientType.Radial
gradient.start_point = self._adjust_coords(self._convert_vector(layer.center))
radius = self._adjust_animated(self._convert_scalar(layer.radius), lambda x: x*45)
if not radius.animated and not gradient.start_point.animated:
gradient.end_point.value = gradient.start_point.value + NVector(radius.value, radius.value)
else:
for time, c, r in self._mix_animations(gradient.start_point.clone(), radius):
gradient.end_point.add_keyframe(time, c + NVector(r + r))
return group
def _gradient_gather_shapes(self, shapes, output: objects.Group):
for shape in shapes:
if isinstance(shape, objects.Shape):
output.add_shape(shape)
elif isinstance(shape, objects.Group):
self._gradient_gather_shapes(shape.shapes, output)
def _convert_gradient_stops(self, sif_gradient):
stops = objects.GradientColors()
if not self._animated(sif_gradient):
stops.set_stops(self._flatten_gradient_colors(sif_gradient.value))
stops.count = len(sif_gradient.value)
else:
# TODO easing
for kf in sif_gradient.keyframes:
stops.add_keyframe(self._time(kf.time), self._flatten_gradient_colors(kf.value))
stops.count = len(kf.value)
return stops
def _flatten_gradient_colors(self, stops):
return [
(stop.pos, self._color_gamma(stop.color))
for stop in stops
]
def _convert_text(self, layer: api.TextLayer):
shape = font.FontShape(layer.text.value, font.FontStyle(layer.family.value, 110, font.TextJustify.Center))
shape.refresh()
trans = shape.wrapped.transform
trans.anchor_point.value = shape.wrapped.bounding_box().center()
trans.anchor_point.value.x /= 2
trans.position = self._adjust_coords(self._convert_vector(layer.origin))
trans.scale = self._adjust_animated(
self._convert_vector(layer.size),
lambda v: v * 100
)
return shape
+7
View File
@@ -0,0 +1,7 @@
from ..tgs import open_maybe_gzipped
def parse_sif_file(file):
from .converter import convert
from . import api
return convert(open_maybe_gzipped(file, api.Canvas.from_xml_file))
View File
+165
View File
@@ -0,0 +1,165 @@
from xml.dom import minidom
import enum
from uuid import uuid4
from lottie.nvector import NVector
from lottie.parsers.sif.xml.utils import xml_text, str_to_bool
from lottie.parsers.sif.xml.utils import xml_child_elements, value_from_xml_string, xml_make_text, value_to_xml_string
from lottie.parsers.sif.sif.frame_time import FrameTime
class ObjectRegistry:
def __init__(self):
self.registry = {}
def register_as(self, object, key):
self.registry[key] = object
def register(self, object):
guid = getattr(object, "guid", None)
if guid is None:
guid = self.guid()
object.guid = guid
self.registry[guid] = object
@classmethod
def guid(cls):
return str(uuid4()).replace("-", "").upper()
def get_object(self, guid):
return self.registry[guid]
def noop(x):
return x
class TypeDescriptor:
_type_tag_names = {
"bone_object": "bone"
}
def __init__(self, typename, default=None, type_wrapper=noop):
self.typename = typename
self.type_wrapper = type_wrapper
self.default_value = default
def value_to_xml_element(self, value, dom: minidom.Document):
element = dom.createElement(self.tag_name)
if self.typename == "vector":
element.appendChild(xml_make_text(dom, "x", str(value.x)))
element.appendChild(xml_make_text(dom, "y", str(value.y)))
if hasattr(value, "guid"):
element.setAttribute("guid", value.guid)
elif self.typename == "color":
element.appendChild(xml_make_text(dom, "r", str(value[0])))
element.appendChild(xml_make_text(dom, "g", str(value[1])))
element.appendChild(xml_make_text(dom, "b", str(value[2])))
element.appendChild(xml_make_text(dom, "a", str(value[3])))
if hasattr(value, "guid"):
element.setAttribute("guid", value.guid)
elif self.typename == "gradient":
for point in value:
element.appendChild(point.to_dom(dom))
elif self.typename == "bool":
element.setAttribute("value", "true" if value else "false")
elif self.typename == "bone_object":
element.setAttribute("guid", value.guid)
element.setAttribute("type", self.typename)
elif self.typename == "string":
element.appendChild(dom.createTextNode(value))
else:
if isinstance(value, enum.Enum):
value = value.value
element.setAttribute("value", str(value))
return element
@property
def tag_name(self):
return self._type_tag_names.get(self.typename, self.typename)
def value_from_xml_element(self, xml: minidom.Element, registry: ObjectRegistry):
if xml.tagName != self.tag_name:
raise ValueError("Wrong value type (%s instead of %s)" % (xml.tagName, self.tag_name))
guid = xml.getAttribute("guid")
if guid and guid in registry.registry:
value = registry.registry[guid]
elif self.typename == "vector":
value = NVector(
float(xml_text(xml.getElementsByTagName("x")[0])),
float(xml_text(xml.getElementsByTagName("y")[0]))
)
if xml.getAttribute("guid"):
value.guid = xml.getAttribute("guid")
registry.register(value)
elif self.typename == "color":
value = NVector(
float(xml_text(xml.getElementsByTagName("r")[0])),
float(xml_text(xml.getElementsByTagName("g")[0])),
float(xml_text(xml.getElementsByTagName("b")[0])),
float(xml_text(xml.getElementsByTagName("a")[0]))
)
elif self.typename == "gradient":
value = [
GradientPoint.from_dom(sub, registry)
for sub in xml_child_elements(xml, GradientPoint.type.typename)
]
elif self.typename == "real" or self.typename == "angle":
value = float(xml.getAttribute("value"))
elif self.typename == "integer":
value = int(xml.getAttribute("value"))
elif self.typename == "time":
value = FrameTime.parse_string(xml.getAttribute("value"), registry)
elif self.typename == "bool":
value = str_to_bool(xml.getAttribute("value"))
elif self.typename == "string":
return xml_text(xml)
elif self.typename == "bone_object":
# Already done above but this forces the guid to be present
return registry.get_object(xml.getAttribute("guid"))
else:
raise ValueError("Unsupported type %s" % self.typename)
return self.type_wrapper(value)
class GradientPoint:
type = TypeDescriptor("color")
def __init__(self, pos: float, color: NVector):
self.pos = pos
self.color = color
def to_dom(self, dom: minidom.Document):
element = self.type.value_to_xml_element(self.color, dom)
element.setAttribute("pos", value_to_xml_string(self.pos, float))
return element
@classmethod
def from_dom(cls, xml: minidom.Element, registry: ObjectRegistry):
return GradientPoint(
value_from_xml_string(xml.getAttribute("pos"), float, registry),
cls.type.value_from_xml_element(xml, registry)
)
def __repr__(self):
return "<GradientPoint %s %s>" % (self.pos, self.color)
class SifNodeMeta(type):
def __new__(cls, name, bases, attr):
props = []
for base in bases:
if type(base) == cls:
props += base._nodes
attr["_nodes"] = props + attr.get("_nodes", [])
if "_tag" not in attr:
attr["_tag"] = name.lower()
attr["_nodemap"] = {
node.att_name: node
for node in attr["_nodes"]
}
return super().__new__(cls, name, bases, attr)
+9
View File
@@ -0,0 +1,9 @@
import enum
class Smooth(enum.Enum):
NearestNeighbour = 0
Linear = 1
Cosine = 2
Spline = 3
Cubic = 4
+46
View File
@@ -0,0 +1,46 @@
import enum
class FrameTime:
class Unit(enum.Enum):
Frame = "f"
Seconds = "s"
def __init__(self, value, unit):
self.value = value
self.unit = unit
def __eq__(self, other):
return self.value == other.value and self.unit == other.unit
def __ne__(self, other):
return self.value == other.value and self.unit == other.unit
def __str__(self):
return "%s%s" % (self.value, self.unit.value)
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self)
@classmethod
def frame(cls, amount):
return cls(amount, cls.Unit.Frame)
@classmethod
def seconds(cls, amount):
return cls(amount, cls.Unit.Seconds)
@classmethod
def parse_string(cls, value_str, canvas):
if " " in value_str:
value = 0
unit = cls.Unit.Frame
for sub in value_str.split():
sv = float(sub[:-1])
if sub[-1] == "s":
sv *= canvas.fps
value += sv
else:
value = float(value_str[:-1])
unit = cls.Unit(value_str[-1])
return FrameTime(value, unit)
File diff suppressed because it is too large Load Diff
View File
+160
View File
@@ -0,0 +1,160 @@
from xml.dom import minidom
import copy
import enum
from .core_nodes import XmlDescriptor, XmlSimpleElement, ValueReference
from .utils import *
from lottie.nvector import NVector
from lottie.parsers.sif.ast_impl.base import SifAstNode, SifValue
from lottie.parsers.sif.sif.core import ObjectRegistry, TypeDescriptor, noop
class XmlAnimatable(XmlDescriptor):
def __init__(self, name, typename, default=None, type_wrapper=noop):
super().__init__(name)
self.type = TypeDescriptor(typename, default, type_wrapper)
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry, param: TypeDescriptor = None):
cn = xml_first_element_child(parent, self.name)
if cn:
value = SifAstNode.from_dom(xml_first_element_child(cn), self.type_for(param), registry)
else:
value = self.default()
setattr(obj, self.att_name, value)
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document, type: TypeDescriptor = None):
value = getattr(obj, self.att_name)
if isinstance(value, SifValue) and isinstance(value.value, ValueReference):
parent.setAttribute(self.name, ":" + value.value.id)
return
param = parent.appendChild(dom.createElement(self.name))
param.appendChild(value.to_dom(dom, self.type_for(type)))
return param
def from_python(self, value):
if not isinstance(value, SifAstNode):
raise ValueError("%s isn't a valid value for %s" % (value, self.name))
return value
def default(self):
return SifValue(copy.deepcopy(self.type.default_value))
def type_for(self, param: TypeDescriptor):
if param is not None and self.type.typename == "_recurse":
return param
return self.type
class XmlParam(XmlDescriptor):
def __init__(self, name, typename, default=None, type_wrapper=noop, static=False):
super().__init__(name)
self.type = TypeDescriptor(typename, default, type_wrapper)
self.static = static
def _def(self):
from ..api import Def
return Def
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
for cn in xml_child_elements(parent, "param"):
if cn.getAttribute("name") == self.name:
use = cn.getAttribute("use")
if use:
value = registry.get_object(use)
else:
value_node = xml_first_element_child(cn)
if self.static:
value = self.type.value_from_xml_element(value_node, registry)
else:
value = SifAstNode.from_dom(value_node, self.type, registry)
break
else:
value = self.default()
setattr(obj, self.att_name, value)
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
param = parent.appendChild(dom.createElement("param"))
param.setAttribute("name", self.name)
value = getattr(obj, self.att_name)
if isinstance(value, self._def()):
param.setAttribute("use", ":" + value.id)
else:
if self.static:
elem = self.type.value_to_xml_element(value, dom)
else:
elem = value.to_dom(dom, self.type)
param.appendChild(elem)
return param
def from_python(self, value):
if self.static:
return self.type.type_wrapper(value)
if not isinstance(value, (SifAstNode, self._def())):
raise ValueError("%s isn't a valid value for %s" % (value, self.name))
return value
def default(self):
if self.static:
return copy.deepcopy(self.type.default_value)
return SifValue(copy.deepcopy(self.type.default_value))
class XmlDynamicListParam(XmlDescriptor):
_tag = "dynamic_list"
def __init__(self, name, typename, att_name=None):
super().__init__(name)
self.type = TypeDescriptor(typename)
if att_name is not None:
self.att_name = att_name
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
param = parent.appendChild(dom.createElement("param"))
param.setAttribute("name", self.name)
dyl = param.appendChild(dom.createElement(self._tag))
dyl.setAttribute("type", self.type.typename)
values = getattr(obj, self.att_name)
for val in values:
entry = dyl.appendChild(dom.createElement("entry"))
entry.appendChild(self._value_to_dom(val, dom))
def _value_to_dom(self, val, dom: minidom.Document):
return val.to_dom(dom, self.type)
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
values = []
for cn in xml_child_elements(parent, "param"):
if cn.getAttribute("name") == self.name:
list = xml_first_element_child(cn)
if list.getAttribute("type") != self.type.typename:
raise ValueError(
"Wrong type for %s: got %s instead of %s" %
(self.name, self.type.typename, list.getAttribute("type"))
)
for entry in xml_child_elements(list, "entry"):
values.append(self._value_from_dom(xml_first_element_child(entry), registry))
break
setattr(obj, self.att_name, values)
def _value_from_dom(self, element, registry):
return SifAstNode.from_dom(element, self.type, registry)
def from_python(self, value):
return value
def default(self):
return []
class XmlStaticListParam(XmlDynamicListParam):
_tag = "static_list"
def _value_to_dom(self, val, dom: minidom.Document):
return self.type.value_to_xml_element(val, dom)
def _value_from_dom(self, element: minidom.Element, registry: ObjectRegistry):
return self.type.value_from_xml_element(element, registry)
+142
View File
@@ -0,0 +1,142 @@
from xml.dom import minidom
import copy
from uuid import uuid4
from .utils import *
from lottie.parsers.sif.sif.core import ObjectRegistry
class XmlDescriptor:
def __init__(self, name):
self.name = name
self.att_name = name.replace("-", "_").replace("[", "_").replace("]", "").replace(".", "_")
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
raise NotImplementedError
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
raise NotImplementedError
def from_python(self, value):
raise NotImplementedError
def initialize_object(self, dict, obj):
if self.att_name in dict:
setattr(obj, self.att_name, self.from_python(dict[self.att_name]))
else:
setattr(obj, self.att_name, self.default())
def clean(self, value):
return self.from_python(value)
def default(self):
return None
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, self.name)
class TypedXmlDescriptor(XmlDescriptor):
def __init__(self, name, type=str, default_value=None, att_name=None):
super().__init__(name)
self.type = type
self.default_value = default_value
if att_name is not None:
self.att_name = att_name
def from_python(self, value):
if value is None and self.default_value is None:
return None
if not value_isinstance(value, self.type):
return self.type(value)
return value
def default(self):
return copy.deepcopy(self.default_value)
class ValueReference:
def __init__(self, id, value=None):
self.value = value
self.id = id
@classmethod
def from_registry(cls, id, registry: ObjectRegistry):
return cls(id, registry.get_object(id))
class XmlAttribute(TypedXmlDescriptor):
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
xml_str = parent.getAttribute(self.name)
if xml_str:
if xml_str.startswith(":") and xml_str[1:] in registry.registry:
value = ValueReference.from_registry(xml_str[1:], registry)
else:
value = value_from_xml_string(xml_str, self.type, registry)
setattr(obj, self.att_name, value)
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
value = getattr(obj, self.att_name)
if value is not None:
if isinstance(value, ValueReference):
xml_str = ":" + value.id
else:
xml_str = value_to_xml_string(value, self.type)
parent.setAttribute(self.name, xml_str)
class XmlFixedAttribute(XmlDescriptor):
def __init__(self, name, value, type=str):
super().__init__(name)
self.value = value
self.type = type
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
parent.setAttribute(self.name, value_to_xml_string(self.value, self.type))
def from_python(self, value):
if value != self.value:
raise ValueError("Value of %s should be %s, got %s" % (self.name, self.value, value))
return value
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
xml_str = parent.getAttribute(self.name)
setattr(obj, self.att_name, value_from_xml_string(xml_str, self.type, registry))
def default(self):
return self.value
class XmlSimpleElement(TypedXmlDescriptor):
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
cn = xml_first_element_child(parent, self.name, allow_none=True)
if cn:
value = value_from_xml_string(xml_text(cn), self.type, registry)
else:
value = self.default_value
setattr(obj, self.att_name, value)
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
value = getattr(obj, self.att_name)
if value is not None:
parent.appendChild(xml_make_text(dom, self.name, value_to_xml_string(value, self.type)))
class XmlMeta(TypedXmlDescriptor):
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
for cn in xml_child_elements(parent, "meta"):
if cn.getAttribute("name") == self.name:
value = value_from_xml_string(cn.getAttribute("content"), self.type, registry)
break
else:
value = self.default_value
setattr(obj, self.att_name, value)
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
value = getattr(obj, self.att_name)
if value is not None:
meta = parent.appendChild(dom.createElement("meta"))
meta.setAttribute("name", self.name)
meta.setAttribute("content", value_to_xml_string(value, self.type))
+85
View File
@@ -0,0 +1,85 @@
from xml.dom import minidom
from distutils.util import strtobool
from lottie.nvector import NVector
from lottie.parsers.sif.sif.frame_time import FrameTime
class _tag:
def __init__(self, type):
self.type = type
def __call__(self, v):
return self.type(v)
bool_str = _tag(bool)
def str_to_bool(strval):
return bool(strtobool(strval))
def value_from_xml_string(xml_str, type, registry):
if type in (bool_str, bool):
return str_to_bool(xml_str)
elif type is NVector:
return NVector(*map(float, xml_str.split()))
if type is FrameTime:
return FrameTime.parse_string(xml_str, registry)
return type(xml_str)
def value_to_xml_string(value, type):
if type is bool:
return "1" if value else "0"
if type is bool_str:
return "true" if value else "false"
elif type is NVector:
return " ".join(map(str, value))
return str(value)
def value_isinstance(value, type):
if isinstance(type, _tag):
type = type.type
return isinstance(value, type)
def xml_text(node):
return "".join(
x.nodeValue
for x in node.childNodes
if x.nodeType in {minidom.Node.TEXT_NODE, minidom.Node.CDATA_SECTION_NODE}
)
def xml_make_text(dom: minidom.Document, tag_name, text):
e = dom.createElement(tag_name)
e.appendChild(dom.createTextNode(text))
return e
def xml_element_matches(ch: minidom.Node, tagname=None):
if ch.nodeType != minidom.Node.ELEMENT_NODE:
return False
if tagname is not None and ch.tagName != tagname:
return False
return True
def xml_child_elements(xml: minidom.Node, tagname=None):
for ch in xml.childNodes:
if xml_element_matches(ch, tagname):
yield ch
def xml_first_element_child(xml: minidom.Node, tagname=None, allow_none=False):
for ch in xml_child_elements(xml, tagname):
return ch
if allow_none:
return None
raise ValueError("No %s in %s" % (tagname or "child element", getattr(xml, "tagName", "node")))
+217
View File
@@ -0,0 +1,217 @@
from .utils import *
from .core_nodes import XmlDescriptor, ObjectRegistry
class XmlParamSif(XmlDescriptor):
def __init__(self, name, child_node, default_ctor=None):
super().__init__(name)
self.child_node = child_node
self.default_ctor = default_ctor or child_node
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
for cn in xml_child_elements(parent, "param"):
if cn.getAttribute("name") == self.name:
value = self.child_node.from_dom(xml_first_element_child(cn), registry)
break
else:
value = self.default()
setattr(obj, self.att_name, value)
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
param = parent.appendChild(dom.createElement("param"))
param.setAttribute("name", self.name)
param.appendChild(getattr(obj, self.att_name).to_dom(dom))
return param
def clean(self, value):
if not isinstance(value, self.child_node):
raise ValueError("%s isn't a valid value for %s" % (value, self.name))
return value
def default(self):
return self.default_ctor()
class SifNodeList:
def __init__(self, type):
self._items = []
self._type = type
def __len__(self):
return len(self._items)
def __iter__(self):
return iter(self._items)
def __getitem__(self, name):
return self._items[name]
def __getslice__(self, i, j):
return self._items[i:j]
def __setitem__(self, key, value: "Layer"):
self.validate(value)
self._items[key] = value
def append(self, value: "Layer"):
self.validate(value)
self._items.append(value)
def __str__(self):
return str(self._items)
def __repr__(self):
return "<SifNodeList %s>" % self._items
def validate(self, value):
if not isinstance(value, self._type):
raise ValueError("Not a valid object: %s" % value)
class XmlSifElement(XmlDescriptor):
def __init__(self, name, child_node, nested=True):
super().__init__(name)
self.child_node = child_node
self.nested = nested
def default(self):
return self.child_node()
def from_python(self, value):
if not isinstance(value, self.child_node):
raise ValueError("Invalid value for %s: %s" % (self.name, value))
return value
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
cn = xml_first_element_child(parent, self.name, allow_none=True)
if cn:
if self.nested:
element = xml_first_element_child(cn)
else:
element = cn
value = self.child_node.from_dom(element, registry)
else:
value = self.default()
setattr(obj, self.att_name, value)
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
value = getattr(obj, self.att_name)
if self.nested:
node = dom.createElement(self.name)
parent.appendChild(node)
else:
node = parent
node.appendChild(value.to_dom(dom))
class XmlList(XmlDescriptor):
def __init__(self, child_node, name=None, wrapper_tag=None, tags=None):
super().__init__(wrapper_tag or child_node._tag)
self.child_node = child_node
self.att_name = self.att_name + "s" if name is None else name
self.wrapper_tag = wrapper_tag
if tags is None:
self.tags = {self.name}
else:
self.tags = tags
def default(self):
return SifNodeList(self.child_node)
def clean(self, value):
return value
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
values = self.default()
for cn in xml_child_elements(parent):
if cn.tagName in self.tags:
value_node = cn
if self.wrapper_tag:
value_node = xml_first_element_child(cn)
values.append(self.child_node.from_dom(value_node, registry))
setattr(obj, self.att_name, values)
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
for value in getattr(obj, self.att_name):
value_node = value.to_dom(dom)
if self.wrapper_tag:
wrapper = dom.createElement(self.wrapper_tag)
wrapper.appendChild(value_node)
value_node = wrapper
parent.appendChild(value_node)
class XmlWrapper(XmlDescriptor):
def __init__(self, name, wrapped: XmlDescriptor):
super().__init__(name)
self.wrapped = wrapped
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
wrapper = parent.appendChild(dom.createElement(self.name))
self.wrapped.to_xml(obj, wrapper, dom)
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
wrapper = xml_first_element_child(parent, self.name, True)
if wrapper:
return self.wrapped.from_xml(obj, wrapper, registry)
return self.default()
def from_python(self, value):
return self.wrapped.from_python(value)
def initialize_object(self, dict, obj):
return self.wrapped.initialize_object(dict, obj)
def clean(self, value):
return self.wrapped.clean(value)
def default(self):
return self.wrapped.default()
class XmlWrapperParam(XmlWrapper):
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
wrapper = parent.appendChild(dom.createElement("param"))
wrapper.setAttribute("name", self.name)
self.wrapped.to_xml(obj, wrapper, dom)
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
for wrapper in xml_child_elements(parent, "param"):
if wrapper.getAttribute("name") == self.name:
return self.wrapped.from_xml(obj, wrapper, registry)
return self.default()
class XmlBoneReference(XmlDescriptor):
def __init__(self, name):
super().__init__(name)
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
value = getattr(obj, self.name, None)
if not value:
return
node = parent.appendChild(dom.createElement(self.name))
value_node = node.appendChild(dom.createElement("bone_valuenode"))
value_node.setAttribute("type", value.type)
value_node.setAttribute("guid", value.guid)
return node
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
node = xml_first_element_child(parent, self.name, True)
value = None
if node:
value_node = xml_first_element_child(node, "bone_valuenode", True)
if value_node:
value = registry.get_object(value_node.getAttribute("guid"))
if value.type != value_node.getAttribute("type"):
raise ValueError("Bone type %s is not %s" % (value.type, value_node.getAttribute("type")))
setattr(obj, self.att_name, value)
def from_python(self, value):
return value