1
0
mirror of synced 2025-01-18 22:24:04 +01:00

Implement forced aspect ratio for renderer.

This commit is contained in:
Jennifer Taylor 2021-05-21 21:31:13 +00:00
parent 3922535555
commit 35c53c3b6c
2 changed files with 84 additions and 10 deletions

View File

@ -3,7 +3,7 @@ from PIL import Image # type: ignore
from .blend import affine_composite
from .swf import SWF, Frame, Tag, AP2ShapeTag, AP2DefineSpriteTag, AP2PlaceObjectTag, AP2RemoveObjectTag, AP2DoActionTag, AP2DefineFontTag, AP2DefineEditTextTag, AP2PlaceCameraTag
from .types import Color, Matrix, Point
from .types import Color, Matrix, Point, Rectangle
from .geo import Shape, DrawParams
from .util import VerboseOutput
@ -154,14 +154,33 @@ class AFPRenderer(VerboseOutput):
data.parse()
self.swfs[name] = data
def render_path(self, path: str, background_color: Optional[Color] = None, verbose: bool = False, only_depths: Optional[List[int]] = None) -> Tuple[int, List[Image.Image]]:
def render_path(
self,
path: str,
background_color: Optional[Color] = None,
only_depths: Optional[List[int]] = None,
movie_transform: Matrix = Matrix.identity(),
verbose: bool = False,
) -> Tuple[int, List[Image.Image]]:
# 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=only_depths)
return self.__render(swf, only_depths, movie_transform)
raise Exception(f'{path} not found in registered SWFs!')
def compute_path_location(
self,
path: str,
) -> Rectangle:
# Given a path to a SWF root animation, find its bounding rectangle.
for name, swf in self.swfs.items():
if swf.exported_name == path:
# This is the SWF we care about.
return swf.location
raise Exception(f'{path} not found in registered SWFs!')
@ -636,21 +655,25 @@ class AFPRenderer(VerboseOutput):
# We didn't find the tag we were after.
return None
def __render(self, swf: SWF, only_depths: Optional[List[int]] = None) -> Tuple[int, List[Image.Image]]:
def __render(self, swf: SWF, only_depths: Optional[List[int]], movie_transform: Matrix) -> Tuple[int, List[Image.Image]]:
# First, let's attempt to resolve imports.
self.__registered_objects = self.__handle_imports(swf)
# Now, let's go through each frame, performing actions as necessary.
# Initialize overall frame advancement stuff.
spf = 1.0 / swf.fps
frames: List[Image.Image] = []
frameno: int = 0
# Calculate actual size based on given movie transform.
actual_size = movie_transform.multiply_point(Point(swf.location.width, swf.location.height)).as_tuple()
print(actual_size)
# Create a root clip for the movie to play.
root_clip = PlacedClip(
-1,
-1,
Point.identity(),
Matrix.identity(),
movie_transform,
Color(1.0, 1.0, 1.0, 1.0),
Color(0.0, 0.0, 0.0, 0.0),
0,
@ -676,7 +699,7 @@ class AFPRenderer(VerboseOutput):
# Now, render out the placed objects. We sort by depth so that we can
# get the layering correct, but its important to preserve the original
# insertion order for delete requests.
curimage = Image.new("RGBA", (int(swf.location.width), int(swf.location.height)), color=color.as_tuple())
curimage = Image.new("RGBA", actual_size, color=color.as_tuple())
curimage = self.__render_object(curimage, root_clip, root_clip.transform, root_clip.rotation_offset, only_depths=only_depths)
else:
# Nothing changed, make a copy of the previous render.
@ -688,6 +711,6 @@ class AFPRenderer(VerboseOutput):
frameno += 1
except KeyboardInterrupt:
# Allow ctrl-c to end early and render a partial animation.
print(f"WARNING: Interrupted early, will render only {len(frames)} of animation!")
print(f"WARNING: Interrupted early, will render only {len(frames)}/{len(root_clip.source.frames)} frames of animation!")
return int(spf * 1000.0), frames

View File

@ -9,7 +9,7 @@ import textwrap
from PIL import Image, ImageDraw # type: ignore
from typing import Any, Dict, List
from bemani.format.afp import TXP2File, Shape, SWF, Frame, Tag, AP2DoActionTag, AP2PlaceObjectTag, AP2DefineSpriteTag, AFPRenderer, Color
from bemani.format.afp import TXP2File, Shape, SWF, Frame, Tag, AP2DoActionTag, AP2PlaceObjectTag, AP2DefineSpriteTag, AFPRenderer, Color, Matrix
from bemani.format import IFS
@ -273,6 +273,12 @@ def main() -> int:
default=None,
help="Only render objects on these depth planes. Can provide either a number or a comma-separated list of numbers.",
)
render_parser.add_argument(
"--force-aspect-ratio",
type=str,
default=None,
help="Force the aspect ratio of the rendered image, as a colon-separated aspect ratio such as 16:9 or 4:3.",
)
render_parser.add_argument(
"--disable-threads",
action="store_true",
@ -714,12 +720,57 @@ def main() -> int:
else:
color = None
# Calculate the size of the SWF so we can apply scaling options.
swf_location = renderer.compute_path_location(args.path)
requested_width = swf_location.width
requested_height = swf_location.height
# Allow overriding the aspect ratio.
if args.force_aspect_ratio:
ratio = args.force_aspect_ratio.split(":")
if len(ratio) != 2:
raise Exception("Invalid aspect ratio, specify a ratio such as 16:9 or 4:3!")
rx, ry = [float(r.strip()) for r in ratio]
if rx <= 0 or ry <= 0:
raise Exception("Ratio must only include positive numbers!")
actual_ratio = rx / ry
swf_ratio = swf_location.width / swf_location.height
if abs(swf_ratio - actual_ratio) > 0.0001:
new_width = actual_ratio * swf_location.height
new_height = swf_location.width / actual_ratio
if new_width < swf_location.width and new_height < swf_location.height:
raise Exception("Impossible aspect ratio!")
if new_width > swf_location.width and new_height > swf_location.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:
requested_width = new_width
else:
requested_height = new_height
# Calculate the overall view matrix based on the requested width/height.
transform = Matrix(
a=requested_width / swf_location.width,
b=0.0,
c=0.0,
d=requested_height / swf_location.height,
tx=0.0,
ty=0.0,
)
print(transform)
# Render the gif/webp frames.
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, only_depths=depths)
duration, images = renderer.render_path(args.path, verbose=args.verbose, background_color=color, only_depths=depths, movie_transform=transform)
if len(images) == 0:
raise Exception("Did not render any frames!")