diff --git a/bemani/format/afp/render.py b/bemani/format/afp/render.py index fec0aab..4cd4900 100644 --- a/bemani/format/afp/render.py +++ b/bemani/format/afp/render.py @@ -514,7 +514,7 @@ class AFPRenderer(VerboseOutput): self, path: str, background_color: Optional[Color] = None, - background_image: Optional[Image.Image] = None, + background_image: Optional[List[Image.Image]] = None, only_depths: Optional[List[int]] = None, only_frames: Optional[List[int]] = None, movie_transform: Matrix = Matrix.identity(), @@ -1543,7 +1543,7 @@ class AFPRenderer(VerboseOutput): only_depths: Optional[List[int]], only_frames: Optional[List[int]], movie_transform: Matrix, - background_image: Optional[Image.Image], + background_image: Optional[List[Image.Image]], ) -> Generator[Image.Image, None, None]: # First, let's attempt to resolve imports. self.__registered_objects = self.__handle_imports(swf) @@ -1579,10 +1579,13 @@ class AFPRenderer(VerboseOutput): self.__root = root_clip # If we have a background image, add it to the root clip. + background_object = RegisteredImage(-1, "INVALID_REFERENCE_NAME") + background_frames = 0 + if background_image: - # Stretch the image to make sure it fits the entire frame. - imgwidth = float(background_image.width) - imgheight = float(background_image.height) + # Stretch the images to make sure they fit the entire frame. + imgwidth = background_image[0].width + imgheight = background_image[0].height background_matrix = Matrix.affine( a=swf.location.width / imgwidth, b=0, @@ -1591,14 +1594,18 @@ class AFPRenderer(VerboseOutput): tx=0, ty=0, ) + background_frames = len(background_image) - # Register the background image with the texture library. - name = f"{swf.exported_name}_inserted_background" - self.textures[name] = background_image.convert("RGBA") + # Register the background images with the texture library. + for background_frame in range(background_frames): + if background_image[background_frame].width != imgwidth or background_image[background_frame].height != imgheight: + raise Exception(f"Frame {background_frame + 1} of background image sequence has different dimensions than others!") + name = f"{swf.exported_name}_inserted_background_{background_frame}" + self.textures[name] = background_image[background_frame].convert("RGBA") # Place an instance of this background on the root clip. root_clip.placed_objects.append( - PlacedShape( + PlacedImage( -1, -1, Point.identity(), @@ -1608,37 +1615,7 @@ class AFPRenderer(VerboseOutput): Color(0.0, 0.0, 0.0, 0.0), 0, None, - RegisteredShape( - -1, - # The coordinates of the rectangle of the shape in screen space. - [ - Point(0, 0), - Point(imgwidth, 0), - Point(imgwidth, imgheight), - Point(0, imgheight), - ], - # The coordinates of the original texture in UV space (we don't use this). - [ - Point(0.0, 0.0), - Point(1.0, 0.0), - Point(1.0, 1.0), - Point(0.0, 1.0), - ], - # No texture colors. - [], - [ - DrawParams( - # Instantiable, includes texture. - 0x3, - # The texture this should use for drawing. - name, - # The coordinates of the triangles that get drawn (we don't use this). - [0, 1, 2, 2, 1, 3], - # The blend color. - None, - ), - ], - ), + background_object ), ) @@ -1665,6 +1642,14 @@ class AFPRenderer(VerboseOutput): while self.__is_dirty(root_clip): changed = self.__process_tags(root_clip, True) or changed + # Calculate a new background frame if needed. + if background_frames > 0: + background_frame = frameno % background_frames + name = f"{swf.exported_name}_inserted_background_{background_frame}" + if background_object.reference != name: + background_object.reference = name + changed = True + # Adjust camera based on the movie's scaling. if self.__camera is not None and not self.__camera.adjusted: self.__camera.center = movie_transform.multiply_point(self.__camera.center) diff --git a/bemani/utils/afputils.py b/bemani/utils/afputils.py index f4755e2..17735de 100644 --- a/bemani/utils/afputils.py +++ b/bemani/utils/afputils.py @@ -8,7 +8,7 @@ import os.path import sys import textwrap from PIL import Image, ImageDraw # type: ignore -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from bemani.format.afp import TXP2File, Shape, SWF, Frame, Tag, AP2DoActionTag, AP2PlaceObjectTag, AP2DefineSpriteTag, AFPRenderer, Color, Matrix from bemani.format import IFS @@ -572,9 +572,65 @@ def render_path( else: color = None - # Allow inserting a background image. + # Allow inserting a background image, series of images or animation. if background_image: - background = Image.open(background_image) + background_image = os.path.abspath(background_image) + background: List[Image.Image] = [] + + if os.path.isfile(background_image): + # This is a direct reference. + bgimg = Image.open(background_image) + frames = getattr(bgimg, "n_frames", 1) + + if frames == 1: + background.append(bgimg) + elif frames > 1: + for frame in range(frames): + bgimg.seek(frame) + background.append(bgimg.copy()) + else: + raise Exception("Invalid image specified as background!") + else: + # This is probably a reference to a list of images. + dirof, fileof = os.path.split(background_image) + startof, endof = os.path.splitext(fileof) + if len(startof) == 0 or len(endof) == 0: + raise Exception("Invalid image specified as background!") + startof = startof + '-' + + # Gather up the sequence of files so we can make frames out of them. + seqdict: Dict[int, str] = {} + for filename in os.listdir(dirof): + if filename.startswith(startof) and filename.endswith(endof): + seqno = filename[len(startof):(-len(endof))] + if seqno.isdigit(): + seqint = int(seqno) + if seqint in seqdict: + raise Exception(f"{filename} specifies the same background frame number as {seqdict[seqint]}!") + seqdict[seqint] = filename + + # Now, order the sequence by the integer of the sequence number so we can load the images. + seqtuple: List[Tuple[int, str]] = sorted( + [(s, p) for (s, p) in seqdict.items()], + key=lambda e: e[0], + ) + + # Finally, get the filenames from this sequence. + filenames: List[str] = [os.path.join(dirof, filename) for (_, filename) in seqtuple] + + # Now that we have the list, lets load the images! + for filename in filenames: + bgimg = Image.open(filename) + frames = getattr(bgimg, "n_frames", 1) + + if frames == 1: + background.append(bgimg) + elif frames > 1: + for frame in range(frames): + bgimg.seek(frame) + background.append(bgimg.copy()) + else: + raise Exception("Invalid image specified as background!") else: background = None @@ -900,7 +956,10 @@ def main() -> int: metavar="IMAGE", type=str, default="out.gif", - help='The output file (ending either in .gif, .webp or .png) where the render should be saved.', + help=( + 'The output file (ending either in .gif, .webp or .png) where the render should be saved. If .png is chosen then the ' + 'output will be a series of png files for each rendered frame.' + ), ) render_parser.add_argument( "--background-color", @@ -912,7 +971,10 @@ def main() -> int: "--background-image", type=str, default=None, - help="Set a background image to be placed behind the animation. Note that it will be stretched to fit the animation.", + help=( + "Set a background image or animation to be placed behind the animation. Note that the background will be stretched to fit " + "the animation. If a .png is specified and multiple rendered frames are present, it will use that series as an animation." + ), ) render_parser.add_argument( "--only-depths",