mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-18 08:06:26 +00:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import svg, tgs, sif
|
||||
__all__ = ["svg", "tgs", "sif"]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
from .sif.nodes import *
|
||||
from . import ast
|
||||
from .ast_impl.base import SifKeyframe, Interpolation
|
||||
@@ -0,0 +1,2 @@
|
||||
from .ast_impl.nodes import *
|
||||
from .ast_impl.base import *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -0,0 +1,9 @@
|
||||
import enum
|
||||
|
||||
|
||||
class Smooth(enum.Enum):
|
||||
NearestNeighbour = 0
|
||||
Linear = 1
|
||||
Cosine = 2
|
||||
Spline = 3
|
||||
Cubic = 4
|
||||
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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")))
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user