diff --git a/bemani/format/afp/render.py b/bemani/format/afp/render.py index 102882c..2c70fc7 100644 --- a/bemani/format/afp/render.py +++ b/bemani/format/afp/render.py @@ -191,6 +191,10 @@ class PlacedClip(PlacedObject): self.requested_frame: Optional[int] = None self.visible_frame: int = -1 + # Root clip resizing, which we don't really support. + self.__width = 0 + self.__height = 0 + @property def source(self) -> RegisteredClip: return self.__source @@ -299,6 +303,30 @@ class PlacedClip(PlacedObject): def _visible(self, val: Any) -> None: self.visible = val != 0 + @property + def _width(self) -> int: + calculated_width = self.__width + for obj in self.placed_objects: + if isinstance(obj, PlacedClip): + calculated_width = max(calculated_width, obj._width) + return calculated_width + + @_width.setter + def _width(self, val: Any) -> None: + self.__width = val + + @property + def _height(self) -> int: + calculated_height = self.__height + for obj in self.placed_objects: + if isinstance(obj, PlacedClip): + calculated_height = max(calculated_height, obj._height) + return calculated_height + + @_height.setter + def _height(self, val: Any) -> None: + self.__height = val + class PlacedImage(PlacedObject): # An image that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag @@ -612,6 +640,8 @@ class AFPRenderer(VerboseOutput): only_depths: Optional[List[int]] = None, only_frames: Optional[List[int]] = None, movie_transform: Matrix = Matrix.identity(), + overridden_width: Optional[float] = None, + overridden_height: Optional[float] = None, verbose: bool = False, ) -> Generator[Image.Image, None, None]: # Given a path to a SWF root animation, attempt to render it to a list of frames. @@ -620,7 +650,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, only_frames, movie_transform, background_image) + yield from self.__render(swf, only_depths, only_frames, movie_transform, background_image, overridden_width, overridden_height) return raise Exception(f'{path} not found in registered SWFs!') @@ -1642,6 +1672,8 @@ class AFPRenderer(VerboseOutput): only_frames: Optional[List[int]], movie_transform: Matrix, background_image: Optional[List[Image.Image]], + overridden_width: Optional[float], + overridden_height: Optional[float], ) -> Generator[Image.Image, None, None]: # First, let's attempt to resolve imports. self.__registered_objects = self.__handle_imports(swf) @@ -1651,10 +1683,14 @@ class AFPRenderer(VerboseOutput): frameno: int = 0 # Calculate actual size based on given movie transform. - resized_width, resized_height, _ = movie_transform.multiply_point(Point(swf.location.width, swf.location.height)).as_tuple() + actual_width = overridden_width or swf.location.width + actual_height = overridden_height or swf.location.height + resized_width, resized_height, _ = movie_transform.multiply_point(Point(actual_width, actual_height)).as_tuple() - # TODO: If the location top/left is nonzero, we need move the root transform - # so that the correct viewport is rendered. + if round(swf.location.top, 2) != 0.0 or round(swf.location.left, 2) != 0.0: + # TODO: If the location top/left is nonzero, we need move the root transform + # so that the correct viewport is rendered. + print("WARNING: Root clip requested to play not in top-left corner!") # Create a root clip for the movie to play. root_clip = PlacedClip( @@ -1674,6 +1710,10 @@ class AFPRenderer(VerboseOutput): swf.labels, ), ) + root_clip._width = int(actual_width) + root_clip._height = int(actual_height) + last_width = actual_width + last_height = actual_height self.__root = root_clip # If we have a background image, add it to the root clip. @@ -1686,10 +1726,10 @@ class AFPRenderer(VerboseOutput): imgwidth = background_image[0].width imgheight = background_image[0].height background_matrix = Matrix.affine( - a=swf.location.width / imgwidth, + a=actual_width / imgwidth, b=0, c=0, - d=swf.location.height / imgheight, + d=actual_height / imgheight, tx=0, ty=0, ) @@ -1772,6 +1812,12 @@ class AFPRenderer(VerboseOutput): continue if changed or last_rendered_frame is None: + if last_width != root_clip._width or last_height != root_clip._height: + last_width = root_clip._width + last_height = root_clip._height + if root_clip._width > actual_width or root_clip._height > actual_height: + print(f"WARNING: Root clip requested to resize to {last_width}x{last_height} which overflows root canvas!") + # Now, render out the placed objects. color = swf.color or Color(0.0, 0.0, 0.0, 0.0) curimage = Image.new("RGBA", (resized_width, resized_height), color=color.as_tuple()) diff --git a/bemani/utils/afputils.py b/bemani/utils/afputils.py index fc80b09..7626fd9 100644 --- a/bemani/utils/afputils.py +++ b/bemani/utils/afputils.py @@ -567,6 +567,8 @@ def render_path( background_loop_start: Optional[int] = None, background_loop_end: Optional[int] = None, background_loop_offset: Optional[int] = None, + override_width: Optional[int] = None, + override_height: Optional[int] = None, force_width: Optional[int] = None, force_height: Optional[int] = None, force_aspect_ratio: Optional[str] = None, @@ -688,8 +690,16 @@ def render_path( # Calculate the size of the animation so we can apply scaling options. swf_location = renderer.compute_path_location(path) - requested_width = force_width if force_width is not None else swf_location.width - requested_height = force_height if force_height is not None else swf_location.height + if override_width is not None: + actual_width = float(override_width) + else: + actual_width = swf_location.width + if override_height is not None: + actual_height = float(override_height) + else: + actual_height = swf_location.height + requested_width = force_width if force_width is not None else actual_width + requested_height = force_height if force_height is not None else actual_height # Allow overriding the aspect ratio. if force_aspect_ratio: @@ -702,20 +712,20 @@ def render_path( raise Exception("Ratio must only include positive numbers!") actual_ratio = rx / ry - swf_ratio = swf_location.width / swf_location.height + swf_ratio = actual_width / actual_height if abs(swf_ratio - actual_ratio) > 0.0001: - new_width = actual_ratio * swf_location.height - new_height = swf_location.width / actual_ratio + new_width = actual_ratio * actual_height + new_height = actual_width / actual_ratio - if new_width < swf_location.width and new_height < swf_location.height: + if new_width < actual_width and new_height < actual_height: raise Exception("Impossible aspect ratio!") - if new_width > swf_location.width and new_height > swf_location.height: + if new_width > actual_width and new_height > actual_height: raise Exception("Impossible aspect ratio!") # We know that one is larger and one is smaller, pick the larger. # This way we always stretch instead of shrinking. - if new_width > swf_location.width: + if new_width > actual_width: requested_width = new_width else: requested_height = new_height @@ -726,10 +736,10 @@ def render_path( # Calculate the overall view matrix based on the requested width/height. transform = Matrix.affine( - a=requested_width / swf_location.width, + a=requested_width / actual_width, b=0.0, c=0.0, - d=requested_height / swf_location.height, + d=requested_height / actual_height, tx=0.0, ty=0.0, ) @@ -760,6 +770,8 @@ def render_path( only_depths=requested_depths, only_frames=requested_frames, movie_transform=transform, + overridden_width=override_width, + overridden_height=override_height, ) ): if show_progress: @@ -1147,6 +1159,24 @@ def main() -> int: "the animation's original height it will be stretched vertically." ), ) + render_parser.add_argument( + "--override-width", + type=int, + default=None, + help=( + "Override the specified the width of the rendered animation to a specific pixel value, such as 640 or 800. Note that this performs " + "no scaling whatsoever. It simply overrides the animation's root canvas size." + ), + ) + render_parser.add_argument( + "--override-height", + type=int, + default=None, + help=( + "Override the specified the height of the rendered animation to a specific pixel value, such as 640 or 800. Note that this performs " + "no scaling whatsoever. It simply overrides the animation's root canvas size." + ), + ) render_parser.add_argument( "--force-aspect-ratio", type=str, @@ -1247,6 +1277,8 @@ def main() -> int: background_loop_start=args.background_loop_start, background_loop_end=args.background_loop_end, background_loop_offset=args.background_loop_offset, + override_width=args.override_width, + override_height=args.override_height, force_width=args.force_width, force_height=args.force_height, force_aspect_ratio=args.force_aspect_ratio,