diff --git a/bemani/tests/test_AFPUtils.py b/bemani/tests/test_AFPUtils.py new file mode 100644 index 0000000..8019473 --- /dev/null +++ b/bemani/tests/test_AFPUtils.py @@ -0,0 +1,150 @@ +# vim: set fileencoding=utf-8 +import unittest + +from bemani.utils.afputils import parse_intlist, adjust_background_loop + + +class TestAFPUtils(unittest.TestCase): + + def test_parse_intlist(self) -> None: + # Simple + self.assertEqual( + parse_intlist("5"), + [5], + ) + + # Comma separated + self.assertEqual( + parse_intlist("5,7,9"), + [5, 7, 9], + ) + + # Range + self.assertEqual( + parse_intlist("5-9"), + [5, 6, 7, 8, 9], + ) + + # Duplicate + self.assertEqual( + parse_intlist("5,7,7,9"), + [5, 7, 9], + ) + + # Overlapping range + self.assertEqual( + parse_intlist("5-9,8-10"), + [5, 6, 7, 8, 9, 10], + ) + + # Out of order + self.assertEqual( + parse_intlist("5,3,1"), + [1, 3, 5], + ) + + # All manner of combos + self.assertEqual( + parse_intlist("5,13-17,23,9,27-29,23,33"), + [5, 9, 13, 14, 15, 16, 17, 23, 27, 28, 29, 33], + ) + + def test_adjust_background_loop(self) -> None: + # No adjustment + self.assertEqual( + adjust_background_loop( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + background_loop_start=None, + background_loop_end=None, + background_loop_offset=None, + ), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ) + + # Specify start + self.assertEqual( + adjust_background_loop( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + background_loop_start=6, + background_loop_end=None, + background_loop_offset=None, + ), + [6, 7, 8, 9, 10], + ) + + # Specify end + self.assertEqual( + adjust_background_loop( + [1, 2, 3, 4, 5], + background_loop_start=None, + background_loop_end=5, + background_loop_offset=None, + ), + [1, 2, 3, 4, 5], + ) + + # Specify start and end + self.assertEqual( + adjust_background_loop( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + background_loop_start=5, + background_loop_end=9, + background_loop_offset=None, + ), + [5, 6, 7, 8, 9], + ) + + # Specify loop offset + self.assertEqual( + adjust_background_loop( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + background_loop_start=None, + background_loop_end=None, + background_loop_offset=7, + ), + [7, 8, 9, 10, 1, 2, 3, 4, 5, 6], + ) + + # Specify start and loop offset + self.assertEqual( + adjust_background_loop( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + background_loop_start=6, + background_loop_end=None, + background_loop_offset=8, + ), + [8, 9, 10, 6, 7], + ) + + # Specify end and loop offset + self.assertEqual( + adjust_background_loop( + [1, 2, 3, 4, 5], + background_loop_start=None, + background_loop_end=5, + background_loop_offset=3, + ), + [3, 4, 5, 1, 2], + ) + + # Specify start, end and loop offset + self.assertEqual( + adjust_background_loop( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + background_loop_start=5, + background_loop_end=9, + background_loop_offset=6, + ), + [6, 7, 8, 9, 5], + ) + + # Only one frame. + self.assertEqual( + adjust_background_loop( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + background_loop_start=5, + background_loop_end=5, + background_loop_offset=None, + ), + [5], + ) diff --git a/bemani/utils/afputils.py b/bemani/utils/afputils.py index 6ea449f..0500100 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, Tuple +from typing import Any, Dict, List, Optional, Tuple, TypeVar from bemani.format.afp import TXP2File, Shape, SWF, Frame, Tag, AP2DoActionTag, AP2PlaceObjectTag, AP2DefineSpriteTag, AFPRenderer, Color, Matrix from bemani.format import IFS @@ -517,6 +517,45 @@ def list_paths(containers: List[str], *, include_frames: bool=False, include_siz return 0 +BackgroundT = TypeVar("BackgroundT") + + +def adjust_background_loop( + background: List[BackgroundT], + background_loop_start: Optional[int] = None, + background_loop_end: Optional[int] = None, + background_loop_offset: Optional[int] = None, +) -> List[BackgroundT]: + # Make sure background frames are 1-indexed here as well. + if background_loop_start is None: + background_loop_start = 0 + else: + background_loop_start -= 1 + + if background_loop_offset is None: + background_loop_offset = 0 + else: + background_loop_offset -= (background_loop_start + 1) + + # Don't one-index the end because we want it to be inclusive. + if background_loop_end is None: + background_loop_end = len(background) + + if background_loop_start >= background_loop_end: + raise Exception("Cannot start background loop after the end of the background loop!") + if background_loop_start < 0 or background_loop_end < 0: + raise Exception("Cannot start or end background loop on a negative frame!") + if background_loop_start >= len(background) or background_loop_end > len(background): + raise Exception("Cannot start or end background loop larger than the number of background animation frames!") + + background = background[background_loop_start:background_loop_end] + + if background_loop_offset < 0 or background_loop_offset >= len(background): + raise Exception("Cannot start first iteration of background loop outside the loop bounds!") + + return background[background_loop_offset:] + background[:background_loop_offset] + + def render_path( containers: List[str], path: str, @@ -528,6 +567,7 @@ def render_path( background_image: Optional[str] = None, background_loop_start: Optional[int] = None, background_loop_end: Optional[int] = None, + background_loop_offset: Optional[int] = None, force_width: Optional[int] = None, force_height: Optional[int] = None, force_aspect_ratio: Optional[str] = None, @@ -634,24 +674,7 @@ def render_path( else: raise Exception("Invalid image specified as background!") - # Make sure background frames are 1-indexed here as well. - if background_loop_start is None: - background_loop_start = 0 - else: - background_loop_start -= 1 - - # Don't one-index the end because we want it to be inclusive. - if background_loop_end is None: - background_loop_end = len(background) - - if background_loop_start >= background_loop_end: - raise Exception("Cannot start background loop after the end of the background loop!") - if background_loop_start < 0 or background_loop_end < 0: - raise Exception("Cannot start or end background loop on a negative frame!") - if background_loop_start >= len(background) or background_loop_end > len(background): - raise Exception("Cannot start or end background loop larger than the number of background animation frames!") - - background = background[background_loop_start:background_loop_end] + background = adjust_background_loop(background, background_loop_start, background_loop_end, background_loop_offset) else: background = None @@ -1001,13 +1024,19 @@ def main() -> int: "--background-loop-start", type=int, default=None, - help="The starting frame of the background animation. Specify this to start the background animation on a frame other than the first.", + help="The starting frame of the background animation loop. Specify this to loop to a background animation frame other than the first.", ) render_parser.add_argument( "--background-loop-end", type=int, default=None, - help="The ending frame of the background animation. Specify this to end the background animation on a frame other than the last.", + help="The ending frame of the background animation loop. Specify this to loop from a background animation frame other than the last.", + ) + render_parser.add_argument( + "--background-loop-offset", + type=int, + default=None, + help="The very first frame of the background animation. Specify this to start the first loop anywhere other than the loop start frame.", ) render_parser.add_argument( "--only-depths", @@ -1136,6 +1165,7 @@ def main() -> int: background_image=args.background_image, background_loop_start=args.background_loop_start, background_loop_end=args.background_loop_end, + background_loop_offset=args.background_loop_offset, force_width=args.force_width, force_height=args.force_height, force_aspect_ratio=args.force_aspect_ratio,