mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-17 07:36:27 +00:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
from . import base, core, sif, svg, pretty_print
|
||||
|
||||
from .base import exporters
|
||||
from .core import export_lottie, export_tgs, export_embedded_html
|
||||
from .pretty_print import prettyprint, prettyprint_summary
|
||||
from .sif import export_sif
|
||||
from .svg import export_svg
|
||||
|
||||
__all__ = [
|
||||
"base", "core", "sif", "svg", "pretty_print",
|
||||
"exporters", "export_lottie", "export_tgs", "export_embedded_html",
|
||||
"prettyprint", "prettyprint_summary", "export_sif", "export_svg",
|
||||
]
|
||||
|
||||
try:
|
||||
from . import cairo, gif
|
||||
__all__ += ["cairo", "gif"]
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
from ..parsers.baseporter import Baseporter, Loader, ExtraOption, io_progress
|
||||
|
||||
|
||||
class ExporterLoader(Loader):
|
||||
def __init__(self):
|
||||
super().__init__(__file__, __name__, "export")
|
||||
|
||||
@property
|
||||
def exporters(self):
|
||||
return self.items
|
||||
|
||||
def set_options(self, parser):
|
||||
group = parser.add_argument_group("Generic output options")
|
||||
group.add_argument(
|
||||
"--pretty", "-p",
|
||||
action="store_true",
|
||||
help="Pretty print (for formats that support it)",
|
||||
)
|
||||
group.add_argument(
|
||||
"--frame",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Frame to extract (for single-image formats)",
|
||||
)
|
||||
|
||||
super().set_options(parser)
|
||||
|
||||
return group
|
||||
|
||||
|
||||
exporters = ExporterLoader()
|
||||
exporter = exporters.decorator
|
||||
@@ -0,0 +1,27 @@
|
||||
import cairosvg
|
||||
import io
|
||||
|
||||
from .base import exporter
|
||||
from .svg import export_svg
|
||||
|
||||
|
||||
def _export_cairo(func, animation, fp, frame, dpi):
|
||||
intermediate = io.StringIO()
|
||||
export_svg(animation, intermediate, frame)
|
||||
intermediate.seek(0)
|
||||
func(file_obj=intermediate, write_to=fp, dpi=dpi)
|
||||
|
||||
|
||||
@exporter("PNG", ["png"], [], {"frame"})
|
||||
def export_png(animation, fp, frame=0, dpi=96):
|
||||
_export_cairo(cairosvg.svg2png, animation, fp, frame, dpi)
|
||||
|
||||
|
||||
@exporter("PDF", ["pdf"], [], {"frame"})
|
||||
def export_pdf(animation, fp, frame=0, dpi=96):
|
||||
_export_cairo(cairosvg.svg2pdf, animation, fp, frame, dpi)
|
||||
|
||||
|
||||
@exporter("PostScript", ["ps"], [], {"frame"})
|
||||
def export_ps(animation, fp, frame=0, dpi=96):
|
||||
_export_cairo(cairosvg.svg2ps, animation, fp, frame, dpi)
|
||||
@@ -0,0 +1,119 @@
|
||||
import sys
|
||||
import json
|
||||
import gzip
|
||||
import codecs
|
||||
|
||||
from .base import exporter
|
||||
from ..utils.file import open_file
|
||||
from ..parsers.baseporter import ExtraOption
|
||||
from .tgs_validator import TgsValidator
|
||||
|
||||
|
||||
@exporter("Lottie JSON", ["json"], [], {"pretty"}, "lottie")
|
||||
def export_lottie(animation, file, pretty=False):
|
||||
with open_file(file) as fp:
|
||||
kw = {}
|
||||
if pretty:
|
||||
kw = dict(indent=4)
|
||||
json.dump(animation.to_dict(), fp, **kw)
|
||||
|
||||
|
||||
@exporter("Telegram Animated Sticker", ["tgs"], [
|
||||
ExtraOption("no_sanitize", help="Disable Sticker fit", action="store_false", dest="sanitize"),
|
||||
ExtraOption("no_validate", help="Disable feature validation", action="store_false", dest="validate"),
|
||||
])
|
||||
def export_tgs(animation, file, sanitize=False, validate=False):
|
||||
if sanitize:
|
||||
animation.tgs_sanitize()
|
||||
|
||||
with gzip.open(file, "wb") as gzfile:
|
||||
lottie_dict = animation.to_dict()
|
||||
lottie_dict["tgs"] = 1
|
||||
json.dump(lottie_dict, codecs.getwriter('utf-8')(gzfile))
|
||||
|
||||
if validate:
|
||||
validator = TgsValidator()
|
||||
validator(animation)
|
||||
validator.check_file_size(file)
|
||||
if validator.errors:
|
||||
sys.stdout.write("\n".join(map(str, validator.errors))+"\n")
|
||||
|
||||
|
||||
class HtmlOutput:
|
||||
def __init__(self, animation, file):
|
||||
self.animation = animation
|
||||
self.file = file
|
||||
|
||||
def style(self):
|
||||
self.file.write("""
|
||||
<style>
|
||||
#bodymovin { width: %spx; height: %spx; margin: auto;
|
||||
background-color: white;
|
||||
background-size: 64px 64px;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(0, 0, 0, .3) 50%%, transparent 50%%),
|
||||
linear-gradient(to bottom, rgba(0, 0, 0, .3) 50%%, transparent 50%%),
|
||||
linear-gradient(to bottom, white 50%%, transparent 50%%),
|
||||
linear-gradient(to right, transparent 50%%, rgba(0, 0, 0, .5) 50%%);
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.5.3/lottie.js"></script>
|
||||
""" % (self.animation.width, self.animation.height))
|
||||
|
||||
def body_pre(self):
|
||||
self.file.write("""
|
||||
<div id="bodymovin"></div>
|
||||
|
||||
<script>
|
||||
var animData = {
|
||||
container: document.getElementById('bodymovin'),
|
||||
renderer: 'svg',
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
""")
|
||||
|
||||
def body_embedded(self):
|
||||
self.file.write("animationData: ")
|
||||
export_lottie(self.animation, self.file, True)
|
||||
|
||||
def body_post(self):
|
||||
self.file.write("""
|
||||
};
|
||||
var anim = bodymovin.loadAnimation(animData);
|
||||
</script>""")
|
||||
|
||||
def html_begin(self):
|
||||
self.file.write("""<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
html, body { width: 100%; height: 100%; margin: 0; }
|
||||
body { display: flex; }
|
||||
</style>""")
|
||||
self.style()
|
||||
self.file.write("</head><body>")
|
||||
|
||||
def html_end(self):
|
||||
self.file.write("</body></html>")
|
||||
|
||||
|
||||
@exporter("Lottie HTML", ["html", "htm"])
|
||||
def export_embedded_html(animation, file):
|
||||
with open_file(file) as fp:
|
||||
out = HtmlOutput(animation, fp)
|
||||
out.html_begin()
|
||||
out.body_pre()
|
||||
out.body_embedded()
|
||||
out.body_post()
|
||||
out.html_end()
|
||||
|
||||
|
||||
def export_linked_html(animation, file, path):
|
||||
with open_file(file) as fp:
|
||||
out = HtmlOutput(animation, fp)
|
||||
out.html_begin()
|
||||
out.body_pre()
|
||||
file.write("path: %r" % path)
|
||||
out.body_post()
|
||||
out.html_end()
|
||||
@@ -0,0 +1,89 @@
|
||||
import json
|
||||
import string
|
||||
import zipfile
|
||||
|
||||
from .base import exporter
|
||||
from ..parsers.baseporter import ExtraOption
|
||||
from ..parsers.tgs import parse_tgs
|
||||
from lottie import __version__
|
||||
from ..objects import assets
|
||||
|
||||
|
||||
@exporter("dotLottie Archive", ["lottie"], [
|
||||
ExtraOption("id", help="ID of the animation", default=None),
|
||||
ExtraOption("append", help="Append animation to existing archive", action="store_true"),
|
||||
ExtraOption("revision", help="File revision", type=int, default=None),
|
||||
ExtraOption("author", help="File author", default=None),
|
||||
ExtraOption("speed", help="Playback speed", type=float, default=1),
|
||||
ExtraOption("theme_color", help="Theme color", type=str, default="#ffffff"),
|
||||
ExtraOption("no_loop", help="Disable Looping", action="store_false", dest="loop"),
|
||||
ExtraOption("no_pack", help="Don't auto-pack images", action="store_false", dest="pack_images"),
|
||||
], slug="dotlottie")
|
||||
def export_dotlottie(animation, file, id=None, append=False, revision=None, author=None,
|
||||
speed=1.0, theme_color="#ffffff", loop=True, pack_images=True):
|
||||
|
||||
files = {}
|
||||
|
||||
if append:
|
||||
with zipfile.ZipFile(file, "r") as zf:
|
||||
with zf.open("manifest.json") as manifest:
|
||||
meta = json.load(manifest)
|
||||
|
||||
for name in zf.namelist():
|
||||
if name != "manifest.json":
|
||||
files[name] = zf.read(name)
|
||||
else:
|
||||
meta = {
|
||||
"generator": "Python Lottie " + __version__,
|
||||
"version": 1.0,
|
||||
"revision": 1,
|
||||
"author": "",
|
||||
"animations": [],
|
||||
"custom": {}
|
||||
}
|
||||
|
||||
if revision is not None:
|
||||
meta["revision"] = revision
|
||||
|
||||
if author is not None:
|
||||
meta["author"] = author
|
||||
|
||||
if id is None:
|
||||
if animation.name:
|
||||
idok = string.ascii_letters + string.digits + "_-"
|
||||
id = "".join(filter(lambda x: x in idok, animation.name.replace(" ", "_")))
|
||||
if not id:
|
||||
id = "animation_%s" % len(meta["animations"])
|
||||
|
||||
meta["animations"].append({
|
||||
"id": id,
|
||||
"speed": speed,
|
||||
"themeColor": theme_color,
|
||||
"loop": loop,
|
||||
})
|
||||
|
||||
if pack_images and animation.assets:
|
||||
animation = animation.clone()
|
||||
image_no = 0
|
||||
for asset in animation.assets:
|
||||
if isinstance(asset, assets.Image):
|
||||
ext, data = asset.image_data()
|
||||
if not ext:
|
||||
continue
|
||||
pathname = "images/"
|
||||
while True:
|
||||
basename = "image_%s.%s" % (image_no, ext)
|
||||
image_no += 1
|
||||
if pathname+basename not in files:
|
||||
break
|
||||
files[pathname+basename] = data
|
||||
asset.image_path = pathname
|
||||
asset.image = basename
|
||||
asset.is_embedded = False
|
||||
|
||||
files["manifest.json"] = json.dumps(meta)
|
||||
files["animations/%s.json" % id] = json.dumps(animation.to_dict())
|
||||
|
||||
with zipfile.ZipFile(file, "w") as zf:
|
||||
for name, data in files.items():
|
||||
zf.writestr(name, data)
|
||||
@@ -0,0 +1,133 @@
|
||||
import io
|
||||
from PIL import Image
|
||||
from PIL import features
|
||||
|
||||
from .cairo import export_png
|
||||
from .base import exporter, io_progress
|
||||
from ..parsers.baseporter import ExtraOption
|
||||
|
||||
|
||||
def _png_gif_prepare(image):
|
||||
if image.mode not in ["RGBA", "RGBa"]:
|
||||
image = image.convert("RGBA")
|
||||
alpha = image.getchannel("A")
|
||||
image = image.convert("RGB").convert('P', palette=Image.ADAPTIVE, colors=255)
|
||||
mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0)
|
||||
image.paste(255, mask=mask)
|
||||
return image
|
||||
|
||||
|
||||
def _log_frame(fmt, frame_no=None, end=None):
|
||||
if frame_no is None:
|
||||
io_progress().report_message("%s frame rendering completed" % (fmt))
|
||||
else:
|
||||
io_progress().report_progress("%s rendering frame" % fmt, frame_no, end)
|
||||
|
||||
|
||||
@exporter("GIF", ["gif"], [
|
||||
ExtraOption("skip_frames", type=int, default=1, help="Only renderer 1 out of these many frames"),
|
||||
])
|
||||
def export_gif(animation, fp, dpi=96, skip_frames=1):
|
||||
"""
|
||||
Gif export
|
||||
|
||||
Note that it's a bit slow.
|
||||
"""
|
||||
start = int(animation.in_point)
|
||||
end = int(animation.out_point)
|
||||
frames = []
|
||||
for i in range(start, end+1, skip_frames):
|
||||
_log_frame("GIF", i, end)
|
||||
file = io.BytesIO()
|
||||
export_png(animation, file, i, dpi)
|
||||
file.seek(0)
|
||||
frames.append(_png_gif_prepare(Image.open(file)))
|
||||
_log_frame("GIF")
|
||||
|
||||
io_progress().report_message("GIF Writing to file...")
|
||||
duration = int(round(1000 / animation.frame_rate * skip_frames / 10)) * 10
|
||||
frames[0].save(
|
||||
fp,
|
||||
format='GIF',
|
||||
append_images=frames[1:],
|
||||
save_all=True,
|
||||
duration=duration,
|
||||
loop=0,
|
||||
transparency=255,
|
||||
disposal=2,
|
||||
)
|
||||
|
||||
|
||||
@exporter("WebP", ["webp"], [
|
||||
ExtraOption("lossless", action="store_true", help="If present, use lossless compression"),
|
||||
ExtraOption("quality", type=int, default=80,
|
||||
help="Compression effort between 0 and 100\n" +
|
||||
"for lossy 0 gives the smallest size\n" +
|
||||
"for lossless 0 gives the largest file"),
|
||||
ExtraOption("method", type=int, default=0, help="Quality/speed trade-off (0=fast, 6=slower-better)"),
|
||||
ExtraOption("skip_frames", type=int, default=1, help="Only renderer 1 out of these many frames"),
|
||||
])
|
||||
def export_webp(animation, fp, dpi=96, lossless=False, quality=80, method=0, skip_frames=1):
|
||||
"""
|
||||
Export WebP
|
||||
|
||||
See https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#webp
|
||||
"""
|
||||
if not features.check("webp_anim"):
|
||||
raise Exception("WebP animations not supported in this system")
|
||||
|
||||
start = int(animation.in_point)
|
||||
end = int(animation.out_point)
|
||||
frames = []
|
||||
for i in range(start, end+1, skip_frames):
|
||||
_log_frame("WebP", i, end)
|
||||
file = io.BytesIO()
|
||||
export_png(animation, file, i, dpi)
|
||||
file.seek(0)
|
||||
frames.append(Image.open(file))
|
||||
|
||||
_log_frame("WebP")
|
||||
|
||||
io_progress().report_message("WebP Writing to file...")
|
||||
duration = int(round(1000 / animation.frame_rate * skip_frames))
|
||||
frames[0].save(
|
||||
fp,
|
||||
format='WebP',
|
||||
append_images=frames[1:],
|
||||
save_all=True,
|
||||
duration=duration,
|
||||
loop=0,
|
||||
background=(0, 0, 0, 0),
|
||||
lossless=lossless,
|
||||
quality=quality,
|
||||
method=method
|
||||
)
|
||||
|
||||
|
||||
@exporter("TIFF", ["tiff"])
|
||||
def export_tiff(animation, fp, dpi=96):
|
||||
"""
|
||||
Export TIFF
|
||||
"""
|
||||
start = int(animation.in_point)
|
||||
end = int(animation.out_point)
|
||||
frames = []
|
||||
for i in range(start, end+1):
|
||||
_log_frame("TIFF", i, end)
|
||||
file = io.BytesIO()
|
||||
export_png(animation, file, i, dpi)
|
||||
file.seek(0)
|
||||
frames.append(Image.open(file))
|
||||
_log_frame("TIFF")
|
||||
|
||||
io_progress().report_message("TIFF Writing to file...")
|
||||
duration = int(round(1000 / animation.frame_rate))
|
||||
frames[0].save(
|
||||
fp,
|
||||
format='TIFF',
|
||||
append_images=frames[1:],
|
||||
save_all=True,
|
||||
duration=duration,
|
||||
loop=0,
|
||||
dpi=(dpi, dpi),
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
import sys
|
||||
|
||||
from ..objects.base import LottieObject, LottieBase
|
||||
from ..objects.properties import MultiDimensional, Value, ShapeProperty
|
||||
from ..objects.layers import Layer
|
||||
|
||||
|
||||
def _prettyprint_scalar(lottie_object, out=sys.stdout):
|
||||
if isinstance(lottie_object, float) and lottie_object == round(lottie_object):
|
||||
lottie_object = int(lottie_object)
|
||||
return str(lottie_object)
|
||||
|
||||
|
||||
def prettyprint(lottie_object, out=sys.stdout, indent=" ", _i=""):
|
||||
if isinstance(lottie_object, LottieObject):
|
||||
out.write(lottie_object.__class__.__name__)
|
||||
out.write('\n')
|
||||
_i += indent
|
||||
maxk = max(map(lambda x: len(x.name), lottie_object._props))
|
||||
for k in lottie_object._props:
|
||||
out.write(_i)
|
||||
out.write(k.name.ljust(maxk))
|
||||
out.write(' : ')
|
||||
prettyprint(k.get(lottie_object), out, indent, _i)
|
||||
elif isinstance(lottie_object, (list, tuple)):
|
||||
if not lottie_object or (not isinstance(lottie_object[0], LottieBase) and len(lottie_object) < 16):
|
||||
out.write("[")
|
||||
out.write(", ".join(map(_prettyprint_scalar, lottie_object)))
|
||||
out.write("]\n")
|
||||
else:
|
||||
out.write("[\n")
|
||||
for k in lottie_object:
|
||||
out.write(_i + indent)
|
||||
prettyprint(k, out, indent, _i + indent)
|
||||
out.write(_i)
|
||||
out.write(']\n')
|
||||
else:
|
||||
out.write(_prettyprint_scalar(lottie_object, out))
|
||||
out.write('\n')
|
||||
|
||||
|
||||
def _prettyprint_summary_printable(obj):
|
||||
if isinstance(obj, LottieObject):
|
||||
return not isinstance(obj, (MultiDimensional, Value, ShapeProperty))
|
||||
return obj and isinstance(obj, (list, tuple)) and isinstance(obj[0], LottieObject)
|
||||
|
||||
|
||||
def prettyprint_summary(lottie_object, out=sys.stdout, indent=" ", _i=""):
|
||||
if isinstance(lottie_object, LottieObject):
|
||||
out.write(lottie_object.__class__.__name__)
|
||||
name = getattr(lottie_object, "name", None)
|
||||
if name:
|
||||
out.write(" %r" % name)
|
||||
if isinstance(lottie_object, Layer):
|
||||
out.write(" %s -> %s" % (lottie_object.index, lottie_object.parent_index))
|
||||
out.write('\n')
|
||||
_i += indent
|
||||
for k in lottie_object._props:
|
||||
val = k.get(lottie_object)
|
||||
if _prettyprint_summary_printable(val):
|
||||
out.write(_i)
|
||||
out.write(k.name)
|
||||
out.write(' : ')
|
||||
prettyprint_summary(val, out, indent, _i)
|
||||
elif _prettyprint_summary_printable(lottie_object):
|
||||
out.write("[\n")
|
||||
for k in lottie_object:
|
||||
out.write(_i + indent)
|
||||
prettyprint_summary(k, out, indent, _i + indent)
|
||||
out.write(_i)
|
||||
out.write(']\n')
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
from .base import exporter
|
||||
from ..parsers.sif.builder import to_sif
|
||||
from ..utils.file import open_file
|
||||
|
||||
|
||||
@exporter("Synfig", ["sif"], [], {"pretty"})
|
||||
def export_sif(animation, file, pretty=True):
|
||||
with open_file(file) as fp:
|
||||
dom = to_sif(animation).to_xml()
|
||||
dom.writexml(fp, "", " " if pretty else "", "\n" if pretty else "")
|
||||
@@ -0,0 +1,22 @@
|
||||
from xml.dom import minidom
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from .base import exporter
|
||||
from ..parsers.svg.builder import to_svg
|
||||
from ..utils.file import open_file
|
||||
|
||||
|
||||
def _print_ugly_xml(dom, file):
|
||||
return dom.write(file, "utf-8", True)
|
||||
|
||||
|
||||
def _print_pretty_xml(dom, file):
|
||||
with open_file(file) as fp:
|
||||
xmlstr = minidom.parseString(ElementTree.tostring(dom.getroot())).toprettyxml(indent=" ")
|
||||
fp.write(xmlstr)
|
||||
|
||||
|
||||
@exporter("SVG", ["svg"], [], {"pretty", "frame"})
|
||||
def export_svg(animation, file, frame=0, pretty=True):
|
||||
_print_xml = _print_pretty_xml if pretty else _print_ugly_xml
|
||||
_print_xml(to_svg(animation, frame), file)
|
||||
@@ -0,0 +1,198 @@
|
||||
import os
|
||||
import enum
|
||||
import json
|
||||
import inspect
|
||||
|
||||
from ..parsers.tgs import parse_tgs
|
||||
from ..objects.base import ObjectVisitor
|
||||
from ..objects.animation import Animation
|
||||
from ..objects import layers
|
||||
from ..objects import shapes
|
||||
from ..objects import helpers
|
||||
|
||||
|
||||
class Severity(enum.Enum):
|
||||
Note = enum.auto()
|
||||
Warning = enum.auto()
|
||||
Error = enum.auto()
|
||||
|
||||
|
||||
class TgsError:
|
||||
def __init__(self, message, target, severity=Severity.Warning):
|
||||
self.message = message
|
||||
self.target = target
|
||||
self.severity = severity
|
||||
|
||||
def target_id(self):
|
||||
if isinstance(self.target, str):
|
||||
return self.target
|
||||
if getattr(self.target, "name", ""):
|
||||
return self.target.name
|
||||
return self.target.__class__.__name__
|
||||
|
||||
def __str__(self):
|
||||
return "%s: on %s: %s" % (
|
||||
self.severity.name,
|
||||
self.target_id(),
|
||||
self.message
|
||||
)
|
||||
|
||||
|
||||
class TgsValidator(ObjectVisitor):
|
||||
def __init__(self, severity=Severity.Note):
|
||||
self.errors = []
|
||||
self.severity = severity
|
||||
|
||||
def _check(self, expr, message, target, severity=Severity.Warning):
|
||||
if severity.value >= self.severity.value and not expr:
|
||||
self.errors.append(TgsError(message, target, severity))
|
||||
|
||||
def check_file_size(self, filename):
|
||||
return self.check_size(os.path.getsize(filename))
|
||||
|
||||
def check_size(self, bytes, filename="file"):
|
||||
size_k = bytes / 1024
|
||||
self._check(
|
||||
size_k <= 64,
|
||||
"Invalid size (%.1fk), should be less than 64k" % size_k,
|
||||
filename,
|
||||
Severity.Error
|
||||
)
|
||||
|
||||
def check_file(self, filename):
|
||||
self.check_file_size(filename)
|
||||
try:
|
||||
self(parse_tgs(filename))
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
self._check(
|
||||
False,
|
||||
"Invalid JSON: %s" % e,
|
||||
filename,
|
||||
Severity.Error
|
||||
)
|
||||
|
||||
def visit(self, object):
|
||||
for cls in inspect.getmro(object.__class__):
|
||||
callback = "_visit_%s" % cls.__name__.lower()
|
||||
if hasattr(self, callback):
|
||||
getattr(self, callback)(object)
|
||||
|
||||
def _visit_animation(self, o: Animation):
|
||||
self._check(
|
||||
o.frame_rate in {30, 60},
|
||||
"Invalid framerate %s, should be 30 or 60" % o.frame_rate,
|
||||
o,
|
||||
Severity.Error
|
||||
)
|
||||
self._check(
|
||||
o.width == 512,
|
||||
"Invalid width %s, should be 512" % o.width,
|
||||
o,
|
||||
Severity.Error
|
||||
)
|
||||
self._check(
|
||||
o.height == 512,
|
||||
"Invalid height %s, should be 512" % o.height,
|
||||
o,
|
||||
Severity.Error
|
||||
)
|
||||
self._check(
|
||||
(o.out_point-o.in_point) <= 180,
|
||||
"Too many frames (%s), should be less than 180" % (o.out_point-o.in_point),
|
||||
o,
|
||||
Severity.Error
|
||||
)
|
||||
|
||||
def _visit_layer(self, o: layers.Layer):
|
||||
self._check(
|
||||
not o.has_masks and not o.masks,
|
||||
"Masks are not officially supported",
|
||||
o,
|
||||
Severity.Note
|
||||
)
|
||||
self._check(
|
||||
not o.effects,
|
||||
"Effects are not supported",
|
||||
o,
|
||||
Severity.Warning
|
||||
)
|
||||
self._check(
|
||||
not o.threedimensional,
|
||||
"3D layers are not supported",
|
||||
o,
|
||||
Severity.Warning
|
||||
)
|
||||
self._check(
|
||||
not isinstance(o, layers.TextLayer),
|
||||
"Text layers are not supported",
|
||||
o,
|
||||
Severity.Warning
|
||||
)
|
||||
self._check(
|
||||
not isinstance(o, layers.ImageLayer),
|
||||
"Image layers are not supported",
|
||||
o,
|
||||
Severity.Warning
|
||||
)
|
||||
self._check(
|
||||
not o.auto_orient,
|
||||
"Auto-orient layers are not supported",
|
||||
o,
|
||||
Severity.Warning
|
||||
)
|
||||
self._check(
|
||||
o.matte_mode in {None, layers.MatteMode.Normal},
|
||||
"Mattes are not supported",
|
||||
o,
|
||||
Severity.Warning
|
||||
)
|
||||
|
||||
def _visit_precomplayer(self, o: layers.PreCompLayer):
|
||||
self._check(
|
||||
o.time_remapping is None,
|
||||
"Time remapping is not supported",
|
||||
o,
|
||||
Severity.Warning
|
||||
)
|
||||
|
||||
def _visit_merge(self, o: shapes.Merge):
|
||||
self._check(
|
||||
False,
|
||||
"Merge paths are not supported",
|
||||
o,
|
||||
Severity.Warning
|
||||
)
|
||||
|
||||
def _visit_transform(self, o: helpers.Transform):
|
||||
self._check(
|
||||
o.skew is None or (
|
||||
not o.skew.animated and o.skew.value == 0
|
||||
),
|
||||
"Skew transforms are not supported",
|
||||
o,
|
||||
Severity.Warning
|
||||
)
|
||||
|
||||
def _visit_gradientstroke(self, o: shapes.GradientStroke):
|
||||
self._check(
|
||||
False,
|
||||
"Gradient strokes are not officially supported",
|
||||
o,
|
||||
Severity.Note
|
||||
)
|
||||
|
||||
def _visit_star(self, o: shapes.Star):
|
||||
self._check(
|
||||
False,
|
||||
"Star Shapes are not officially supported",
|
||||
o,
|
||||
Severity.Note
|
||||
)
|
||||
|
||||
def _visit_repeater(self, o: shapes.Repeater):
|
||||
self._check(
|
||||
False,
|
||||
"Repeaters are not officially supported",
|
||||
o,
|
||||
Severity.Note
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
import io
|
||||
import os
|
||||
|
||||
import cv2
|
||||
import numpy
|
||||
from PIL import Image
|
||||
|
||||
from .cairo import export_png
|
||||
from .gif import _log_frame
|
||||
from .base import exporter
|
||||
from ..parsers.baseporter import ExtraOption
|
||||
|
||||
|
||||
## @see http://www.fourcc.org/codecs.php
|
||||
formats4cc = {
|
||||
"avi": cv2.VideoWriter_fourcc(*"XVID"),
|
||||
"mp4": cv2.VideoWriter_fourcc(*'MP4V'),
|
||||
#"mp4": cv2.VideoWriter_fourcc(*'X264'),
|
||||
"webm": cv2.VideoWriter_fourcc(*'VP80'),
|
||||
}
|
||||
|
||||
|
||||
@exporter("Video", list(formats4cc.keys()), [
|
||||
ExtraOption("format", default=None, help="Specific video format", choices=list(formats4cc.keys())),
|
||||
], [], "video")
|
||||
def export_video(animation, fp, format=None):
|
||||
start = int(animation.in_point)
|
||||
end = int(animation.out_point)
|
||||
if format is None:
|
||||
fn = fp if isinstance(fp, str) else fp.name
|
||||
format = os.path.splitext(fn)[1][1:]
|
||||
fmt = formats4cc[format]
|
||||
video = cv2.VideoWriter(fp, fmt, animation.frame_rate, (animation.width, animation.height))
|
||||
|
||||
for i in range(start, end+1):
|
||||
_log_frame(format, i, end)
|
||||
file = io.BytesIO()
|
||||
export_png(animation, file, i)
|
||||
file.seek(0)
|
||||
video.write(cv2.cvtColor(numpy.array(Image.open(file)), cv2.COLOR_RGB2BGR))
|
||||
|
||||
_log_frame(format)
|
||||
video.release()
|
||||
Reference in New Issue
Block a user