Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 83 additions & 38 deletions j2a-extract.py
Original file line number Diff line number Diff line change
@@ -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!")
220 changes: 135 additions & 85 deletions j2a-import.py
Original file line number Diff line number Diff line change
@@ -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!")
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pillow
pyyaml