1
0
mirror of synced 2024-11-12 01:00:46 +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 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

View File

@ -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