1
0
mirror of synced 2024-11-27 23:50:47 +01:00

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:
Jennifer Taylor 2021-05-24 17:36:34 +00:00
parent 8622e0980c
commit 8b3ce489b1
2 changed files with 53 additions and 30 deletions

View File

@ -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 .blend import affine_composite
@ -297,14 +297,15 @@ class AFPRenderer(VerboseOutput):
only_depths: Optional[List[int]] = None,
movie_transform: Matrix = Matrix.identity(),
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.
for name, swf in self.swfs.items():
if swf.exported_name == path:
# This is the SWF we care about.
with self.debugging(verbose):
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!')
@ -320,14 +321,37 @@ class AFPRenderer(VerboseOutput):
raise Exception(f'{path} not found in registered SWFs!')
def list_paths(self, verbose: bool = False) -> List[str]:
# Given the loaded animations, return a list of possible paths to render.
paths: List[str] = []
def compute_path_frames(
self,
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():
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:
if self.__movie is None:
@ -1044,13 +1068,12 @@ class AFPRenderer(VerboseOutput):
only_depths: Optional[List[int]],
movie_transform: Matrix,
background_image: Optional[Image.Image],
) -> Tuple[int, List[Image.Image]]:
) -> Generator[Image.Image, None, None]:
# First, let's attempt to resolve imports.
self.__registered_objects = self.__handle_imports(swf)
# Initialize overall frame advancement stuff.
spf = 1.0 / swf.fps
frames: List[Image.Image] = []
last_rendered_frame: Optional[Image.Image] = None
frameno: int = 0
# Calculate actual size based on given movie transform.
@ -1149,32 +1172,30 @@ class AFPRenderer(VerboseOutput):
try:
while root_clip.playing and not root_clip.finished:
# Create a new image to render into.
time = spf * frameno
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.
changed = self.__process_tags(root_clip, False)
while self.__is_dirty(root_clip):
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.
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)
else:
# Nothing changed, make a copy of the previous render.
self.vprint(" Using previous frame render")
curimage = frames[-1].copy()
curimage = last_rendered_frame.copy()
# Advance our bookkeeping.
frames.append(curimage)
# Return that frame, advance our bookkeeping.
last_rendered_frame = curimage
frameno += 1
yield curimage
except KeyboardInterrupt:
# 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
self.movie = None
return int(spf * 1000.0), frames

View File

@ -790,18 +790,19 @@ def main() -> int:
ty=0.0,
)
# Render the gif/webp frames.
# Support rendering only certain depth planes.
if args.only_depths is not None:
depths = [int(d.strip()) for d in args.only_depths.split(",")]
else:
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"]:
# 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:
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:]
# Figure out padding for the images.
frames = len(images)
frames = renderer.compute_path_frames(args.path)
if frames > 0:
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}"
with open(fullname, "wb") as bfp:
@ -825,8 +828,7 @@ def main() -> int:
print(f"Wrote animation frame to {fullname}")
elif args.action == "list":
paths = renderer.list_paths(verbose=args.verbose)
for path in paths:
for path in renderer.list_paths(verbose=args.verbose):
print(path)
return 0