Convert from rendering to a list of images to rendering in a generator so extremely long sequences can be rendered to pngs without OOM.
This commit is contained in:
parent
8622e0980c
commit
8b3ce489b1
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Dict, List, Tuple, Optional, Union
|
from typing import Any, Dict, Generator, List, Tuple, Optional, Union
|
||||||
from PIL import Image # type: ignore
|
from PIL import Image # type: ignore
|
||||||
|
|
||||||
from .blend import affine_composite
|
from .blend import affine_composite
|
||||||
@ -297,14 +297,15 @@ class AFPRenderer(VerboseOutput):
|
|||||||
only_depths: Optional[List[int]] = None,
|
only_depths: Optional[List[int]] = None,
|
||||||
movie_transform: Matrix = Matrix.identity(),
|
movie_transform: Matrix = Matrix.identity(),
|
||||||
verbose: bool = False,
|
verbose: bool = False,
|
||||||
) -> Tuple[int, List[Image.Image]]:
|
) -> Generator[Image.Image, None, None]:
|
||||||
# Given a path to a SWF root animation, attempt to render it to a list of frames.
|
# Given a path to a SWF root animation, attempt to render it to a list of frames.
|
||||||
for name, swf in self.swfs.items():
|
for name, swf in self.swfs.items():
|
||||||
if swf.exported_name == path:
|
if swf.exported_name == path:
|
||||||
# This is the SWF we care about.
|
# This is the SWF we care about.
|
||||||
with self.debugging(verbose):
|
with self.debugging(verbose):
|
||||||
swf.color = background_color or swf.color
|
swf.color = background_color or swf.color
|
||||||
return self.__render(swf, only_depths, movie_transform, background_image)
|
yield from self.__render(swf, only_depths, movie_transform, background_image)
|
||||||
|
return
|
||||||
|
|
||||||
raise Exception(f'{path} not found in registered SWFs!')
|
raise Exception(f'{path} not found in registered SWFs!')
|
||||||
|
|
||||||
@ -320,14 +321,37 @@ class AFPRenderer(VerboseOutput):
|
|||||||
|
|
||||||
raise Exception(f'{path} not found in registered SWFs!')
|
raise Exception(f'{path} not found in registered SWFs!')
|
||||||
|
|
||||||
def list_paths(self, verbose: bool = False) -> List[str]:
|
def compute_path_frames(
|
||||||
# Given the loaded animations, return a list of possible paths to render.
|
self,
|
||||||
paths: List[str] = []
|
path: str,
|
||||||
|
) -> int:
|
||||||
|
# Given a path to a SWF root animation, figure out how many frames are
|
||||||
|
# in that root path with no regard to bytecode 'stop()' commands.
|
||||||
for name, swf in self.swfs.items():
|
for name, swf in self.swfs.items():
|
||||||
paths.append(swf.exported_name)
|
if swf.exported_name == path:
|
||||||
|
# This is the SWF we care about.
|
||||||
|
return len(swf.frames)
|
||||||
|
|
||||||
return paths
|
raise Exception(f'{path} not found in registered SWFs!')
|
||||||
|
|
||||||
|
def compute_path_frame_duration(
|
||||||
|
self,
|
||||||
|
path: str,
|
||||||
|
) -> int:
|
||||||
|
# Given a path to a SWF root animation, figure out how many milliseconds are
|
||||||
|
# occupied by each frame.
|
||||||
|
for name, swf in self.swfs.items():
|
||||||
|
if swf.exported_name == path:
|
||||||
|
# This is the SWF we care about.
|
||||||
|
spf = 1.0 / swf.fps
|
||||||
|
return int(spf * 1000.0)
|
||||||
|
|
||||||
|
raise Exception(f'{path} not found in registered SWFs!')
|
||||||
|
|
||||||
|
def list_paths(self, verbose: bool = False) -> Generator[str, None, None]:
|
||||||
|
# Given the loaded animations, return a list of possible paths to render.
|
||||||
|
for name, swf in self.swfs.items():
|
||||||
|
yield swf.exported_name
|
||||||
|
|
||||||
def __execute_bytecode(self, bytecode: ByteCode, clip: PlacedClip, thisptr: Optional[Any] = MissingThis, prefix: str="") -> None:
|
def __execute_bytecode(self, bytecode: ByteCode, clip: PlacedClip, thisptr: Optional[Any] = MissingThis, prefix: str="") -> None:
|
||||||
if self.__movie is None:
|
if self.__movie is None:
|
||||||
@ -1044,13 +1068,12 @@ class AFPRenderer(VerboseOutput):
|
|||||||
only_depths: Optional[List[int]],
|
only_depths: Optional[List[int]],
|
||||||
movie_transform: Matrix,
|
movie_transform: Matrix,
|
||||||
background_image: Optional[Image.Image],
|
background_image: Optional[Image.Image],
|
||||||
) -> Tuple[int, List[Image.Image]]:
|
) -> Generator[Image.Image, None, None]:
|
||||||
# First, let's attempt to resolve imports.
|
# First, let's attempt to resolve imports.
|
||||||
self.__registered_objects = self.__handle_imports(swf)
|
self.__registered_objects = self.__handle_imports(swf)
|
||||||
|
|
||||||
# Initialize overall frame advancement stuff.
|
# Initialize overall frame advancement stuff.
|
||||||
spf = 1.0 / swf.fps
|
last_rendered_frame: Optional[Image.Image] = None
|
||||||
frames: List[Image.Image] = []
|
|
||||||
frameno: int = 0
|
frameno: int = 0
|
||||||
|
|
||||||
# Calculate actual size based on given movie transform.
|
# Calculate actual size based on given movie transform.
|
||||||
@ -1149,32 +1172,30 @@ class AFPRenderer(VerboseOutput):
|
|||||||
try:
|
try:
|
||||||
while root_clip.playing and not root_clip.finished:
|
while root_clip.playing and not root_clip.finished:
|
||||||
# Create a new image to render into.
|
# Create a new image to render into.
|
||||||
time = spf * frameno
|
|
||||||
color = swf.color or Color(0.0, 0.0, 0.0, 0.0)
|
color = swf.color or Color(0.0, 0.0, 0.0, 0.0)
|
||||||
self.vprint(f"Rendering frame {frameno + 1}/{len(root_clip.source.frames)} ({round(time, 2)}s)")
|
self.vprint(f"Rendering frame {frameno + 1}/{len(root_clip.source.frames)}")
|
||||||
|
|
||||||
# Go through all registered clips, place all needed tags.
|
# Go through all registered clips, place all needed tags.
|
||||||
changed = self.__process_tags(root_clip, False)
|
changed = self.__process_tags(root_clip, False)
|
||||||
while self.__is_dirty(root_clip):
|
while self.__is_dirty(root_clip):
|
||||||
changed = self.__process_tags(root_clip, True) or changed
|
changed = self.__process_tags(root_clip, True) or changed
|
||||||
|
|
||||||
if changed or frameno == 0:
|
if changed or last_rendered_frame is None:
|
||||||
# Now, render out the placed objects.
|
# Now, render out the placed objects.
|
||||||
curimage = Image.new("RGBA", actual_size, color=color.as_tuple())
|
curimage = Image.new("RGBA", actual_size, color=color.as_tuple())
|
||||||
curimage = self.__render_object(curimage, root_clip, movie_transform, movie_mask, actual_mult_color, actual_add_color, actual_blend, only_depths=only_depths)
|
curimage = self.__render_object(curimage, root_clip, movie_transform, movie_mask, actual_mult_color, actual_add_color, actual_blend, only_depths=only_depths)
|
||||||
else:
|
else:
|
||||||
# Nothing changed, make a copy of the previous render.
|
# Nothing changed, make a copy of the previous render.
|
||||||
self.vprint(" Using previous frame render")
|
self.vprint(" Using previous frame render")
|
||||||
curimage = frames[-1].copy()
|
curimage = last_rendered_frame.copy()
|
||||||
|
|
||||||
# Advance our bookkeeping.
|
# Return that frame, advance our bookkeeping.
|
||||||
frames.append(curimage)
|
last_rendered_frame = curimage
|
||||||
frameno += 1
|
frameno += 1
|
||||||
|
yield curimage
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
# Allow ctrl-c to end early and render a partial animation.
|
# Allow ctrl-c to end early and render a partial animation.
|
||||||
print(f"WARNING: Interrupted early, will render only {len(frames) + 1}/{len(root_clip.source.frames)} frames of animation!")
|
print(f"WARNING: Interrupted early, will render only {frameno}/{len(root_clip.source.frames)} frames of animation!")
|
||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
self.movie = None
|
self.movie = None
|
||||||
|
|
||||||
return int(spf * 1000.0), frames
|
|
||||||
|
@ -790,18 +790,19 @@ def main() -> int:
|
|||||||
ty=0.0,
|
ty=0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Render the gif/webp frames.
|
# Support rendering only certain depth planes.
|
||||||
if args.only_depths is not None:
|
if args.only_depths is not None:
|
||||||
depths = [int(d.strip()) for d in args.only_depths.split(",")]
|
depths = [int(d.strip()) for d in args.only_depths.split(",")]
|
||||||
else:
|
else:
|
||||||
depths = None
|
depths = None
|
||||||
duration, images = renderer.render_path(args.path, verbose=args.verbose, background_color=color, background_image=background, only_depths=depths, movie_transform=transform)
|
|
||||||
|
|
||||||
if len(images) == 0:
|
|
||||||
raise Exception("Did not render any frames!")
|
|
||||||
|
|
||||||
if fmt in ["GIF", "WEBP"]:
|
if fmt in ["GIF", "WEBP"]:
|
||||||
# Write all the frames out in one file.
|
# Write all the frames out in one file.
|
||||||
|
duration = renderer.compute_path_frame_duration(args.path)
|
||||||
|
images = list(renderer.render_path(args.path, verbose=args.verbose, background_color=color, background_image=background, only_depths=depths, movie_transform=transform))
|
||||||
|
if len(images) == 0:
|
||||||
|
raise Exception("Did not render any frames!")
|
||||||
|
|
||||||
with open(args.output, "wb") as bfp:
|
with open(args.output, "wb") as bfp:
|
||||||
images[0].save(bfp, format=fmt, save_all=True, append_images=images[1:], duration=duration, optimize=True)
|
images[0].save(bfp, format=fmt, save_all=True, append_images=images[1:], duration=duration, optimize=True)
|
||||||
|
|
||||||
@ -812,11 +813,13 @@ def main() -> int:
|
|||||||
ext = args.output[-4:]
|
ext = args.output[-4:]
|
||||||
|
|
||||||
# Figure out padding for the images.
|
# Figure out padding for the images.
|
||||||
frames = len(images)
|
frames = renderer.compute_path_frames(args.path)
|
||||||
if frames > 0:
|
if frames > 0:
|
||||||
digits = f"0{int(math.log10(frames)) + 1}"
|
digits = f"0{int(math.log10(frames)) + 1}"
|
||||||
|
|
||||||
for i, img in enumerate(images):
|
for i, img in enumerate(
|
||||||
|
renderer.render_path(args.path, verbose=args.verbose, background_color=color, background_image=background, only_depths=depths, movie_transform=transform)
|
||||||
|
):
|
||||||
fullname = f"{filename}-{i:{digits}}{ext}"
|
fullname = f"{filename}-{i:{digits}}{ext}"
|
||||||
|
|
||||||
with open(fullname, "wb") as bfp:
|
with open(fullname, "wb") as bfp:
|
||||||
@ -825,8 +828,7 @@ def main() -> int:
|
|||||||
print(f"Wrote animation frame to {fullname}")
|
print(f"Wrote animation frame to {fullname}")
|
||||||
|
|
||||||
elif args.action == "list":
|
elif args.action == "list":
|
||||||
paths = renderer.list_paths(verbose=args.verbose)
|
for path in renderer.list_paths(verbose=args.verbose):
|
||||||
for path in paths:
|
|
||||||
print(path)
|
print(path)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
Loading…
Reference in New Issue
Block a user