diff --git a/j2a-extract.py b/j2a-extract.py index db6d36b..f308a9a 100644 --- a/j2a-extract.py +++ b/j2a-extract.py @@ -1,41 +1,86 @@ -from __future__ import print_function +readme = """ +Unpack a J2A file + +Will unpack a J2A file insto a given folder. The resulting folder structure will +look like this: + + [target folder] -> + set-001 -> + animation-001 -> + frame-001.png + frame-002.png + ... + animation.settings + animation-002 -> + ... + set-002 -> + ... + ... + +Existing files will be overwritten. +""" + +import argparse +import pathlib +import yaml import os -import sys -import struct + from j2a import J2A -if sys.version_info[0] <= 2: - input = raw_input - -def main(): - animsfilename = sys.argv[1] if (len(sys.argv) >= 2) else \ - input("Please type the animsfilename of the .j2a file you wish to extract:\n") - outputdir = sys.argv[2] if (len(sys.argv) >= 3) else \ - os.path.join(os.path.dirname(animsfilename), os.path.basename(animsfilename).replace('.', '-')) - j2a = J2A(animsfilename).read() - for setnum, s in enumerate(j2a.sets): - s = j2a.sets[setnum] - setdir = os.path.join(outputdir, str(setnum)) - if not os.path.exists(setdir): - os.makedirs(setdir) - for animnum, anim in enumerate(s.animations): - dirname = os.path.join(setdir, str(animnum)) - if not os.path.exists(dirname): - os.makedirs(dirname) - fps_filename = os.path.join(dirname, "fps.%d" % anim.fps) - open(fps_filename, "a").close() # Touch fps file, leave it empty - for framenum, frame in enumerate(anim.frames): - frameid = str(framenum) - if frame.tagged: - frameid += "t" - imgfilename = os.path.join(dirname, "{0:s},{1:d},{2:d},{3:d},{4:d},{5:d},{6:d}.png".format( - frameid, - *frame.origin + - frame.coldspot + - frame.gunspot - )) - j2a.render_paletted_pixelmap(frame).save(imgfilename) - print("Finished extracting set %d (%d animations)" % (setnum, animnum + 1)) - -if __name__ == "__main__": - main() +cli = argparse.ArgumentParser(description=readme, prog="J2A Unpacker", formatter_class=argparse.RawDescriptionHelpFormatter) +cli.add_argument("--palettefile", "-p", default="Diamondus_2.pal", help="Palette file to use") +cli.add_argument("j2afile", help="The J2A file to extract") +cli.add_argument("--folder", "-f", default=".", + help="Where to extract the animation data. Defaults to current working directory.") +args = cli.parse_args() + +# check if all files we need exist and can be opened properly +destination_folder = pathlib.Path(args.folder) +source_file = pathlib.Path(args.j2afile) +palette_file = pathlib.Path(args.palettefile) + +for check_file in (source_file, palette_file): + if not check_file.exists(): + print("File '%s' does not exist." % str(check_file)) + exit(1) + +try: + j2afile = J2A(str(source_file), palette=str(palette_file)).read() +except Exception: + print("Could not open J2A file %s. Is it a valid J2A file?" % source_file.name) + exit(1) + +# loop through all animations and unpack their frames +for set_index, set in enumerate(j2afile.sets): + print("Importing set %i..." % set_index) + set_folder = destination_folder.joinpath("set-%s" % str(set_index).zfill(3)) + if not set_folder.exists(): + os.makedirs(set_folder) + + for animation_index, animation in enumerate(set.animations): + print("Unpacking animation %i..." % animation_index) + animation_folder = set_folder.joinpath("animation-%s" % str(animation_index).zfill(3)) + if not animation_folder.exists(): + os.makedirs(animation_folder) + + settings_file = animation_folder.joinpath("animation.settings") + settings = { + "default": {"origin": "0,0", "coldspot": "0,0", "gunspot": "0,0", "tagged": 0, "fps": animation.fps}} + + for frame_index, frame in enumerate(animation.frames): + frame_file = animation_folder.joinpath("frame-%s.png" % str(frame_index).zfill(3)) + frame_name = frame_file.stem + + settings[frame_name] = { + "origin": ",".join([str(coordinate) for coordinate in frame.origin]), + "coldspot": ",".join([str(coordinate) for coordinate in frame.coldspot]), + "gunspot": ",".join([str(coordinate) for coordinate in frame.gunspot]), + "tagged": {False: 0, True: 1}[frame.tagged] + } + + j2afile.render_paletted_pixelmap(frame).save(str(frame_file)) + + with settings_file.open("w") as settings_output: + yaml.dump(settings, settings_output) + +print("Done!") \ No newline at end of file diff --git a/j2a-import.py b/j2a-import.py index cf1456e..6c41f6e 100644 --- a/j2a-import.py +++ b/j2a-import.py @@ -1,87 +1,137 @@ -from __future__ import print_function -import os -import sys -import glob -import struct -import re -import zlib +readme = """ +Create a J2A file from a folder of animation sets + +The folder should contain one subfolder per animation set. This set folder +should contain one folder per animation. The animation folder should contain +animation frames as paletted .png files, as well as a Yaml file with the +.settings extension that defines animation properties. In other words, the +folder you point this script as should have the following structure: + + my_animations -> + enemies + -> evil_ghost + -> frame001.png + -> frame002.png + -> evil_ghost.settings + -> machinegun_marine + -> ... + pickups + -> ... + +And so on. The .settings Yaml file should be structured as follows: + + default: + origin: 16,16 + coldspot: 8,8 + gunspot: 9,4 + tagged=1 + fps=12 + + frame005: + coldspot: 4,3 + tagged=0 + +And so on. The 'default' dictionary is required, the rest is optional and can +overwrite the settings for individual frames. The dictionary keys should +correspond to frame file names: in the example above, the coldspot and tagged +setting are overridden for frame005.png. Overriding 'fps' for individual frames +has no effect. + +Sets, animations & frames are imported in file name order (i.e. +alphabetically). File and folder names can be arbitrary. +""" + +import argparse +import pathlib +import yaml + from PIL import Image from j2a import J2A -import misc - -if sys.version_info[0] <= 2: - input = raw_input - -#leaf frame order: -#0 1 2 1 0 3 4 5 6 7 8 9 3 4 5 6 7 8 9 0 1 2 1 0 10 11 12 13 14 15 16 - -frame_filename_pat = re.compile(r"^(\d+)(t?),(-?\d+),(-?\d+),(-?\d+),(-?\d+),(-?\d+),(-?\d+).png$") -def get_numeric_subdirs(folder = "."): - dirlist = sorted((dirname for dirname in os.listdir(folder) if dirname.isdigit()), key = lambda x : int(x)) - return [dirname for dirname in dirlist if os.path.isdir(dirname)] - - -def main(): - dirname = sys.argv[1] if (len(sys.argv) >= 2) else \ - input("Please type the folder you wish to import:\n") - dirname = os.path.abspath(dirname) - if not os.path.basename(dirname).endswith("-j2a"): - print("Folder name is improperly formatted (must be named ***-j2a)!", file=sys.stderr) - return 1 - outfilename = sys.argv[2] if (len(sys.argv) >= 3) else dirname.replace("-j2a", ".j2a") - os.chdir(dirname) - setdirlist = get_numeric_subdirs() - if not setdirlist: - print("No sets were found in that folder. No .j2a file will be compiled.", file=sys.stderr) - return 1 - anims = J2A(outfilename, empty_set = "crop") - anims.sets = [J2A.Set() for _ in range(int(setdirlist[-1]) + 1)] - for set_dir in setdirlist: - os.chdir(set_dir) - cur_set = anims.sets[int(set_dir)] - animdirlist = get_numeric_subdirs() - num_animations = 1 + int(animdirlist[-1]) if animdirlist else 0 - cur_set.animations = [J2A.Animation() for _ in range(num_animations)] - for anim_dir in animdirlist: - os.chdir(anim_dir) - cur_anim = cur_set.animations[int(anim_dir)] - framelist = glob.glob('*.png') - frameinfo_list = list(map(frame_filename_pat.match, framelist)) - for frame_filename, frameinfo in zip(framelist, frameinfo_list): - if not frameinfo: - print("Warning: found file %s/%s/%s not matching frame naming format" % - (set_dir, anim_dir, frame_filename), file=sys.stderr) - frameinfo_list = sorted(filter(bool, frameinfo_list), key = lambda x : int(x.group(1))) - fpsfile = [filename[4:] for filename in glob.glob('fps.*') if filename[4:].isdigit()] - if len(fpsfile) == 1: - cur_anim.fps = int(fpsfile[0]) - elif len(fpsfile) > 1: - print("Warning: found multiple fps files in folder %s/%s, ignoring" % (set_dir, anim_dir)) - - for frame_num, frameinfo in enumerate(frameinfo_list): - frame_filename = frameinfo.group() - groups = list(frameinfo.groups()) - groups[2:] = list(map(int, groups[2:])) - assert int(groups[0]) == frame_num, \ - "unexected frame %s/%s/%s, might be a duplicate or a frame is missing" % (set_dir, anim_dir, frame_filename) - image = Image.open(frame_filename) - assert image.mode == "P", "image file %s is not paletted" % os.path.abspath(frame_filename) - frame = J2A.Frame( - pixmap = image, - origin = tuple(groups[2:4]), - coldspot = tuple(groups[4:6]), - gunspot = tuple(groups[6:8]), - tagged = bool(groups[1]) - ) - frame.autogenerate_mask() - cur_anim.frames.append(frame) - os.chdir("..") - cur_set.samplesbaseindex = 0 - cur_set.pack(anims.config) - os.chdir("..") - anims.write() - return 0 - - -if __name__ == "__main__": - sys.exit(main()) + +cli = argparse.ArgumentParser(description=readme, prog="J2A Creator", formatter_class=argparse.RawDescriptionHelpFormatter) +cli.add_argument("folder", help="Folder containing animation frames, one folder per animation") +cli.add_argument("output", help="Output file name") +cli.add_argument("--yes", "-y", help="Always answer confirmation prompts with 'yes'", default=False, + action="store_true") +args = cli.parse_args() + +# check and open all relevant files and folders +source_folder = pathlib.Path(args.folder) +if not source_folder.exists(): + print("Folder %s does not exist." % args.folder) + exit(1) + +output_file = pathlib.Path(args.output) + +set_folders = sorted([subfolder for subfolder in source_folder.iterdir() if subfolder.is_dir()]) +j2a_file = J2A(str(output_file), empty_set="crop") +j2a_file.sets = [J2A.Set() for _ in range(len(set_folders))] + +# loop through everything and store it +set_index = 0 +for set_folder in set_folders: + animation_folders = sorted([subfolder for subfolder in set_folder.iterdir() if subfolder.is_dir()]) + animation_set = j2a_file.sets[set_index] + animation_set.animations = [J2A.Animation() for _ in range(len(animation_folders))] + animation_index = 0 + + print("Importing set %s" % set_folder.name) + + for animation_folder in animation_folders: + frame_files = sorted(animation_folder.glob("*.png")) + if not frame_files: + print("No frames found for animation '%s', skipping" % animation_folder.name) + continue + + settings_file = list(animation_folder.glob("*.settings")) + if len(settings_file) != 1: + print("Need exactly one *.fps file for animation %s, %i found, skipping" % ( + animation_folder.name, len(settings_file))) + continue + + settings_file = settings_file[0] + with settings_file.open() as settings_input: + settings = yaml.load(settings_input) + + # require at least a 'default' dictionary with all keys, if not we + # can't properly store the data + required_settings = {"origin", "coldspot", "gunspot", "tagged", "fps"} + if "default" not in settings or type(settings["default"]) != dict or set( + settings["default"].keys()) & required_settings != required_settings: + print( + "Settings file for animation %s does not define all required properties, skipping" % animation_folder.name) + continue + + print("Importing animation '%s'" % animation_folder.name) + animation = animation_set.animations[animation_index] + animation.fps = settings["default"]["fps"] # cannot be set per-frame + + for frame_num, frame_file in enumerate(frame_files): + image = Image.open(frame_file) + if image.mode != "P": + print("Frame image %s for animation %s is not paletted, skipping" % ( + frame_file.name, animation_folder.name)) + continue + + # read the settings from default, unless they have been defined for + # this frame explicitly + frame_settings = settings.get(frame_file.stem, settings["default"]) + frame = J2A.Frame( + pixmap=image, + origin=tuple([int(x) for x in frame_settings.get("origin", settings["default"]["origin"]).split(",")]), + coldspot=tuple([int(x) for x in frame_settings.get("coldspot", settings["default"]["coldspot"]).split(",")]), + gunspot=tuple([int(x) for x in frame_settings.get("gunspot", settings["default"]["gunspot"]).split(",")]), + tagged=bool(frame_settings.get("tagged", settings["default"]["tagged"])) + ) + frame.autogenerate_mask() + + animation.frames.append(frame) + + animation_index += 1 + + animation_set.samplesbaseindex = 0 + animation_set.pack(j2a_file.config) + set_index += 1 + +j2a_file.write() +print("Done!") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..00c6a06 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pillow +pyyaml \ No newline at end of file