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
+2
View File
@@ -0,0 +1,2 @@
from . import svg, tgs, sif
__all__ = ["svg", "tgs", "sif"]
+140
View File
@@ -0,0 +1,140 @@
import sys
import os
import pkgutil
import argparse
import importlib
class Baseporter:
def __init__(self, name, extensions, callback, extra_options=[], generic_options=set(), slug=None):
self.name = name
self.extensions = extensions
self.callback = callback
self.extra_options = extra_options
self.generic_options = generic_options
self.slug = slug if slug is not None else extensions[0]
def process(self, *a, **kw):
return self.callback(*a, **kw)
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.slug)
def argparse_options(self, ns):
o_options = {}
for opt in self.extra_options:
o_options[opt.dest] = getattr(ns, opt.nsvar(self.slug))
for opt in self.generic_options:
o_options[opt] = getattr(ns, opt)
return o_options
class ExtraOption:
def __init__(self, name, **kwargs):
self.name = name
self.kwargs = kwargs
if "action" not in self.kwargs:
self.kwargs["metavar"] = self.name
self.dest = kwargs.pop("dest", name)
def add_argument(self, slug, parser):
opt = "--%s-%s" % (slug, self.name.replace("_", "-"))
parser.add_argument(opt, dest=self.nsvar(slug), **self.kwargs)
def nsvar(self, slug):
return "%s_%s" % (slug, self.dest)
def _add_options(parser, ie, object):
if not object.extra_options:
return
suf = " %sing options" % ie
group = parser.add_argument_group(object.name + suf)
for op in object.extra_options:
op.add_argument(object.slug, group)
class Loader:
def __init__(self, module_path, module_name, ie):
self._loaded = False
self._registry = {}
self._module_path = os.path.dirname(module_path)
self._module_name = module_name.replace(".base", "")
self._ie = ie
self._failed = {}
def load_modules(self):
self._loaded = True
for _, modname, _ in pkgutil.iter_modules([self._module_path]):
if modname == "base":
continue
full_modname = "." + modname
try:
importlib.import_module(full_modname, self._module_name)
except ImportError as e:
self._failed[modname] = e.name
@property
def failed_modules(self):
if not self._loaded:
self.load_modules()
return self._failed
@property
def items(self):
if not self._loaded:
self.load_modules()
return self._registry
def __iter__(self):
return iter(self.items.values())
def get(self, slug):
return self.items.get(slug, None)
def __getitem__(self, key):
return self.get(key)
def get_from_filename(self, filename):
return self.get_from_extension(os.path.splitext(filename)[1][1:])
def get_from_extension(self, ext):
for p in self.items.values():
if ext in p.extensions:
return p
return None
def set_options(self, parser):
for exporter in self.items.values():
_add_options(parser, self._ie, exporter)
def keys(self):
return self.items.keys()
def decorator(self, name, extensions, extra_options=[], generic_options=set(), slug=None):
def decorator(callback):
porter = Baseporter(name, extensions, callback, extra_options, generic_options, slug)
self._registry[porter.slug] = porter
return callback
return decorator
class IoProgressReporter:
def report_progress(self, title, value, total):
sys.stderr.write("\r%s %s/%s" % (title, value, total))
sys.stderr.flush()
def report_message(self, message):
sys.stderr.write("\r" + message + "\n")
sys.stderr.flush()
IoProgressReporter.instance = IoProgressReporter()
def io_progress():
return IoProgressReporter.instance
+275
View File
@@ -0,0 +1,275 @@
from PIL import Image
from .. import objects
from .. import NVector, Color
from ..utils import color
class Polygen:
def __init__(self, x, y):
self.vertices = [
NVector(x, y),
NVector(x+1, y),
NVector(x+1, y+1),
NVector(x, y+1),
]
self._has_x = False
self._has_y = False
def add_pixel_x(self, x, y):
i = self.vertices.index(NVector(x, y))
if len(self.vertices) > i and self.vertices[i+1] == NVector(x, y+1):
self._has_x = True
self.vertices.insert(i+1, NVector(x+1, y))
self.vertices.insert(i+2, NVector(x+1, y+1))
else:
raise ValueError()
def add_pixel_x_neg(self, x, y):
i = self.vertices.index(NVector(x+1, y))
if i > 0 and self.vertices[i-1] == NVector(x+1, y+1):
self._has_x = True
self.vertices.insert(i, NVector(x, y))
self.vertices.insert(i, NVector(x, y+1))
else:
raise ValueError()
def add_pixel_y(self, x, y):
i = self.vertices.index(NVector(x, y))
if i > 0 and self.vertices[i-1] == NVector(x+1, y):
self._has_y = True
if i > 1 and self.vertices[i-2] == NVector(x+1, y+1):
self.vertices[i-1] = NVector(x, y+1)
else:
self.vertices.insert(i, NVector(x, y+1))
self.vertices.insert(i, NVector(x+1, y+1))
else:
raise ValueError()
def _to_rect(self, id1, id2):
p1 = self.vertices[id1]
p2 = self.vertices[id2]
return objects.Rect((p1+p2)/2, p2-p1)
def to_shape(self):
if not self._has_x or not self._has_y:
return self._to_rect(0, int(len(self.vertices)/2))
bez = objects.Bezier()
bez.closed = True
for point in self.vertices:
if len(bez.vertices) > 1 and (
bez.vertices[-1].x == bez.vertices[-2].x == point.x or
bez.vertices[-1].y == bez.vertices[-2].y == point.y
):
bez.vertices[-1] = point
else:
bez.add_point(point)
if len(bez.vertices) > 2 and bez.vertices[0].x == bez.vertices[-1].x == bez.vertices[-2].x:
bez.vertices.pop()
bez.out_tangents.pop()
bez.in_tangents.pop()
return objects.Path(bez)
def pixel_add_layer_paths(animation, raster):
layer = animation.add_layer(objects.ShapeLayer())
groups = {}
processed = set()
xneg_candidates = set()
def avail(x, y):
rid = (x, y)
return not (
x < 0 or x >= raster.width or y >= raster.height or
rid in processed or raster.getpixel(rid) != colort
)
def recurse(gen, x, y, xneg):
processed.add((x, y))
if avail(x+1, y):
gen.add_pixel_x(x+1, y)
recurse(gen, x+1, y, False)
if avail(x, y+1):
gen.add_pixel_y(x, y+1)
recurse(gen, x, y+1, True)
if xneg and avail(x-1, y):
xneg_candidates.add((x-1, y))
for y in range(raster.height):
for x in range(raster.width):
pid = (x, y)
colort = raster.getpixel(pid)
if colort[-1] == 0 or pid in processed:
continue
gen = Polygen(x, y)
xneg_candidates = set()
recurse(gen, x, y, False)
xneg_candidates -= processed
while xneg_candidates:
p = next(iter(sorted(xneg_candidates, key=lambda t: (t[1], t[0]))))
gen.add_pixel_x_neg(*p)
recurse(gen, p[0], p[1], True)
processed.add(p)
xneg_candidates -= processed
g = groups.setdefault(colort, set())
g.add(gen.to_shape())
for colort, rects in groups.items():
g = layer.add_shape(objects.Group())
g.shapes = list(rects) + g.shapes
g.name = "".join("%02x" % c for c in colort)
fill = g.add_shape(objects.Fill())
fill.color.value = color.from_uint8(*colort[:3])
fill.opacity.value = colort[-1] / 255 * 100
stroke = g.add_shape(objects.Stroke(fill.color.value, 0.1))
stroke.opacity.value = fill.opacity.value
return layer
def pixel_add_layer_rects(animation, raster):
layer = animation.add_layer(objects.ShapeLayer())
last_rects = {}
groups = {}
def merge_up():
if last_rect and last_rect._start in last_rects:
yrect = last_rects[last_rect._start]
if yrect.size.value.x == last_rect.size.value.x and yrect._color == last_rect._color:
groups[last_rect._color].remove(last_rect)
yrect.position.value.y += 0.5
yrect.size.value.y += 1
rects[last_rect._start] = yrect
def group(colort):
return groups.setdefault(colort, set())
for y in range(raster.height):
rects = {}
last_color = None
last_rect = None
for x in range(raster.width):
colort = raster.getpixel((x, y))
if colort[-1] == 0:
continue
yrect = last_rects.get(x, None)
if colort == last_color:
last_rect.position.value.x += 0.5
last_rect.size.value.x += 1
elif yrect and colort == yrect._color and yrect.size.value.x == 1:
yrect.position.value.y += 0.5
yrect.size.value.y += 1
rects[x] = yrect
last_color = last_rect = colort = None
else:
merge_up()
g = group(colort)
last_rect = objects.Rect()
g.add(last_rect)
last_rect.size.value = NVector(1, 1)
last_rect.position.value = NVector(x + 0.5, y + 0.5)
rects[x] = last_rect
last_rect._start = x
last_rect._color = colort
last_color = colort
merge_up()
last_rects = rects
for colort, rects in groups.items():
g = layer.add_shape(objects.Group())
g.shapes = list(rects) + g.shapes
g.name = "".join("%02x" % c for c in colort)
fill = g.add_shape(objects.Fill())
fill.color.value = color.from_uint8(*colort[:3])
fill.opacity.value = colort[-1] / 255 * 100
stroke = g.add_shape(objects.Stroke(fill.color.value, 0.1))
stroke.opacity.value = fill.opacity.value
return layer
def _vectorizing_func(filenames, frame_delay, framerate, callback):
if not isinstance(filenames, list):
filenames = [filenames]
animation = objects.Animation(0, framerate)
nframes = 0
for filename in filenames:
raster = Image.open(filename)
if nframes == 0:
animation.width = raster.width
animation.height = raster.height
if not hasattr(raster, "is_animated"):
raster.n_frames = 1
raster.seek = lambda x: None
for frame in range(raster.n_frames):
raster.seek(frame)
new_im = Image.new("RGBA", raster.size)
new_im.paste(raster)
callback(animation, new_im, nframes + frame)
new_im.close()
nframes += raster.n_frames
animation.out_point = frame_delay * nframes
#animation._nframes = nframes
return animation
def raster_to_embedded_assets(filenames, frame_delay=1, framerate=60, embed_format=None):
"""!
@brief Loads external assets
"""
def callback(animation, raster, frame):
asset = objects.assets.Image.embedded(raster, embed_format)
animation.assets.append(asset)
layer = animation.add_layer(objects.ImageLayer(asset.id))
layer.in_point = frame * frame_delay
layer.out_point = layer.in_point + frame_delay
return _vectorizing_func(filenames, frame_delay, framerate, callback)
def raster_to_linked_assets(filenames, frame_delay=1, framerate=60):
"""!
@brief Loads external assets
"""
animation = objects.Animation(frame_delay * len(filenames), framerate)
for frame, filename in enumerate(filenames):
asset = objects.assets.Image.linked(filename)
animation.assets.append(asset)
layer = animation.add_layer(objects.ImageLayer(asset.id))
layer.in_point = frame * frame_delay
layer.out_point = layer.in_point + frame_delay
return animation
def pixel_to_animation(filenames, frame_delay=1, framerate=60):
"""!
@brief Converts pixel art to vector
"""
def callback(animation, raster, frame):
layer = pixel_add_layer_rects(animation, raster.convert("RGBA"))
layer.in_point = frame * frame_delay
layer.out_point = layer.in_point + frame_delay
return _vectorizing_func(filenames, frame_delay, framerate, callback)
def pixel_to_animation_paths(filenames, frame_delay=1, framerate=60):
"""!
@brief Converts pixel art to vector paths
Slower and yields larger files compared to pixel_to_animation,
but it produces a single shape for each area with the same color.
Mostly useful when you want to add your own animations to the loaded image
"""
def callback(animation, raster, frame):
layer = pixel_add_layer_paths(animation, raster.convert("RGBA"))
layer.in_point = frame * frame_delay
layer.out_point = layer.in_point + frame_delay
return _vectorizing_func(filenames, frame_delay, framerate, callback)
+248
View File
@@ -0,0 +1,248 @@
# NOTE: requires pillow, pypotrace>=0.2, numpy, scipy to be installed
from PIL import Image
import potrace
import numpy
import enum
from scipy.cluster.vq import kmeans
from .. import objects
from ..nvector import NVector
from .pixel import _vectorizing_func
class QuanzationMode(enum.Enum):
Nearest = 1
Exact = 2
class RasterImage:
def __init__(self, data):
self.data = data
@classmethod
def from_pil(cls, image):
return cls(numpy.array(image))
#@classmethod
#def open(cls, filename):
#return cls.from_pil(Image.open(filename))
def k_means(self, n_colors):
"""!
Returns a list of centroids
"""
colors = []
for row in range(self.data.shape[0]):
for column in range(self.data.shape[1]):
if self.get_alpha(row, column) == 255:
colors.append(self.data[row][column])
colors = numpy.array(colors, numpy.float)
return kmeans(colors, n_colors+1)[0]
def get_alpha(self, row, column):
if self.data.shape[2] >= 4:
return self.data[row][column][3]
return 255
def quantize(self, codebook, quantization_mode=QuanzationMode.Nearest):
"""!
Returns a list of tuple [color, data] where for each color in codebook
data is a bit mask for the image
You can get codebook from k_means
"""
if codebook is None or len(codebook) == 0:
return [(numpy.array([0., 0., 0., 255.]), self.mono())]
mono_data = []
for c in codebook:
mono_data.append((c, numpy.zeros(self.data.shape[:2])))
for row in range(self.data.shape[0]):
for column in range(self.data.shape[1]):
if self.get_alpha(row, column) == 255:
if quantization_mode == QuanzationMode.Nearest:
min_norm = 511 # (norm of [255, 255, 255, 255]) + 1
best = None
for color, bitmap in mono_data:
norm = numpy.linalg.norm(self.data[row][column] - color)
if norm < min_norm:
min_norm = norm
best = bitmap
if norm == 0:
break
best[row][column] = 1
else:
for color, bitmap in mono_data:
if numpy.array_equal(color, self.data[row][column]):
bitmap[row][column] = 1
break
return mono_data
def mono(self):
"""!
Returns a bit mask of opaque pixels
"""
mono_data = numpy.zeros(self.data.shape[:2])
for row in range(self.data.shape[0]):
for column in range(self.data.shape[1]):
mono_data[row][column] = int(self.data[row][column][3] == 255)
return mono_data
class Vectorizer:
def __init__(self):
self.palette = None
self.layers = {}
def _create_layer(self, animation, layer_name):
layer = animation.add_layer(objects.ShapeLayer())
if layer_name:
self.layers[layer_name] = layer
layer.name = layer_name
return layer
def prepare_layer(self, animation, layer_name=None):
layer = self._create_layer(animation, layer_name)
layer._max_verts = {}
if self.palette is None:
group = layer.add_shape(objects.Group())
group.name = "bitmap"
layer._max_verts[group.name] = 0
group.add_shape(objects.Path())
group.add_shape(objects.Fill(NVector(0, 0, 0)))
else:
for color in self.palette:
group = layer.add_shape(objects.Group())
group.name = "color_%s" % "".join("%02x" % int(c) for c in color)
layer._max_verts[group.name] = 0
fcol = color/255
fill = group.add_shape(objects.Fill(NVector(*fcol)))
if len(fcol) > 3 and fcol[3] < 1:
fill.opacity.value = fcol[3] * 100
return layer
def raster_to_layer(self, animation, raster, layer_name=None, mode=QuanzationMode.Nearest):
layer = self.prepare_layer(animation, layer_name)
mono_data = raster.quantize(self.palette, mode)
for (color, bitmap), group in zip(mono_data, layer.shapes):
self.raster_to_shapes(group, bitmap)
return layer
def raster_to_shapes(self, group, mono_data):
shapes = []
for bezier in self.raster_to_bezier(mono_data):
shape = group.insert_shape(0, objects.Path())
shapes.append(shape)
shape.shape.value = bezier
return shapes
def raster_to_bezier(self, mono_data):
bmp = potrace.Bitmap(mono_data)
path = bmp.trace()
shapes = []
for curve in path:
bezier = objects.Bezier()
shapes.append(bezier)
bezier.add_point(NVector(*curve.start_point))
for segment in curve:
if segment.is_corner:
bezier.add_point(NVector(*segment.c))
bezier.add_point(NVector(*segment.end_point))
else:
sp = NVector(*bezier.vertices[-1])
ep = NVector(*segment.end_point)
c1 = NVector(*segment.c1) - sp
c2 = NVector(*segment.c2) - ep
bezier.out_tangents[-1] = c1
bezier.add_point(ep, c2)
return shapes
def _frame_keyframe(self, layer, group, time, shapes, beziers):
if shapes:
# TODO handle multiple shapes
nverts = len(beziers[0].vertices)
if nverts > layer._max_verts[group.name]:
layer._max_verts[group.name] = nverts
for shape, bezier in zip(shapes, beziers):
shape.shape.add_keyframe(time, bezier)
def raster_to_frame(self, animation, raster, layer_name, time, mode=QuanzationMode.Nearest):
mono_data = raster.quantize(self.palette, mode)
if layer_name not in self.layers:
layer = self.prepare_layer(animation, layer_name)
for (color, bitmap), group in zip(mono_data, layer.shapes):
shapes = self.raster_to_shapes(group, bitmap)
beziers = [s.shape.value for s in shapes]
self._frame_keyframe(layer, group, time, shapes, beziers)
else:
layer = self.layers[layer_name]
for (color, bitmap), group in zip(mono_data, layer.shapes):
shapes = [s for s in group.shapes if isinstance(s, objects.Path)]
beziers = self.raster_to_bezier(bitmap)
self._frame_keyframe(layer, group, time, shapes, beziers)
def adjust_missing_vertices(self, layer_name):
layer = self.layers[layer_name]
for group in layer.shapes:
# TODO handle multiple shapes
shape = group.shapes[0]
nverts = layer._max_verts[group.name]
if shape.shape.animated:
for kf in shape.shape.keyframes:
bezier = kf.start
count = nverts - len(bezier.vertices)
bezier.vertices += [bezier.vertices[-1]] * count
bezier.in_tangents += [NVector(0, 0)] * count
bezier.out_tangents += [NVector(0, 0)] * count
def duplicate_start_frame(self, layer_name, time):
layer = self.layers[layer_name]
for group in layer.shapes:
shape = group.shapes[0]
bezier = shape.shape.keyframes[0].start
group.shapes[0].shape.add_keyframe(time, bezier)
def color2numpy(vcolor):
l = (vcolor * 255).components
if len(l) == 3:
l.append(255)
return numpy.array(l, numpy.uint8)
def raster_to_animation(filenames, n_colors=1, frame_delay=1,
looping=True, framerate=60, palette=[],
mode=QuanzationMode.Nearest):
vc = Vectorizer()
def callback(animation, raster, frame):
raster = RasterImage.from_pil(raster)
if vc.palette is None:
if palette:
vc.palette = [color2numpy(c) for c in palette]
elif n_colors > 1:
vc.palette = raster.k_means(n_colors)
#vc.raster_to_frame(animation, raster, "anim", frame * frame_delay, mode)
layer = vc.raster_to_layer(animation, raster, "frame_%s" % frame, mode)
layer.in_point = frame * frame_delay
layer.out_point = (frame + 1) * frame_delay
animation = _vectorizing_func(filenames, frame_delay, framerate, callback)
#vc.adjust_missing_vertices("anim")
#if looping and animation._nframes > 1:
#animation.out_point += frame_delay
#vc.duplicate_start_frame("anim", animation.out_point)
#elif animation._nframes == 1:
#for g in animation.find("anim").shapes:
#for shape in g.find_all(objects.Path):
#shape.shape.clear_animation(shape.shape.get_value(0))
#animation.find("anim").out_point = animation.out_point
return animation
+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
+3
View File
@@ -0,0 +1,3 @@
from .importer import parse_svg_etree, parse_svg_file
from . import builder, importer
__all__ = ["builder", "importer", "parse_svg_etree", "parse_svg_file"]
+719
View File
@@ -0,0 +1,719 @@
import re
import math
from xml.etree import ElementTree
from .handler import SvgHandler, NameMode
from ... import objects
from ...nvector import NVector
from ...utils import restructure
from ...utils.transform import TransformMatrix
try:
from ...utils import font
has_font = True
except ImportError:
has_font = False
class PrecompTime:
def __init__(self, pcl: objects.PreCompLayer):
self.pcl = pcl
def get_time_offset(self, time, lot):
remap = time
if self.pcl.time_remapping:
remapf = self.pcl.time_remapping.get_value(time)
remap = lot.in_point * (1-remapf) + lot.out_point * remapf
return remap - self.pcl.start_time
class SvgBuilder(SvgHandler, restructure.AbstractBuilder):
merge_paths = True
namestart = (
r":_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF" +
r"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" +
r"\uFDF0-\uFFFD\U00010000-\U000EFFFF"
)
namenostart = r"-.0-9\xB7\u0300-\u036F\u203F-\u2040"
id_re = re.compile("^[%s][%s%s]*$" % (namestart, namenostart, namestart))
def __init__(self, time=0):
super().__init__()
self.svg = ElementTree.Element("svg")
self.dom = ElementTree.ElementTree(self.svg)
self.svg.attrib["xmlns"] = self.ns_map["svg"]
self.ids = set()
self.idc = 0
self.name_mode = NameMode.Inkscape
self.actual_time = time
self.precomp_times = []
self._precomps = {}
self._assets = {}
self._current_layer = []
@property
def time(self):
time = self.actual_time
if self.precomp_times:
for pct in self.precomp_times:
time = pct.get_time_offset(time, self._current_layer[-1])
return time
def gen_id(self, prefix="id"):
while True:
self.idc += 1
id = "%s_%s" % (prefix, self.idc)
if id not in self.ids:
break
self.ids.add(id)
return id
def set_clean_id(self, dom, n):
idn = n.replace(" ", "_")
if self.id_re.match(idn) and idn not in self.ids:
self.ids.add(idn)
else:
idn = self.gen_id(dom.tag)
dom.attrib["id"] = idn
return idn
def set_id(self, dom, lottieobj, inkscape_qual=None, force=False):
n = getattr(lottieobj, "name", None)
if n is None or self.name_mode == NameMode.NoName:
if force:
id = self.gen_id(dom.tag)
dom.attrib["id"] = id
return id
return None
idn = self.set_clean_id(dom, n)
if inkscape_qual is None:
inkscape_qual = self.qualified("inkscape", "label")
if inkscape_qual:
dom.attrib[inkscape_qual] = n
return idn
def _on_animation(self, animation: objects.Animation):
self.svg.attrib["width"] = str(animation.width)
self.svg.attrib["height"] = str(animation.height)
self.svg.attrib["viewBox"] = "0 0 %s %s" % (animation.width, animation.height)
self.svg.attrib["version"] = "1.1"
self.set_id(self.svg, animation, self.qualified("sodipodi", "docname"))
self.defs = ElementTree.SubElement(self.svg, "defs")
if self.name_mode == NameMode.Inkscape:
self.svg.attrib[self.qualified("inkscape", "export-xdpi")] = "96"
self.svg.attrib[self.qualified("inkscape", "export-ydpi")] = "96"
namedview = ElementTree.SubElement(self.svg, self.qualified("sodipodi", "namedview"))
namedview.attrib[self.qualified("inkscape", "pagecheckerboard")] = "true"
namedview.attrib["borderlayer"] = "true"
namedview.attrib["bordercolor"] = "#666666"
namedview.attrib["pagecolor"] = "#ffffff"
self.svg.attrib["style"] = "fill: none; stroke: none"
self._current_layer = [animation]
return self.svg
def _mask_to_def(self, mask):
svgmask = ElementTree.SubElement(self.defs, "mask")
mask_id = self.gen_id()
svgmask.attrib["id"] = mask_id
svgmask.attrib["mask-type"] = "alpha"
path = ElementTree.SubElement(svgmask, "path")
path.attrib["d"] = self._bezier_to_d(mask.shape.get_value(self.time))
path.attrib["fill"] = "#fff"
path.attrib["fill-opacity"] = str(mask.opacity.get_value(self.time) / 100)
return mask_id
def _matte_source_to_def(self, layer_builder):
svgmask = ElementTree.SubElement(self.defs, "mask")
if not layer_builder.matte_id:
layer_builder.matte_id = self.gen_id()
svgmask.attrib["id"] = layer_builder.matte_id
matte_mode = layer_builder.matte_target.lottie.matte_mode
mask_type = "alpha"
if matte_mode == objects.MatteMode.Luma:
mask_type = "luminance"
svgmask.attrib["mask-type"] = mask_type
return svgmask
def _on_masks(self, masks):
if len(masks) == 1:
return self._mask_to_def(masks[0])
mask_ids = list(map(self._mask_to_def, masks))
mask_def = ElementTree.SubElement(self.defs, "mask")
mask_id = self.gen_id()
mask_def.attrib["id"] = mask_id
g = mask_def
for mid in mask_ids:
g = ElementTree.SubElement(g, "g")
g.attrib["mask"] = "url(#%s)" % mid
full = ElementTree.SubElement(g, "rect")
full.attrib["fill"] = "#fff"
full.attrib["width"] = self.svg.attrib["width"]
full.attrib["height"] = self.svg.attrib["height"]
full.attrib["x"] = "0"
full.attrib["y"] = "0"
return mask_id
def _on_layer(self, layer_builder, dom_parent):
lot = layer_builder.lottie
self._current_layer.append(lot)
if not self.precomp_times and (lot.in_point > self.time or lot.out_point < self.time):
self._current_layer.pop()
return None
if layer_builder.matte_target:
dom_parent = self._matte_source_to_def(layer_builder)
g = self.group_from_lottie(lot, dom_parent, True)
if lot.masks:
g.attrib["mask"] = "url(#%s)" % self._on_masks(lot.masks)
elif layer_builder.matte_source:
matte_id = layer_builder.matte_source.matte_id
if not matte_id:
matte_id = layer_builder.matte_source.matte_id = self.gen_id()
g.attrib["mask"] = "url(#%s)" % matte_id
if isinstance(lot, objects.PreCompLayer):
self.precomp_times.append(PrecompTime(lot))
for layer in self._precomps.get(lot.reference_id, []):
self.process_layer(layer, g)
self.precomp_times.pop()
elif isinstance(lot, objects.NullLayer):
g.attrib["opacity"] = "1"
elif isinstance(lot, objects.ImageLayer):
use = ElementTree.SubElement(g, "use")
use.attrib[self.qualified("xlink", "href")] = "#" + self._assets[lot.image_id]
elif isinstance(lot, objects.TextLayer):
self._on_text_layer(g, lot)
elif isinstance(lot, objects.SolidColorLayer):
rect = ElementTree.SubElement(g, "rect")
rect.attrib["width"] = str(lot.width)
rect.attrib["height"] = str(lot.height)
rect.attrib["fill"] = lot.color
if not lot.name:
g.attrib[self.qualified("inkscape", "label")] = lot.__class__.__name__
if layer_builder.shapegroup:
g.attrib["style"] = self.group_to_style(layer_builder.shapegroup)
self._split_stroke(layer_builder.shapegroup, g, dom_parent)
#if lot.hidden:
#g.attrib.setdefault("style", "")
#g.attrib["style"] += "display: none;"
return g
def _on_text_layer(self, g, lot):
text = ElementTree.SubElement(g, "text")
doc = lot.data.get_value(self.time)
if doc:
text.attrib["font-family"] = doc.font_family
text.attrib["font-size"] = str(doc.font_size)
if doc.line_height:
text.attrib["line-height"] = "%s%%" % doc.line_height
if doc.justify == objects.text.TextJustify.Left:
text.attrib["text-align"] = "start"
elif doc.justify == objects.text.TextJustify.Center:
text.attrib["text-align"] = "center"
elif doc.justify == objects.text.TextJustify.Right:
text.attrib["text-align"] = "end"
text.attrib["fill"] = color_to_css(doc.color)
text.text = doc.text
def _on_layer_end(self, out_layer):
self._current_layer.pop()
def _on_precomp(self, id, dom_parent, layers):
self._precomps[id] = layers
def _on_asset(self, asset):
if isinstance(asset, objects.assets.Image):
img = ElementTree.SubElement(self.defs, "image")
xmlid = self.set_clean_id(img, asset.id)
self._assets[asset.id] = xmlid
if asset.is_embedded:
url = asset.image
else:
url = asset.image_path + asset.image
img.attrib[self.qualified("xlink", "href")] = url
img.attrib["width"] = str(asset.width)
img.attrib["height"] = str(asset.height)
def _get_value(self, prop, default=NVector(0, 0)):
if prop:
v = prop.get_value(self.time)
else:
v = default
if v is None:
return default
if isinstance(v, NVector):
return v.clone()
return v
def set_transform(self, dom, transform, auto_orient=False):
mat = transform.to_matrix(self.time, auto_orient)
dom.attrib["transform"] = mat.to_css_2d()
if transform.opacity is not None:
op = transform.opacity.get_value(self.time)
if op != 100:
dom.attrib["opacity"] = str(op/100)
def _get_group_stroke(self, group):
style = {}
if group.stroke:
if isinstance(group.stroke, objects.GradientStroke):
style["stroke"] = "url(#%s)" % self.process_gradient(group.stroke)
else:
style["stroke"] = color_to_css(group.stroke.color.get_value(self.time))
style["stroke-opacity"] = group.stroke.opacity.get_value(self.time) / 100
style["stroke-width"] = group.stroke.width.get_value(self.time)
if group.stroke.miter_limit is not None:
style["stroke-miterlimit"] = group.stroke.miter_limit
if group.stroke.line_cap == objects.LineCap.Round:
style["stroke-linecap"] = "round"
elif group.stroke.line_cap == objects.LineCap.Butt:
style["stroke-linecap"] = "butt"
elif group.stroke.line_cap == objects.LineCap.Square:
style["stroke-linecap"] = "square"
if group.stroke.line_join == objects.LineJoin.Round:
style["stroke-linejoin"] = "round"
elif group.stroke.line_join == objects.LineJoin.Bevel:
style["stroke-linejoin"] = "bevel"
elif group.stroke.line_join == objects.LineJoin.Miter:
style["stroke-linejoin"] = "miter"
if group.stroke.dashes:
dasharray = []
last = 0
last_mode = objects.StrokeDashType.Dash
for dash in group.stroke.dashes:
if last_mode == dash.type:
last += dash.length.get_value(self.time)
else:
if last_mode != objects.StrokeDashType.Offset:
dasharray.append(str(last))
last = 0
last_mode = dash.type
style["stroke-dasharray"] = " ".join(dasharray)
return style
def _style_to_css(self, style):
return ";".join(map(
lambda x: ":".join(map(str, x)),
style.items()
))
def _split_stroke(self, group, fill_layer, out_parent):
if not group.stroke:# or group.stroke_above:
return
style = self._get_group_stroke(group)
if style.get("stroke-width", 0) <= 0 or style["stroke-opacity"] <= 0:
return
if group.stroke_above:
if fill_layer.attrib.get("style", ""):
fill_layer.attrib["style"] += ";"
else:
fill_layer.attrib["style"] = ""
fill_layer.attrib["style"] += self._style_to_css(style)
return fill_layer
g = ElementTree.Element("g")
self.set_clean_id(g, "stroke")
use = ElementTree.Element("use")
for i, e in enumerate(out_parent):
if e is fill_layer:
out_parent.insert(i, g)
out_parent.remove(fill_layer)
break
else:
return
g.append(use)
g.append(fill_layer)
use.attrib[self.qualified("xlink", "href")] = "#" + fill_layer.attrib["id"]
use.attrib["style"] = self._style_to_css(style)
return g
def group_to_style(self, group):
style = {}
if group.fill:
style["fill-opacity"] = group.fill.opacity.get_value(self.time) / 100
if isinstance(group.fill, objects.GradientFill):
style["fill"] = "url(#%s)" % self.process_gradient(group.fill)
else:
style["fill"] = color_to_css(group.fill.color.get_value(self.time))
if group.fill.fill_rule:
style["fill-rule"] = "evenodd" if group.fill.fill_rule == objects.FillRule.EvenOdd else "nonzero"
if group.lottie.hidden:
style["display"] = "none"
#if group.stroke_above:
#style.update(self._get_group_stroke(group))
return self._style_to_css(style)
def process_gradient(self, gradient):
spos = gradient.start_point.get_value(self.time)
epos = gradient.end_point.get_value(self.time)
if gradient.gradient_type == objects.GradientType.Linear:
dom = ElementTree.SubElement(self.defs, "linearGradient")
dom.attrib["x1"] = str(spos[0])
dom.attrib["y1"] = str(spos[1])
dom.attrib["x2"] = str(epos[0])
dom.attrib["y2"] = str(epos[1])
elif gradient.gradient_type == objects.GradientType.Radial:
dom = ElementTree.SubElement(self.defs, "radialGradient")
dom.attrib["cx"] = str(spos[0])
dom.attrib["cy"] = str(spos[1])
dom.attrib["r"] = str((epos-spos).length)
a = gradient.highlight_angle.get_value(self.time) * math.pi / 180
l = gradient.highlight_length.get_value(self.time)
dom.attrib["fx"] = str(spos[0] + math.cos(a) * l)
dom.attrib["fy"] = str(spos[1] + math.sin(a) * l)
id = self.set_id(dom, gradient, force=True)
dom.attrib["gradientUnits"] = "userSpaceOnUse"
for off, color in gradient.colors.stops_at(self.time):
stop = ElementTree.SubElement(dom, "stop")
stop.attrib["offset"] = "%s%%" % (off * 100)
stop.attrib["stop-color"] = color_to_css(color[:3])
if len(color) > 3:
stop.attrib["stop-opacity"] = str(color[3])
return id
def group_from_lottie(self, lottie, dom_parent, layer):
g = ElementTree.SubElement(dom_parent, "g")
if layer and self.name_mode == NameMode.Inkscape:
g.attrib[self.qualified("inkscape", "groupmode")] = "layer"
self.set_id(g, lottie, force=True)
self.set_transform(g, lottie.transform, getattr(lottie, "auto_orient", False))
return g
def _on_shapegroup(self, group, dom_parent):
if group.empty():
return
if len(group.children) == 1 and isinstance(group.children[0], restructure.RestructuredPathMerger):
path = self.build_path(group.paths.paths, dom_parent)
self.set_id(path, group.paths.paths[0], force=True)
path.attrib["style"] = self.group_to_style(group)
self.set_transform(path, group.lottie.transform)
return self._split_stroke(group, path, dom_parent)
g = self.group_from_lottie(group.lottie, dom_parent, group.layer)
g.attrib["style"] = self.group_to_style(group)
self.shapegroup_process_children(group, g)
return self._split_stroke(group, g, dom_parent)
def _on_merged_path(self, shape, shapegroup, out_parent):
path = self.build_path(shape.paths, out_parent)
self.set_id(path, shape.paths[0])
path.attrib["style"] = self.group_to_style(shapegroup)
#self._split_stroke(shapegroup, path, out_parent)
return path
def _on_shape(self, shape, shapegroup, out_parent):
if isinstance(shape, objects.Rect):
svgshape = self.build_rect(shape, out_parent)
elif isinstance(shape, objects.Ellipse):
svgshape = self.build_ellipse(shape, out_parent)
elif isinstance(shape, objects.Star):
svgshape = self.build_path([shape.to_bezier()], out_parent)
elif isinstance(shape, objects.Path):
svgshape = self.build_path([shape], out_parent)
elif has_font and isinstance(shape, font.FontShape):
svgshape = self.build_text(shape, out_parent)
else:
return
self.set_id(svgshape, shape, force=True)
if "style" not in svgshape.attrib:
svgshape.attrib["style"] = ""
svgshape.attrib["style"] += self.group_to_style(shapegroup)
#self._split_stroke(shapegroup, svgshape, out_parent)
if shape.hidden:
svgshape.attrib["style"] += "display: none;"
return svgshape
def build_rect(self, shape, parent):
rect = ElementTree.SubElement(parent, "rect")
size = shape.size.get_value(self.time)
pos = shape.position.get_value(self.time)
rect.attrib["width"] = str(size[0])
rect.attrib["height"] = str(size[1])
rect.attrib["x"] = str(pos[0] - size[0] / 2)
rect.attrib["y"] = str(pos[1] - size[1] / 2)
rect.attrib["rx"] = str(shape.rounded.get_value(self.time))
return rect
def build_ellipse(self, shape, parent):
ellipse = ElementTree.SubElement(parent, "ellipse")
size = shape.size.get_value(self.time)
pos = shape.position.get_value(self.time)
ellipse.attrib["rx"] = str(size[0] / 2)
ellipse.attrib["ry"] = str(size[1] / 2)
ellipse.attrib["cx"] = str(pos[0])
ellipse.attrib["cy"] = str(pos[1])
return ellipse
def build_path(self, shapes, parent):
path = ElementTree.SubElement(parent, "path")
d = ""
for shape in shapes:
bez = shape.shape.get_value(self.time)
if isinstance(bez, list):
bez = bez[0]
if not bez.vertices:
continue
if d:
d += "\n"
d += self._bezier_to_d(bez)
path.attrib["d"] = d
return path
def _bezier_tangent(self, tangent):
_tangent_threshold = 0.5
if tangent.length < _tangent_threshold:
return NVector(0, 0)
return tangent
def _bezier_to_d(self, bez):
d = "M %s,%s " % tuple(bez.vertices[0].components[:2])
for i in range(1, len(bez.vertices)):
qfrom = bez.vertices[i-1]
h1 = self._bezier_tangent(bez.out_tangents[i-1]) + qfrom
qto = bez.vertices[i]
h2 = self._bezier_tangent(bez.in_tangents[i]) + qto
d += "C %s,%s %s,%s %s,%s " % (
h1[0], h1[1],
h2[0], h2[1],
qto[0], qto[1],
)
if bez.closed:
qfrom = bez.vertices[-1]
h1 = self._bezier_tangent(bez.out_tangents[-1]) + qfrom
qto = bez.vertices[0]
h2 = self._bezier_tangent(bez.in_tangents[0]) + qto
d += "C %s,%s %s,%s %s,%s Z" % (
h1[0], h1[1],
h2[0], h2[1],
qto[0], qto[1],
)
return d
def _on_shape_modifier(self, shape, shapegroup, out_parent):
if isinstance(shape.lottie, objects.Repeater):
svgshape = self.build_repeater(shape.lottie, shape.child, shapegroup, out_parent)
elif isinstance(shape.lottie, objects.RoundedCorners):
svgshape = self.build_rouded_corners(shape.lottie, shape.child, shapegroup, out_parent)
elif isinstance(shape.lottie, objects.Trim):
svgshape = self.build_trim_path(shape.lottie, shape.child, shapegroup, out_parent)
else:
return self.shapegroup_process_child(shape.child, shapegroup, out_parent)
return svgshape
def build_repeater(self, shape, child, shapegroup, out_parent):
original = self.shapegroup_process_child(child, shapegroup, out_parent)
if not original:
return
ncopies = int(round(shape.copies.get_value(self.time)))
if ncopies == 1:
return
out_parent.remove(original)
g = ElementTree.SubElement(out_parent, "g")
self.set_clean_id(g, "repeater")
for copy in range(ncopies-1):
use = ElementTree.SubElement(g, "use")
use.attrib[self.qualified("xlink", "href")] = "#" + original.attrib["id"]
orig_wrapper = ElementTree.SubElement(g, "g")
orig_wrapper.append(original)
transform = objects.Transform()
so = shape.transform.start_opacity.get_value(self.time)
eo = shape.transform.end_opacity.get_value(self.time)
position = shape.transform.position.get_value(self.time)
rotation = shape.transform.rotation.get_value(self.time)
anchor_point = shape.transform.anchor_point.get_value(self.time)
for i in range(ncopies-1, -1, -1):
of = i / (ncopies-1)
transform.opacity.value = so * of + eo * (1 - of)
self.set_transform(g[i], transform)
transform.position.value += position
transform.rotation.value += rotation
transform.anchor_point.value += anchor_point
return g
def build_rouded_corners(self, shape, child, shapegroup, out_parent):
round_amount = shape.radius.get_value(self.time)
return self._modifier_process(child, shapegroup, out_parent, self._build_rouded_corners_shape, round_amount)
def _build_rouded_corners_shape(self, shape, round_amount):
if not isinstance(shape, objects.Shape):
return [shape]
path = shape.to_bezier()
bezier = path.shape.get_value(self.time).rounded(round_amount)
path.shape.clear_animation(bezier)
return [path]
def build_trim_path(self, shape, child, shapegroup, out_parent):
start = max(0, min(1, shape.start.get_value(self.time) / 100))
end = max(0, min(1, shape.end.get_value(self.time) / 100))
offset = shape.offset.get_value(self.time) / 360 % 1
multidata = {}
length = 0
if shape.multiple == objects.TrimMultipleShapes.Individually:
for visishape in reversed(list(self._modifier_foreach_shape(child))):
bez = visishape.to_bezier().shape.get_value(self.time)
local_length = bez.rough_length()
multidata[visishape] = (bez, length, local_length)
length += local_length
return self._modifier_process(
child, shapegroup, out_parent, self._build_trim_path_shape,
start+offset, end+offset, multidata, length
)
def _modifier_foreach_shape(self, shape):
if isinstance(shape, restructure.RestructuredShapeGroup):
for child in shape.children:
for chsh in self._modifier_foreach_shape(child):
yield chsh
elif isinstance(shape, restructure.RestructuredPathMerger):
for p in shape.paths:
yield p
elif isinstance(shape, objects.Shape):
yield shape
def _modifier_process(self, child, shapegroup, out_parent, callback, *args):
children = self._modifier_process_child(child, shapegroup, out_parent, callback, *args)
return [self.shapegroup_process_child(ch, shapegroup, out_parent) for ch in children]
def _trim_offlocal(self, t, local_start, local_length, total_length):
gt = (t * total_length - local_start) / local_length
return max(0, min(1, gt))
def _build_trim_path_shape(self, shape, start, end, multidata, total_length):
if not isinstance(shape, objects.Shape):
return [shape]
if multidata:
bezier, local_start, local_length = multidata[shape]
if end > 1:
lstart = self._trim_offlocal(start, local_start, local_length, total_length)
lend = self._trim_offlocal(end-1, local_start, local_length, total_length)
out = []
if lstart < 1:
out.append(objects.Path(bezier.segment(lstart, 1)))
if lend > 0:
out.append(objects.Path(bezier.segment(0, lend)))
return out
lstart = self._trim_offlocal(start, local_start, local_length, total_length)
lend = self._trim_offlocal(end, local_start, local_length, total_length)
if lend <= 0 or lstart >= 1:
return []
if lstart <= 0 and lend >= 1:
return [objects.Path(bezier)]
seg = bezier.segment(lstart, lend)
return [objects.Path(seg)]
path = shape.to_bezier()
bezier = path.shape.get_value(self.time)
if end > 1:
bez1 = bezier.segment(start, 1)
bez2 = bezier.segment(0, end-1)
return [objects.Path(bez1), objects.Path(bez2)]
else:
seg = bezier.segment(start, end)
return [objects.Path(seg)]
def _modifier_process_children(self, shapegroup, out_parent, callback, *args):
children = []
for shape in shapegroup.children:
children.extend(self._modifier_process_child(shape, shapegroup, out_parent, callback, *args))
shapegroup.children = children
def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args):
if isinstance(shape, restructure.RestructuredShapeGroup):
self._modifier_process_children(shape, out_parent, callback, *args)
return [shape]
elif isinstance(shape, restructure.RestructuredPathMerger):
paths = []
for p in shape.paths:
paths.extend(callback(p, *args))
shape.paths = paths
if paths:
return [shape]
return []
else:
return callback(shape, *args)
def _custom_object_supported(self, shape):
if has_font and isinstance(shape, font.FontShape):
return True
return False
def build_text(self, shape, parent):
text = ElementTree.SubElement(parent, "text")
if "family" in shape.query:
text.attrib["font-family"] = shape.query["family"]
if "weight" in shape.query:
text.attrib["font-weight"] = str(shape.query.weight_to_css())
slant = int(shape.query.get("slant", 0))
if slant > 0 and slant < 110:
text.attrib["font-style"] = "italic"
elif slant >= 110:
text.attrib["font-style"] = "oblique"
text.attrib["font-size"] = str(shape.size)
text.attrib["white-space"] = "pre"
pos = shape.style.position
text.attrib["x"] = str(pos.x)
text.attrib["y"] = str(pos.y)
text.text = shape.text
return text
def color_to_css(color):
#if len(color) == 4:
#return ("rgba(%s, %s, %s" % tuple(map(lambda c: int(round(c*255)), color[:3]))) + ", %s)" % color[3]
return "rgb(%s, %s, %s)" % tuple(map(lambda c: int(round(c*255)), color[:3]))
def to_svg(animation, time):
builder = SvgBuilder(time)
builder.process(animation)
return builder.dom
+38
View File
@@ -0,0 +1,38 @@
import enum
from xml.etree import ElementTree
class SvgHandler:
ns_map = {
"dc": "http://purl.org/dc/elements/1.1/",
"cc": "http://creativecommons.org/ns#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"svg": "http://www.w3.org/2000/svg",
"sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
"inkscape": "http://www.inkscape.org/namespaces/inkscape",
"xlink": "http://www.w3.org/1999/xlink",
}
def init_etree(self):
for n, u in self.ns_map.items():
ElementTree.register_namespace(n, u)
def qualified(self, ns, name):
return "{%s}%s" % (self.ns_map[ns], name)
def simplified(self, name):
for k, v in self.ns_map.items():
name = name.replace("{%s}" % v, k+":")
return name
def unqualified(self, name):
return name.split("}")[-1]
def __init__(self):
self.init_etree()
class NameMode(enum.Enum):
NoName = 0
Id = 1
Inkscape = 2
File diff suppressed because it is too large Load Diff
+211
View File
@@ -0,0 +1,211 @@
color_table = {
"aliceblue": [0.9411764705882353, 0.9725490196078431, 1.0, 1],
"antiquewhite": [0.9803921568627451, 0.9215686274509803, 0.8431372549019608, 1],
"aqua": [0.0, 1.0, 1.0, 1],
"aquamarine": [0.4980392156862745, 1.0, 0.8313725490196079, 1],
"azure": [0.9411764705882353, 1.0, 1.0, 1],
"beige": [0.9607843137254902, 0.9607843137254902, 0.8627450980392157, 1],
"bisque": [1.0, 0.8941176470588236, 0.7686274509803922, 1],
"black": [0.0, 0.0, 0.0, 1],
"blanchedalmond": [1.0, 0.9215686274509803, 0.803921568627451, 1],
"blue": [0.0, 0.0, 1.0, 1],
"blueviolet": [0.5411764705882353, 0.16862745098039217, 0.8862745098039215, 1],
"brown": [0.6470588235294118, 0.16470588235294117, 0.16470588235294117, 1],
"burlywood": [0.8705882352941177, 0.7215686274509804, 0.5294117647058824, 1],
"cadetblue": [0.37254901960784315, 0.6196078431372549, 0.6274509803921569, 1],
"chartreuse": [0.4980392156862745, 1.0, 0.0, 1],
"chocolate": [0.8235294117647058, 0.4117647058823529, 0.11764705882352941, 1],
"coral": [1.0, 0.4980392156862745, 0.3137254901960784, 1],
"cornflowerblue": [0.39215686274509803, 0.5843137254901961, 0.9294117647058824, 1],
"cornsilk": [1.0, 0.9725490196078431, 0.8627450980392157, 1],
"crimson": [0.8627450980392157, 0.0784313725490196, 0.23529411764705882, 1],
"cyan": [0.0, 1.0, 1.0, 1],
"darkblue": [0.0, 0.0, 0.5450980392156862, 1],
"darkcyan": [0.0, 0.5450980392156862, 0.5450980392156862, 1],
"darkgoldenrod": [0.7215686274509804, 0.5254901960784314, 0.043137254901960784, 1],
"darkgray": [0.6627450980392157, 0.6627450980392157, 0.6627450980392157, 1],
"darkgreen": [0.0, 0.39215686274509803, 0.0, 1],
"darkgrey": [0.6627450980392157, 0.6627450980392157, 0.6627450980392157, 1],
"darkkhaki": [0.7411764705882353, 0.7176470588235294, 0.4196078431372549, 1],
"darkmagenta": [0.5450980392156862, 0.0, 0.5450980392156862, 1],
"darkolivegreen": [0.3333333333333333, 0.4196078431372549, 0.1843137254901961, 1],
"darkorange": [1.0, 0.5490196078431373, 0.0, 1],
"darkorchid": [0.6, 0.19607843137254902, 0.8, 1],
"darkred": [0.5450980392156862, 0.0, 0.0, 1],
"darksalmon": [0.9137254901960784, 0.5882352941176471, 0.47843137254901963, 1],
"darkseagreen": [0.5607843137254902, 0.7372549019607844, 0.5607843137254902, 1],
"darkslateblue": [0.2823529411764706, 0.23921568627450981, 0.5450980392156862, 1],
"darkslategray": [0.1843137254901961, 0.30980392156862746, 0.30980392156862746, 1],
"darkslategrey": [0.1843137254901961, 0.30980392156862746, 0.30980392156862746, 1],
"darkturquoise": [0.0, 0.807843137254902, 0.8196078431372549, 1],
"darkviolet": [0.5803921568627451, 0.0, 0.8274509803921568, 1],
"deeppink": [1.0, 0.0784313725490196, 0.5764705882352941, 1],
"deepskyblue": [0.0, 0.7490196078431373, 1.0, 1],
"dimgray": [0.4117647058823529, 0.4117647058823529, 0.4117647058823529, 1],
"dimgrey": [0.4117647058823529, 0.4117647058823529, 0.4117647058823529, 1],
"dodgerblue": [0.11764705882352941, 0.5647058823529412, 1.0, 1],
"firebrick": [0.6980392156862745, 0.13333333333333333, 0.13333333333333333, 1],
"floralwhite": [1.0, 0.9803921568627451, 0.9411764705882353, 1],
"forestgreen": [0.13333333333333333, 0.5450980392156862, 0.13333333333333333, 1],
"fuchsia": [1.0, 0.0, 1.0, 1],
"gainsboro": [0.8627450980392157, 0.8627450980392157, 0.8627450980392157, 1],
"ghostwhite": [0.9725490196078431, 0.9725490196078431, 1.0, 1],
"gold": [1.0, 0.8431372549019608, 0.0, 1],
"goldenrod": [0.8549019607843137, 0.6470588235294118, 0.12549019607843137, 1],
"gray": [0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 1],
"green": [0.0, 0.5019607843137255, 0.0, 1],
"greenyellow": [0.6784313725490196, 1.0, 0.1843137254901961, 1],
"grey": [0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 1],
"honeydew": [0.9411764705882353, 1.0, 0.9411764705882353, 1],
"hotpink": [1.0, 0.4117647058823529, 0.7058823529411765, 1],
"indianred": [0.803921568627451, 0.3607843137254902, 0.3607843137254902, 1],
"indigo": [0.29411764705882354, 0.0, 0.5098039215686274, 1],
"ivory": [1.0, 1.0, 0.9411764705882353, 1],
"khaki": [0.9411764705882353, 0.9019607843137255, 0.5490196078431373, 1],
"lavender": [0.9019607843137255, 0.9019607843137255, 0.9803921568627451, 1],
"lavenderblush": [1.0, 0.9411764705882353, 0.9607843137254902, 1],
"lawngreen": [0.48627450980392156, 0.9882352941176471, 0.0, 1],
"lemonchiffon": [1.0, 0.9803921568627451, 0.803921568627451, 1],
"lightblue": [0.6784313725490196, 0.8470588235294118, 0.9019607843137255, 1],
"lightcoral": [0.9411764705882353, 0.5019607843137255, 0.5019607843137255, 1],
"lightcyan": [0.8784313725490196, 1.0, 1.0, 1],
"lightgoldenrodyellow": [0.9803921568627451, 0.9803921568627451, 0.8235294117647058, 1],
"lightgray": [0.8274509803921568, 0.8274509803921568, 0.8274509803921568, 1],
"lightgreen": [0.5647058823529412, 0.9333333333333333, 0.5647058823529412, 1],
"lightgrey": [0.8274509803921568, 0.8274509803921568, 0.8274509803921568, 1],
"lightpink": [1.0, 0.7137254901960784, 0.7568627450980392, 1],
"lightsalmon": [1.0, 0.6274509803921569, 0.47843137254901963, 1],
"lightseagreen": [0.12549019607843137, 0.6980392156862745, 0.6666666666666666, 1],
"lightskyblue": [0.5294117647058824, 0.807843137254902, 0.9803921568627451, 1],
"lightslategray": [0.4666666666666667, 0.5333333333333333, 0.6, 1],
"lightslategrey": [0.4666666666666667, 0.5333333333333333, 0.6, 1],
"lightsteelblue": [0.6901960784313725, 0.7686274509803922, 0.8705882352941177, 1],
"lightyellow": [1.0, 1.0, 0.8784313725490196, 1],
"lime": [0.0, 1.0, 0.0, 1],
"limegreen": [0.19607843137254902, 0.803921568627451, 0.19607843137254902, 1],
"linen": [0.9803921568627451, 0.9411764705882353, 0.9019607843137255, 1],
"magenta": [1.0, 0.0, 1.0, 1],
"maroon": [0.5019607843137255, 0.0, 0.0, 1],
"mediumaquamarine": [0.4, 0.803921568627451, 0.6666666666666666, 1],
"mediumblue": [0.0, 0.0, 0.803921568627451, 1],
"mediumorchid": [0.7294117647058823, 0.3333333333333333, 0.8274509803921568, 1],
"mediumpurple": [0.5764705882352941, 0.4392156862745098, 0.8588235294117647, 1],
"mediumseagreen": [0.23529411764705882, 0.7019607843137254, 0.44313725490196076, 1],
"mediumslateblue": [0.4823529411764706, 0.40784313725490196, 0.9333333333333333, 1],
"mediumspringgreen": [0.0, 0.9803921568627451, 0.6039215686274509, 1],
"mediumturquoise": [0.2823529411764706, 0.8196078431372549, 0.8, 1],
"mediumvioletred": [0.7803921568627451, 0.08235294117647059, 0.5215686274509804, 1],
"midnightblue": [0.09803921568627451, 0.09803921568627451, 0.4392156862745098, 1],
"mintcream": [0.9607843137254902, 1.0, 0.9803921568627451, 1],
"mistyrose": [1.0, 0.8941176470588236, 0.8823529411764706, 1],
"moccasin": [1.0, 0.8941176470588236, 0.7098039215686275, 1],
"navajowhite": [1.0, 0.8705882352941177, 0.6784313725490196, 1],
"navy": [0.0, 0.0, 0.5019607843137255, 1],
"oldlace": [0.9921568627450981, 0.9607843137254902, 0.9019607843137255, 1],
"olive": [0.5019607843137255, 0.5019607843137255, 0.0, 1],
"olivedrab": [0.4196078431372549, 0.5568627450980392, 0.13725490196078433, 1],
"orange": [1.0, 0.6470588235294118, 0.0, 1],
"orangered": [1.0, 0.27058823529411763, 0.0, 1],
"orchid": [0.8549019607843137, 0.4392156862745098, 0.8392156862745098, 1],
"palegoldenrod": [0.9333333333333333, 0.9098039215686274, 0.6666666666666666, 1],
"palegreen": [0.596078431372549, 0.984313725490196, 0.596078431372549, 1],
"paleturquoise": [0.6862745098039216, 0.9333333333333333, 0.9333333333333333, 1],
"palevioletred": [0.8588235294117647, 0.4392156862745098, 0.5764705882352941, 1],
"papayawhip": [1.0, 0.9372549019607843, 0.8352941176470589, 1],
"peachpuff": [1.0, 0.8549019607843137, 0.7254901960784313, 1],
"peru": [0.803921568627451, 0.5215686274509804, 0.24705882352941178, 1],
"pink": [1.0, 0.7529411764705882, 0.796078431372549, 1],
"plum": [0.8666666666666667, 0.6274509803921569, 0.8666666666666667, 1],
"powderblue": [0.6901960784313725, 0.8784313725490196, 0.9019607843137255, 1],
"purple": [0.5019607843137255, 0.0, 0.5019607843137255, 1],
"red": [1.0, 0.0, 0.0, 1],
"rosybrown": [0.7372549019607844, 0.5607843137254902, 0.5607843137254902, 1],
"royalblue": [0.2549019607843137, 0.4117647058823529, 0.8823529411764706, 1],
"saddlebrown": [0.5450980392156862, 0.27058823529411763, 0.07450980392156863, 1],
"salmon": [0.9803921568627451, 0.5019607843137255, 0.4470588235294118, 1],
"sandybrown": [0.9568627450980393, 0.6431372549019608, 0.3764705882352941, 1],
"seagreen": [0.1803921568627451, 0.5450980392156862, 0.3411764705882353, 1],
"seashell": [1.0, 0.9607843137254902, 0.9333333333333333, 1],
"sienna": [0.6274509803921569, 0.3215686274509804, 0.17647058823529413, 1],
"silver": [0.7529411764705882, 0.7529411764705882, 0.7529411764705882, 1],
"skyblue": [0.5294117647058824, 0.807843137254902, 0.9215686274509803, 1],
"slateblue": [0.41568627450980394, 0.35294117647058826, 0.803921568627451, 1],
"slategray": [0.4392156862745098, 0.5019607843137255, 0.5647058823529412, 1],
"slategrey": [0.4392156862745098, 0.5019607843137255, 0.5647058823529412, 1],
"snow": [1.0, 0.9803921568627451, 0.9803921568627451, 1],
"springgreen": [0.0, 1.0, 0.4980392156862745, 1],
"steelblue": [0.27450980392156865, 0.5098039215686274, 0.7058823529411765, 1],
"tan": [0.8235294117647058, 0.7058823529411765, 0.5490196078431373, 1],
"teal": [0.0, 0.5019607843137255, 0.5019607843137255, 1],
"thistle": [0.8470588235294118, 0.7490196078431373, 0.8470588235294118, 1],
"tomato": [1.0, 0.38823529411764707, 0.2784313725490196, 1],
"turquoise": [0.25098039215686274, 0.8784313725490196, 0.8156862745098039, 1],
"violet": [0.9333333333333333, 0.5098039215686274, 0.9333333333333333, 1],
"wheat": [0.9607843137254902, 0.8705882352941177, 0.7019607843137254, 1],
"white": [1.0, 1.0, 1.0, 1],
"whitesmoke": [0.9607843137254902, 0.9607843137254902, 0.9607843137254902, 1],
"yellow": [1.0, 1.0, 0.0, 1],
"yellowgreen": [0.6039215686274509, 0.803921568627451, 0.19607843137254902, 1],
}
css_atrrs = {
"fill",
"alignment-baseline",
"baseline-shift",
"clip-path",
"clip-rule",
"color",
"color-interpolation",
"color-interpolation-filters",
"color-rendering",
"cursor",
"direction",
"display",
"dominant-baseline",
"fill-opacity",
"fill-rule",
"filter",
"flood-color",
"flood-opacity",
"font-family",
"font-size",
"font-size-adjust",
"font-stretch",
"font-style",
"font-variant",
"font-weight",
"glyph-orientation-horizontal",
"glyph-orientation-vertical",
"image-rendering",
"letter-spacing",
"lighting-color",
"marker-end",
"marker-mid",
"marker-start",
"mask",
"opacity",
"overflow",
"paint-order",
"pointer-events",
"shape-rendering",
"stop-color",
"stop-opacity",
"stroke",
"stroke-dasharray",
"stroke-dashoffset",
"stroke-linecap",
"stroke-linejoin",
"stroke-miterlimit",
"stroke-opacity",
"stroke-width",
"text-anchor",
"text-decoration",
"text-overflow",
"text-rendering",
"unicode-bidi",
"vector-effect",
"visibility",
"white-space",
"word-spacing",
"writing-mode"
}
+41
View File
@@ -0,0 +1,41 @@
import io
import json
import gzip
from ..objects import Animation
def parse_tgs_json(file):
"""!
Reads both tgs and lottie files, returns the json structure
"""
return open_maybe_gzipped(file, json.load)
def open_maybe_gzipped(file, on_open):
if isinstance(file, str):
with open(file, "r") as fileobj:
return open_maybe_gzipped(fileobj, on_open)
if isinstance(file, io.TextIOBase):
binfile = file.buffer
else:
binfile = file
mn = binfile.read(2)
binfile.seek(0)
if mn == b'\x1f\x8b': # gzip magic number
final_file = gzip.open(binfile, "rb")
elif isinstance(file, io.TextIOBase):
final_file = file
else:
final_file = io.TextIOWrapper(file)
return on_open(final_file)
def parse_tgs(filename):
"""!
Reads both tgs and lottie files
"""
lottie = parse_tgs_json(filename)
return Animation.load(lottie)