diff --git a/bemani/format/afp/render.py b/bemani/format/afp/render.py index 74b2647..080d43f 100644 --- a/bemani/format/afp/render.py +++ b/bemani/format/afp/render.py @@ -398,6 +398,7 @@ class AFPRenderer(VerboseOutput): background_color: Optional[Color] = None, background_image: Optional[Image.Image] = None, only_depths: Optional[List[int]] = None, + only_frames: Optional[List[int]] = None, movie_transform: Matrix = Matrix.identity(), verbose: bool = False, ) -> Generator[Image.Image, None, None]: @@ -407,7 +408,7 @@ class AFPRenderer(VerboseOutput): # This is the SWF we care about. with self.debugging(verbose): swf.color = background_color or swf.color - yield from self.__render(swf, only_depths, movie_transform, background_image) + yield from self.__render(swf, only_depths, only_frames, movie_transform, background_image) return raise Exception(f'{path} not found in registered SWFs!') @@ -1077,6 +1078,8 @@ class AFPRenderer(VerboseOutput): # during some bytecode update in this loop. if clip.frame > clip.requested_frame: # Rewind this clip to the beginning so we can replay until the requested frame. + if clip is self.__root: + print("WARNING: Root clip was rewound, its possible this animation plays forever!") clip.rewind() self.vprint(f"{prefix} Processing frame {clip.frame} on our way to frame {clip.requested_frame}") @@ -1235,6 +1238,7 @@ class AFPRenderer(VerboseOutput): self, swf: SWF, only_depths: Optional[List[int]], + only_frames: Optional[List[int]], movie_transform: Matrix, background_image: Optional[Image.Image], ) -> Generator[Image.Image, None, None]: @@ -1338,11 +1342,15 @@ class AFPRenderer(VerboseOutput): actual_add_color = Color(0.0, 0.0, 0.0, 0.0) actual_blend = 0 + if only_frames: + max_frame = max(only_frames) + else: + max_frame = None + # Now play the frames of the root clip. try: while root_clip.playing and not root_clip.finished: # Create a new image to render into. - 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)}") # Go through all registered clips, place all needed tags. @@ -1350,8 +1358,17 @@ class AFPRenderer(VerboseOutput): while self.__is_dirty(root_clip): changed = self.__process_tags(root_clip, True) or changed + # If we're only rendering some frames, don't bother to do the draw operations + # if we aren't going to return the frame. + if only_frames and (frameno + 1) not in only_frames: + self.vprint(f"Skipped rendering frame {frameno + 1}/{len(root_clip.source.frames)}") + last_rendered_frame = None + frameno += 1 + continue + if changed or last_rendered_frame is None: # Now, render out the placed objects. + color = swf.color or Color(0.0, 0.0, 0.0, 0.0) 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: @@ -1360,12 +1377,17 @@ class AFPRenderer(VerboseOutput): curimage = last_rendered_frame.copy() # Return that frame, advance our bookkeeping. + self.vprint(f"Finished rendering frame {frameno + 1}/{len(root_clip.source.frames)}") last_rendered_frame = curimage frameno += 1 yield curimage + + # See if we should bail because we passed the last requested frame. + if max_frame is not None and frameno == max_frame: + break except KeyboardInterrupt: # Allow ctrl-c to end early and render a partial animation. print(f"WARNING: Interrupted early, will render only {frameno}/{len(root_clip.source.frames)} frames of animation!") # Clean up - self.movie = None + self.__root = None diff --git a/bemani/utils/afputils.py b/bemani/utils/afputils.py index 2d453f6..9433408 100644 --- a/bemani/utils/afputils.py +++ b/bemani/utils/afputils.py @@ -64,6 +64,22 @@ def write_bytecode(swf: SWF, directory: str, verbose: bool=False) -> None: bfp.write(f"{os.linesep}{os.linesep}".join(buff).encode('utf-8')) +def parse_intlist(data: str) -> List[int]: + ints: List[int] = [] + + for chunk in data.split(","): + chunk = chunk.strip() + if '-' in chunk: + start, end = chunk.split('-', 1) + start_int = int(start.strip()) + end_int = int(end.strip()) + ints.extend(range(start_int, end_int + 1)) + else: + ints.append(int(chunk)) + + return sorted(set(ints)) + + def main() -> int: parser = argparse.ArgumentParser(description="Konami AFP graphic file unpacker/repacker") subparsers = parser.add_subparsers(help='Action to take', dest='action') @@ -272,7 +288,13 @@ def main() -> int: "--only-depths", type=str, default=None, - help="Only render objects on these depth planes. Can provide either a number or a comma-separated list of numbers.", + help="Only render objects on these depth planes. Can provide either a number or a comma-separated list of numbers, or a range such as 3-5.", + ) + render_parser.add_argument( + "--only-frames", + type=str, + default=None, + help="Only render these frames. Can provide either a number or a comma-separated list of numbers, or a range such as 10-20.", ) render_parser.add_argument( "--force-aspect-ratio", @@ -792,14 +814,30 @@ def main() -> int: # Support rendering only certain depth planes. if args.only_depths is not None: - depths = [int(d.strip()) for d in args.only_depths.split(",")] + requested_depths = parse_intlist(args.only_depths) else: - depths = None + requested_depths = None + + # Support rendering only certain frames. + if args.only_frames is not None: + requested_frames = parse_intlist(args.only_frames) + else: + requested_frames = None 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)) + images = list( + renderer.render_path( + args.path, + verbose=args.verbose, + background_color=color, + background_image=background, + only_depths=requested_depths, + only_frames=requested_frames, + movie_transform=transform, + ) + ) if len(images) == 0: raise Exception("Did not render any frames!") @@ -818,7 +856,15 @@ def main() -> int: digits = f"0{int(math.log10(frames)) + 1}" 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) + renderer.render_path( + args.path, + verbose=args.verbose, + background_color=color, + background_image=background, + only_depths=requested_depths, + only_frames=requested_frames, + movie_transform=transform, + ) ): fullname = f"{filename}-{i:{digits}}{ext}"