diff --git a/bemani/utils/afputils.py b/bemani/utils/afputils.py
index 9433408..ff5c033 100644
--- a/bemani/utils/afputils.py
+++ b/bemani/utils/afputils.py
@@ -8,13 +8,13 @@ import os.path
import sys
import textwrap
from PIL import Image, ImageDraw # type: ignore
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
from bemani.format.afp import TXP2File, Shape, SWF, Frame, Tag, AP2DoActionTag, AP2PlaceObjectTag, AP2DefineSpriteTag, AFPRenderer, Color, Matrix
from bemani.format import IFS
-def write_bytecode(swf: SWF, directory: str, verbose: bool=False) -> None:
+def write_bytecode(swf: SWF, directory: str, *, verbose: bool) -> None:
# Actually place the files down.
os.makedirs(directory, exist_ok=True)
@@ -80,6 +80,591 @@ def parse_intlist(data: str) -> List[int]:
return sorted(set(ints))
+def extract_txp2(
+ fname: str,
+ output_dir: str,
+ *,
+ split_textures: bool=False,
+ generate_mapping_overlays: bool=False,
+ write_mappings: bool=False,
+ write_raw: bool=False,
+ write_binaries: bool=False,
+ pretend: bool=False,
+ verbose: bool=False,
+) -> int:
+ if split_textures:
+ if write_raw:
+ raise Exception("Cannot write raw textures when splitting sprites!")
+ if generate_mapping_overlays:
+ raise Exception("Cannot generate mapping overlays when splitting sprites!")
+
+ with open(fname, "rb") as bfp:
+ afpfile = TXP2File(bfp.read(), verbose=verbose)
+
+ # Actually place the files down.
+ os.makedirs(output_dir, exist_ok=True)
+
+ if not split_textures:
+ for texture in afpfile.textures:
+ filename = os.path.join(output_dir, texture.name)
+
+ if texture.img:
+ if pretend:
+ print(f"Would write {filename}.png texture...")
+ else:
+ print(f"Writing {filename}.png texture...")
+ with open(f"{filename}.png", "wb") as bfp:
+ texture.img.save(bfp, format='PNG')
+
+ if not texture.img or write_raw:
+ if pretend:
+ print(f"Would write {filename}.raw texture...")
+ else:
+ print(f"Writing {filename}.raw texture...")
+ with open(f"{filename}.raw", "wb") as bfp:
+ bfp.write(texture.raw)
+
+ if pretend:
+ print(f"Would write {filename}.xml texture info...")
+ else:
+ print(f"Writing {filename}.xml texture info...")
+ with open(f"{filename}.xml", "w") as sfp:
+ sfp.write(textwrap.dedent(f"""
+
+ {texture.width}
+ {texture.height}
+ {hex(texture.fmt)}
+ {filename}.raw
+
+ """).strip())
+
+ if write_mappings:
+ if not split_textures:
+ for i, name in enumerate(afpfile.regionmap.entries):
+ if i < 0 or i >= len(afpfile.texture_to_region):
+ raise Exception(f"Out of bounds region {i}")
+ region = afpfile.texture_to_region[i]
+ texturename = afpfile.texturemap.entries[region.textureno]
+ filename = os.path.join(output_dir, name)
+
+ if pretend:
+ print(f"Would write {filename}.xml region information...")
+ else:
+ print(f"Writing {filename}.xml region information...")
+ with open(f"{filename}.xml", "w") as sfp:
+ sfp.write(textwrap.dedent(f"""
+
+ {region.left}
+ {region.top}
+ {region.right}
+ {region.bottom}
+ {texturename}
+
+ """).strip())
+
+ if afpfile.fontdata is not None:
+ filename = os.path.join(output_dir, "fontinfo.xml")
+
+ if pretend:
+ print(f"Would write {filename} font information...")
+ else:
+ print(f"Writing {filename} font information...")
+ with open(filename, "w") as sfp:
+ sfp.write(str(afpfile.fontdata))
+
+ if write_binaries:
+ for i, name in enumerate(afpfile.swfmap.entries):
+ swf = afpfile.swfdata[i]
+ filename = os.path.join(output_dir, name)
+
+ if pretend:
+ print(f"Would write {filename}.afp SWF data...")
+ print(f"Would write {filename}.bsi SWF descramble data...")
+ else:
+ print(f"Writing {filename}.afp SWF data...")
+ with open(f"{filename}.afp", "wb") as bfp:
+ bfp.write(swf.data)
+ print(f"Writing {filename}.bsi SWF descramble data...")
+ with open(f"{filename}.bsi", "wb") as bfp:
+ bfp.write(swf.descramble_info)
+
+ for i, name in enumerate(afpfile.shapemap.entries):
+ shape = afpfile.shapes[i]
+ filename = os.path.join(output_dir, f"{name}.geo")
+
+ if pretend:
+ print(f"Would write {filename} shape data...")
+ else:
+ print(f"Writing {filename} shape data...")
+ with open(filename, "wb") as bfp:
+ bfp.write(shape.data)
+
+ if generate_mapping_overlays:
+ overlays: Dict[str, Any] = {}
+
+ for i, name in enumerate(afpfile.regionmap.entries):
+ if i < 0 or i >= len(afpfile.texture_to_region):
+ raise Exception(f"Out of bounds region {i}")
+ region = afpfile.texture_to_region[i]
+ texturename = afpfile.texturemap.entries[region.textureno]
+
+ if texturename not in overlays:
+ for texture in afpfile.textures:
+ if texture.name == texturename:
+ overlays[texturename] = Image.new(
+ 'RGBA',
+ (texture.width, texture.height),
+ (0, 0, 0, 0),
+ )
+ break
+ else:
+ raise Exception(f"Couldn't find texture {texturename}")
+
+ draw = ImageDraw.Draw(overlays[texturename])
+ draw.rectangle(
+ ((region.left // 2, region.top // 2), (region.right // 2, region.bottom // 2)),
+ fill=(0, 0, 0, 0),
+ outline=(255, 0, 0, 255),
+ width=1,
+ )
+ draw.text(
+ (region.left // 2, region.top // 2),
+ name,
+ fill=(255, 0, 255, 255),
+ )
+
+ for name, img in overlays.items():
+ filename = os.path.join(output_dir, name) + "_overlay.png"
+ if pretend:
+ print(f"Would write {filename} overlay...")
+ else:
+ print(f"Writing {filename} overlay...")
+ with open(filename, "wb") as bfp:
+ img.save(bfp, format='PNG')
+
+ if split_textures:
+ textures: Dict[str, Any] = {}
+ announced: Dict[str, bool] = {}
+
+ for i, name in enumerate(afpfile.regionmap.entries):
+ if i < 0 or i >= len(afpfile.texture_to_region):
+ raise Exception(f"Out of bounds region {i}")
+ region = afpfile.texture_to_region[i]
+ texturename = afpfile.texturemap.entries[region.textureno]
+
+ if texturename not in textures:
+ for tex in afpfile.textures:
+ if tex.name == texturename:
+ textures[texturename] = tex
+ break
+ else:
+ raise Exception("Could not find texture {texturename} to split!")
+
+ if textures[texturename].img:
+ # Grab the location in the image, save it out to a new file.
+ filename = f"{texturename}_{name}.png"
+ filename = os.path.join(output_dir, filename)
+
+ if pretend:
+ print(f"Would write {filename} sprite...")
+ else:
+ print(f"Writing {filename} sprite...")
+ sprite = textures[texturename].img.crop(
+ (region.left // 2, region.top // 2, region.right // 2, region.bottom // 2),
+ )
+ with open(filename, "wb") as bfp:
+ sprite.save(bfp, format='PNG')
+ else:
+ if not announced.get(texturename, False):
+ print(f"Cannot extract sprites from {texturename} because it is not a supported format!")
+ announced[texturename] = True
+ if write_bytecode:
+ for swf in afpfile.swfdata:
+ write_bytecode(swf, output_dir, verbose=verbose)
+
+ return 0
+
+
+def update_txp2(fname: str, update_dir: str, *, pretend: bool=False, verbose: bool=False) -> int:
+ # First, parse the file out
+ with open(fname, "rb") as bfp:
+ afpfile = TXP2File(bfp.read(), verbose=verbose)
+
+ # Now, find any PNG files that match texture names.
+ for texture in afpfile.textures:
+ filename = os.path.join(update_dir, texture.name) + ".png"
+
+ if os.path.isfile(filename):
+ print(f"Updating {texture.name} from {filename}...")
+
+ with open(filename, "rb") as bfp:
+ afpfile.update_texture(texture.name, bfp.read())
+
+ # Now, find any PNG files that match a specific sprite.
+ for i, spritename in enumerate(afpfile.regionmap.entries):
+ if i < 0 or i >= len(afpfile.texture_to_region):
+ raise Exception(f"Out of bounds region {i}")
+ region = afpfile.texture_to_region[i]
+ texturename = afpfile.texturemap.entries[region.textureno]
+
+ # Grab the location in the image to see if it exists.
+ filename = f"{texturename}_{spritename}.png"
+ filename = os.path.join(update_dir, filename)
+
+ if os.path.isfile(filename):
+ print(f"Updating {texturename} sprite piece {spritename} from {filename}...")
+
+ with open(filename, "rb") as bfp:
+ afpfile.update_sprite(texturename, spritename, bfp.read())
+
+ # Now, write out the updated file
+ if pretend:
+ print(f"Would write {fname}...")
+ afpfile.unparse()
+ else:
+ print(f"Writing {fname}...")
+ data = afpfile.unparse()
+ with open(fname, "wb") as bfp:
+ bfp.write(data)
+
+ return 0
+
+
+def print_txp2(fname: str, *, decompile_bytecode: bool=False, verbose: bool=False) -> int:
+ # First, parse the file out
+ with open(fname, "rb") as bfp:
+ afpfile = TXP2File(bfp.read(), verbose=verbose)
+
+ # Now, print it
+ print(json.dumps(afpfile.as_dict(decompile_bytecode=decompile_bytecode, verbose=verbose), sort_keys=True, indent=4))
+
+ return 0
+
+
+def parse_afp(afp: str, bsi: str, *, decompile_bytecode: bool=False, verbose: bool=False) -> int:
+ # First, load the AFP and BSI files
+ with open(afp, "rb") as bafp:
+ with open(bsi, "rb") as bbsi:
+ swf = SWF("", bafp.read(), bbsi.read())
+
+ # Now, print it
+ swf.parse(verbose=verbose)
+ print(json.dumps(swf.as_dict(decompile_bytecode=decompile_bytecode, verbose=verbose), sort_keys=True, indent=4))
+
+ return 0
+
+
+def decompile_afp(afp: str, bsi: str, output_dir: str, *, decompile_bytecode: bool=False, verbose: bool=False) -> int:
+ # First, load the AFP and BSI files
+ with open(afp, "rb") as bafp:
+ with open(bsi, "rb") as bbsi:
+ swf = SWF("", bafp.read(), bbsi.read())
+
+ # Now, decompile it
+ swf.parse(verbose=verbose)
+ write_bytecode(swf, output_dir, verbose=verbose)
+
+ return 0
+
+
+def parse_geo(geo: str, *, verbose: bool=False) -> int:
+ # First, load the AFP and BSI files
+ with open(geo, "rb") as bfp:
+ shape = Shape("", bfp.read())
+
+ # Now, print it
+ shape.parse()
+ if verbose:
+ print(shape, file=sys.stderr)
+ print(json.dumps(shape.as_dict(), sort_keys=True, indent=4))
+
+ return 0
+
+
+def load_containers(renderer: AFPRenderer, containers: List[str], verbose: bool) -> None:
+ # This is a complicated one, as we need to be able to specify multiple
+ # directories of files as well as support IFS files and TXP2 files.
+ for container in containers:
+ # TODO: Allow specifying individual folders and such.
+ with open(container, "rb") as bfp:
+ data = bfp.read()
+
+ afpfile = None
+ try:
+ afpfile = TXP2File(data, verbose=verbose)
+ except Exception:
+ pass
+
+ if afpfile is not None:
+ if verbose:
+ print(f"Loading files out of TXP2 container {container}...", file=sys.stderr)
+
+ # First, load GE2D structures into the renderer.
+ for i, name in enumerate(afpfile.shapemap.entries):
+ shape = afpfile.shapes[i]
+ renderer.add_shape(name, shape)
+
+ if verbose:
+ print(f"Added {name} to SWF shape library.", file=sys.stderr)
+
+ # Now, split and load textures into the renderer.
+ sheets: Dict[str, Any] = {}
+
+ for i, name in enumerate(afpfile.regionmap.entries):
+ if i < 0 or i >= len(afpfile.texture_to_region):
+ raise Exception(f"Out of bounds region {i}")
+ region = afpfile.texture_to_region[i]
+ texturename = afpfile.texturemap.entries[region.textureno]
+
+ if texturename not in sheets:
+ for tex in afpfile.textures:
+ if tex.name == texturename:
+ sheets[texturename] = tex
+ break
+ else:
+ raise Exception("Could not find texture {texturename} to split!")
+
+ if sheets[texturename].img:
+ sprite = sheets[texturename].img.crop(
+ (region.left // 2, region.top // 2, region.right // 2, region.bottom // 2),
+ )
+ renderer.add_texture(name, sprite)
+
+ if verbose:
+ print(f"Added {name} to SWF texture library.", file=sys.stderr)
+ else:
+ print(f"Cannot load {name} from {texturename} because it is not a supported format!")
+
+ # Finally, load the SWF data itself into the renderer.
+ for i, name in enumerate(afpfile.swfmap.entries):
+ swf = afpfile.swfdata[i]
+ renderer.add_swf(name, swf)
+
+ if verbose:
+ print(f"Added {name} to SWF library.", file=sys.stderr)
+
+ continue
+
+ ifsfile = None
+ try:
+ ifsfile = IFS(data, decode_textures=True)
+ except Exception:
+ pass
+
+ if ifsfile is not None:
+ if verbose:
+ print(f"Loading files out of IFS container {container}...", file=sys.stderr)
+ for fname in ifsfile.filenames:
+ if fname.startswith(f"geo{os.sep}"):
+ # Trim off directory.
+ shapename = fname[(3 + len(os.sep)):]
+
+ # Load file, register it.
+ fdata = ifsfile.read_file(fname)
+ shape = Shape(shapename, fdata)
+ renderer.add_shape(shapename, shape)
+
+ if verbose:
+ print(f"Added {shapename} to SWF shape library.", file=sys.stderr)
+ elif fname.startswith(f"tex{os.sep}") and fname.endswith(".png"):
+ # Trim off directory, png extension.
+ texname = fname[(3 + len(os.sep)):][:-4]
+
+ # Load file, register it.
+ fdata = ifsfile.read_file(fname)
+ tex = Image.open(io.BytesIO(fdata))
+ renderer.add_texture(texname, tex)
+
+ if verbose:
+ print(f"Added {texname} to SWF texture library.", file=sys.stderr)
+ elif fname.startswith(f"afp{os.sep}"):
+ # Trim off directory, see if it has a corresponding bsi.
+ afpname = fname[(3 + len(os.sep)):]
+ bsipath = f"afp{os.sep}bsi{os.sep}{afpname}"
+
+ if bsipath in ifsfile.filenames:
+ afpdata = ifsfile.read_file(fname)
+ bsidata = ifsfile.read_file(bsipath)
+ flash = SWF(afpname, afpdata, bsidata)
+ renderer.add_swf(afpname, flash)
+
+ if verbose:
+ print(f"Added {afpname} to SWF library.", file=sys.stderr)
+ continue
+
+
+def list_paths(containers: List[str], *, verbose: bool=False) -> int:
+ renderer = AFPRenderer()
+ load_containers(renderer, containers, verbose=verbose)
+
+ for path in renderer.list_paths(verbose=verbose):
+ print(path)
+
+ return 0
+
+
+def render_path(
+ containers: List[str],
+ path: str,
+ output: str,
+ *,
+ disable_threads: bool = False,
+ background_color: Optional[str] = None,
+ background_image: Optional[str] = None,
+ force_aspect_ratio: Optional[str] = None,
+ scale_width: float = 1.0,
+ scale_height: float = 1.0,
+ only_depths: Optional[str] = None,
+ only_frames: Optional[str] = None,
+ verbose: bool = False,
+) -> int:
+ renderer = AFPRenderer(single_threaded=disable_threads)
+ load_containers(renderer, containers, verbose=verbose)
+
+ # Verify the correct params.
+ if output.lower().endswith(".gif"):
+ fmt = "GIF"
+ elif output.lower().endswith(".webp"):
+ fmt = "WEBP"
+ elif output.lower().endswith(".png"):
+ fmt = "PNG"
+ else:
+ raise Exception("Unrecognized file extension for output!")
+
+ # Allow overriding background color.
+ if background_color:
+ colorvals = background_color.split(",")
+ if len(colorvals) not in [3, 4]:
+ raise Exception("Invalid color, specify a color as a comma-separated RGB or RGBA value!")
+
+ if len(colorvals) == 3:
+ colorvals.append("255")
+ colorints = [int(c.strip()) for c in colorvals]
+ for c in colorints:
+ if c < 0 or c > 255:
+ raise Exception("Color values should be between 0 and 255!")
+
+ color = Color(*[c / 255.0 for c in colorints])
+ else:
+ color = None
+
+ # Allow inserting a background image.
+ if background_image:
+ background = Image.open(background_image)
+ else:
+ background = None
+
+ # Calculate the size of the SWF so we can apply scaling options.
+ swf_location = renderer.compute_path_location(path)
+ requested_width = swf_location.width
+ requested_height = swf_location.height
+
+ # Allow overriding the aspect ratio.
+ if force_aspect_ratio:
+ ratio = 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
+
+ requested_width *= scale_width
+ requested_height *= scale_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,
+ )
+
+ # Support rendering only certain depth planes.
+ if only_depths is not None:
+ requested_depths = parse_intlist(only_depths)
+ else:
+ requested_depths = None
+
+ # Support rendering only certain frames.
+ if only_frames is not None:
+ requested_frames = parse_intlist(only_frames)
+ else:
+ requested_frames = None
+
+ if fmt in ["GIF", "WEBP"]:
+ # Write all the frames out in one file.
+ duration = renderer.compute_path_frame_duration(path)
+ images = list(
+ renderer.render_path(
+ path,
+ verbose=verbose,
+ background_color=color,
+ background_image=background,
+ only_depths=requested_depths,
+ only_frames=requested_frames,
+ movie_transform=transform,
+ )
+ )
+ if len(images) == 0:
+ raise Exception("Did not render any frames!")
+
+ with open(output, "wb") as bfp:
+ images[0].save(bfp, format=fmt, save_all=True, append_images=images[1:], duration=duration, optimize=True)
+
+ print(f"Wrote animation to {output}")
+ else:
+ # Write all the frames out in individual_files.
+ filename = output[:-4]
+ ext = output[-4:]
+
+ # Figure out padding for the images.
+ frames = renderer.compute_path_frames(path)
+ if frames > 0:
+ digits = f"0{int(math.log10(frames)) + 1}"
+
+ for i, img in enumerate(
+ renderer.render_path(
+ path,
+ verbose=verbose,
+ background_color=color,
+ background_image=background,
+ only_depths=requested_depths,
+ only_frames=requested_frames,
+ movie_transform=transform,
+ )
+ ):
+ fullname = f"{filename}-{i:{digits}}{ext}"
+
+ with open(fullname, "wb") as bfp:
+ img.save(bfp, format=fmt)
+
+ print(f"Wrote animation frame to {fullname}")
+
+ return 0
+
+
def main() -> int:
parser = argparse.ArgumentParser(description="Konami AFP graphic file unpacker/repacker")
subparsers = parser.add_subparsers(help='Action to take', dest='action')
@@ -344,540 +929,46 @@ def main() -> int:
args = parser.parse_args()
if args.action == "extract":
- if args.split_textures:
- if args.write_raw:
- raise Exception("Cannot write raw textures when splitting sprites!")
- if args.generate_mapping_overlays:
- raise Exception("Cannot generate mapping overlays when splitting sprites!")
-
- with open(args.file, "rb") as bfp:
- afpfile = TXP2File(bfp.read(), verbose=args.verbose)
-
- # Actually place the files down.
- os.makedirs(args.dir, exist_ok=True)
-
- if not args.split_textures:
- for texture in afpfile.textures:
- filename = os.path.join(args.dir, texture.name)
-
- if texture.img:
- if args.pretend:
- print(f"Would write {filename}.png texture...")
- else:
- print(f"Writing {filename}.png texture...")
- with open(f"{filename}.png", "wb") as bfp:
- texture.img.save(bfp, format='PNG')
-
- if not texture.img or args.write_raw:
- if args.pretend:
- print(f"Would write {filename}.raw texture...")
- else:
- print(f"Writing {filename}.raw texture...")
- with open(f"{filename}.raw", "wb") as bfp:
- bfp.write(texture.raw)
-
- if args.pretend:
- print(f"Would write {filename}.xml texture info...")
- else:
- print(f"Writing {filename}.xml texture info...")
- with open(f"{filename}.xml", "w") as sfp:
- sfp.write(textwrap.dedent(f"""
-
- {texture.width}
- {texture.height}
- {hex(texture.fmt)}
- {filename}.raw
-
- """).strip())
-
- if args.write_mappings:
- if not args.split_textures:
- for i, name in enumerate(afpfile.regionmap.entries):
- if i < 0 or i >= len(afpfile.texture_to_region):
- raise Exception(f"Out of bounds region {i}")
- region = afpfile.texture_to_region[i]
- texturename = afpfile.texturemap.entries[region.textureno]
- filename = os.path.join(args.dir, name)
-
- if args.pretend:
- print(f"Would write {filename}.xml region information...")
- else:
- print(f"Writing {filename}.xml region information...")
- with open(f"{filename}.xml", "w") as sfp:
- sfp.write(textwrap.dedent(f"""
-
- {region.left}
- {region.top}
- {region.right}
- {region.bottom}
- {texturename}
-
- """).strip())
-
- if afpfile.fontdata is not None:
- filename = os.path.join(args.dir, "fontinfo.xml")
-
- if args.pretend:
- print(f"Would write {filename} font information...")
- else:
- print(f"Writing {filename} font information...")
- with open(filename, "w") as sfp:
- sfp.write(str(afpfile.fontdata))
-
- if args.write_binaries:
- for i, name in enumerate(afpfile.swfmap.entries):
- swf = afpfile.swfdata[i]
- filename = os.path.join(args.dir, name)
-
- if args.pretend:
- print(f"Would write {filename}.afp SWF data...")
- print(f"Would write {filename}.bsi SWF descramble data...")
- else:
- print(f"Writing {filename}.afp SWF data...")
- with open(f"{filename}.afp", "wb") as bfp:
- bfp.write(swf.data)
- print(f"Writing {filename}.bsi SWF descramble data...")
- with open(f"{filename}.bsi", "wb") as bfp:
- bfp.write(swf.descramble_info)
-
- for i, name in enumerate(afpfile.shapemap.entries):
- shape = afpfile.shapes[i]
- filename = os.path.join(args.dir, f"{name}.geo")
-
- if args.pretend:
- print(f"Would write {filename} shape data...")
- else:
- print(f"Writing {filename} shape data...")
- with open(filename, "wb") as bfp:
- bfp.write(shape.data)
-
- if args.generate_mapping_overlays:
- overlays: Dict[str, Any] = {}
-
- for i, name in enumerate(afpfile.regionmap.entries):
- if i < 0 or i >= len(afpfile.texture_to_region):
- raise Exception(f"Out of bounds region {i}")
- region = afpfile.texture_to_region[i]
- texturename = afpfile.texturemap.entries[region.textureno]
-
- if texturename not in overlays:
- for texture in afpfile.textures:
- if texture.name == texturename:
- overlays[texturename] = Image.new(
- 'RGBA',
- (texture.width, texture.height),
- (0, 0, 0, 0),
- )
- break
- else:
- raise Exception(f"Couldn't find texture {texturename}")
-
- draw = ImageDraw.Draw(overlays[texturename])
- draw.rectangle(
- ((region.left // 2, region.top // 2), (region.right // 2, region.bottom // 2)),
- fill=(0, 0, 0, 0),
- outline=(255, 0, 0, 255),
- width=1,
- )
- draw.text(
- (region.left // 2, region.top // 2),
- name,
- fill=(255, 0, 255, 255),
- )
-
- for name, img in overlays.items():
- filename = os.path.join(args.dir, name) + "_overlay.png"
- if args.pretend:
- print(f"Would write {filename} overlay...")
- else:
- print(f"Writing {filename} overlay...")
- with open(filename, "wb") as bfp:
- img.save(bfp, format='PNG')
-
- if args.split_textures:
- textures: Dict[str, Any] = {}
- announced: Dict[str, bool] = {}
-
- for i, name in enumerate(afpfile.regionmap.entries):
- if i < 0 or i >= len(afpfile.texture_to_region):
- raise Exception(f"Out of bounds region {i}")
- region = afpfile.texture_to_region[i]
- texturename = afpfile.texturemap.entries[region.textureno]
-
- if texturename not in textures:
- for tex in afpfile.textures:
- if tex.name == texturename:
- textures[texturename] = tex
- break
- else:
- raise Exception("Could not find texture {texturename} to split!")
-
- if textures[texturename].img:
- # Grab the location in the image, save it out to a new file.
- filename = f"{texturename}_{name}.png"
- filename = os.path.join(args.dir, filename)
-
- if args.pretend:
- print(f"Would write {filename} sprite...")
- else:
- print(f"Writing {filename} sprite...")
- sprite = textures[texturename].img.crop(
- (region.left // 2, region.top // 2, region.right // 2, region.bottom // 2),
- )
- with open(filename, "wb") as bfp:
- sprite.save(bfp, format='PNG')
- else:
- if not announced.get(texturename, False):
- print(f"Cannot extract sprites from {texturename} because it is not a supported format!")
- announced[texturename] = True
- if args.write_bytecode:
- for swf in afpfile.swfdata:
- write_bytecode(swf, args.dir, verbose=args.verbose)
-
- if args.action == "update":
- # First, parse the file out
- with open(args.file, "rb") as bfp:
- afpfile = TXP2File(bfp.read(), verbose=args.verbose)
-
- # Now, find any PNG files that match texture names.
- for texture in afpfile.textures:
- filename = os.path.join(args.dir, texture.name) + ".png"
-
- if os.path.isfile(filename):
- print(f"Updating {texture.name} from {filename}...")
-
- with open(filename, "rb") as bfp:
- afpfile.update_texture(texture.name, bfp.read())
-
- # Now, find any PNG files that match a specific sprite.
- for i, spritename in enumerate(afpfile.regionmap.entries):
- if i < 0 or i >= len(afpfile.texture_to_region):
- raise Exception(f"Out of bounds region {i}")
- region = afpfile.texture_to_region[i]
- texturename = afpfile.texturemap.entries[region.textureno]
-
- # Grab the location in the image to see if it exists.
- filename = f"{texturename}_{spritename}.png"
- filename = os.path.join(args.dir, filename)
-
- if os.path.isfile(filename):
- print(f"Updating {texturename} sprite piece {spritename} from {filename}...")
-
- with open(filename, "rb") as bfp:
- afpfile.update_sprite(texturename, spritename, bfp.read())
-
- # Now, write out the updated file
- if args.pretend:
- print(f"Would write {args.file}...")
- afpfile.unparse()
- else:
- print(f"Writing {args.file}...")
- data = afpfile.unparse()
- with open(args.file, "wb") as bfp:
- bfp.write(data)
-
- if args.action == "print":
- # First, parse the file out
- with open(args.file, "rb") as bfp:
- afpfile = TXP2File(bfp.read(), verbose=args.verbose)
-
- # Now, print it
- print(json.dumps(afpfile.as_dict(decompile_bytecode=args.decompile_bytecode, verbose=args.verbose), sort_keys=True, indent=4))
-
- if args.action == "parseafp":
- # First, load the AFP and BSI files
- with open(args.afp, "rb") as bafp:
- with open(args.bsi, "rb") as bbsi:
- swf = SWF("", bafp.read(), bbsi.read())
-
- # Now, print it
- swf.parse(verbose=args.verbose)
- print(json.dumps(swf.as_dict(decompile_bytecode=args.decompile_bytecode, verbose=args.verbose), sort_keys=True, indent=4))
-
- if args.action == "decompile":
- # First, load the AFP and BSI files
- with open(args.afp, "rb") as bafp:
- with open(args.bsi, "rb") as bbsi:
- swf = SWF("", bafp.read(), bbsi.read())
-
- # Now, decompile it
- swf.parse(verbose=args.verbose)
- write_bytecode(swf, args.directory, verbose=args.verbose)
-
- if args.action == "parsegeo":
- # First, load the AFP and BSI files
- with open(args.geo, "rb") as bfp:
- geo = Shape("", bfp.read())
-
- # Now, print it
- geo.parse()
- if args.verbose:
- print(geo, file=sys.stderr)
- print(json.dumps(geo.as_dict(), sort_keys=True, indent=4))
-
- if args.action in ["render", "list"]:
- # This is a complicated one, as we need to be able to specify multiple
- # directories of files as well as support IFS files and TXP2 files.
- if args.action == 'render':
- renderer = AFPRenderer(single_threaded=args.disable_threads)
- else:
- renderer = AFPRenderer()
-
- # TODO: Allow specifying individual folders and such.
- for container in args.container:
- with open(container, "rb") as bfp:
- data = bfp.read()
-
- afpfile = None
- try:
- afpfile = TXP2File(data, verbose=args.verbose)
- except Exception:
- pass
-
- if afpfile is not None:
- if args.verbose:
- print(f"Loading files out of TXP2 container {container}...", file=sys.stderr)
-
- # First, load GE2D structures into the renderer.
- for i, name in enumerate(afpfile.shapemap.entries):
- shape = afpfile.shapes[i]
- renderer.add_shape(name, shape)
-
- if args.verbose:
- print(f"Added {name} to SWF shape library.", file=sys.stderr)
-
- # Now, split and load textures into the renderer.
- sheets: Dict[str, Any] = {}
-
- for i, name in enumerate(afpfile.regionmap.entries):
- if i < 0 or i >= len(afpfile.texture_to_region):
- raise Exception(f"Out of bounds region {i}")
- region = afpfile.texture_to_region[i]
- texturename = afpfile.texturemap.entries[region.textureno]
-
- if texturename not in sheets:
- for tex in afpfile.textures:
- if tex.name == texturename:
- sheets[texturename] = tex
- break
- else:
- raise Exception("Could not find texture {texturename} to split!")
-
- if sheets[texturename].img:
- sprite = sheets[texturename].img.crop(
- (region.left // 2, region.top // 2, region.right // 2, region.bottom // 2),
- )
- renderer.add_texture(name, sprite)
-
- if args.verbose:
- print(f"Added {name} to SWF texture library.", file=sys.stderr)
- else:
- print(f"Cannot load {name} from {texturename} because it is not a supported format!")
-
- # Finally, load the SWF data itself into the renderer.
- for i, name in enumerate(afpfile.swfmap.entries):
- swf = afpfile.swfdata[i]
- renderer.add_swf(name, swf)
-
- if args.verbose:
- print(f"Added {name} to SWF library.", file=sys.stderr)
-
- continue
-
- ifsfile = None
- try:
- ifsfile = IFS(data, decode_textures=True)
- except Exception:
- pass
-
- if ifsfile is not None:
- if args.verbose:
- print(f"Loading files out of IFS container {container}...", file=sys.stderr)
- for fname in ifsfile.filenames:
- if fname.startswith(f"geo{os.sep}"):
- # Trim off directory.
- shapename = fname[(3 + len(os.sep)):]
-
- # Load file, register it.
- fdata = ifsfile.read_file(fname)
- shape = Shape(shapename, fdata)
- renderer.add_shape(shapename, shape)
-
- if args.verbose:
- print(f"Added {shapename} to SWF shape library.", file=sys.stderr)
- elif fname.startswith(f"tex{os.sep}") and fname.endswith(".png"):
- # Trim off directory, png extension.
- texname = fname[(3 + len(os.sep)):][:-4]
-
- # Load file, register it.
- fdata = ifsfile.read_file(fname)
- tex = Image.open(io.BytesIO(fdata))
- renderer.add_texture(texname, tex)
-
- if args.verbose:
- print(f"Added {texname} to SWF texture library.", file=sys.stderr)
- elif fname.startswith(f"afp{os.sep}"):
- # Trim off directory, see if it has a corresponding bsi.
- afpname = fname[(3 + len(os.sep)):]
- bsipath = f"afp{os.sep}bsi{os.sep}{afpname}"
-
- if bsipath in ifsfile.filenames:
- afpdata = ifsfile.read_file(fname)
- bsidata = ifsfile.read_file(bsipath)
- flash = SWF(afpname, afpdata, bsidata)
- renderer.add_swf(afpname, flash)
-
- if args.verbose:
- print(f"Added {afpname} to SWF library.", file=sys.stderr)
- continue
-
- if args.action == "render":
- # Verify the correct params.
- if args.output.lower().endswith(".gif"):
- fmt = "GIF"
- elif args.output.lower().endswith(".webp"):
- fmt = "WEBP"
- elif args.output.lower().endswith(".png"):
- fmt = "PNG"
- else:
- raise Exception("Unrecognized file extension for output!")
-
- # Allow overriding background color.
- if args.background_color:
- colorvals = args.background_color.split(",")
- if len(colorvals) not in [3, 4]:
- raise Exception("Invalid color, specify a color as a comma-separated RGB or RGBA value!")
-
- if len(colorvals) == 3:
- colorvals.append("255")
- colorints = [int(c.strip()) for c in colorvals]
- for c in colorints:
- if c < 0 or c > 255:
- raise Exception("Color values should be between 0 and 255!")
-
- color = Color(*[c / 255.0 for c in colorints])
- else:
- color = None
-
- # Allow inserting a background image.
- if args.background_image:
- background = Image.open(args.background_image)
- else:
- background = 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
-
- requested_width *= args.scale_width
- requested_height *= args.scale_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,
- )
-
- # Support rendering only certain depth planes.
- if args.only_depths is not None:
- requested_depths = parse_intlist(args.only_depths)
- else:
- requested_depths = None
-
- # Support rendering only certain frames.
- if args.only_frames is not None:
- requested_frames = parse_intlist(args.only_frames)
- else:
- requested_frames = None
-
- if fmt in ["GIF", "WEBP"]:
- # Write all the frames out in one file.
- duration = renderer.compute_path_frame_duration(args.path)
- images = list(
- renderer.render_path(
- args.path,
- verbose=args.verbose,
- background_color=color,
- background_image=background,
- only_depths=requested_depths,
- only_frames=requested_frames,
- movie_transform=transform,
- )
- )
- if len(images) == 0:
- raise Exception("Did not render any frames!")
-
- with open(args.output, "wb") as bfp:
- images[0].save(bfp, format=fmt, save_all=True, append_images=images[1:], duration=duration, optimize=True)
-
- print(f"Wrote animation to {args.output}")
- else:
- # Write all the frames out in individual_files.
- filename = args.output[:-4]
- ext = args.output[-4:]
-
- # Figure out padding for the images.
- frames = renderer.compute_path_frames(args.path)
- if frames > 0:
- digits = f"0{int(math.log10(frames)) + 1}"
-
- for i, img in enumerate(
- renderer.render_path(
- args.path,
- verbose=args.verbose,
- background_color=color,
- background_image=background,
- only_depths=requested_depths,
- only_frames=requested_frames,
- movie_transform=transform,
- )
- ):
- fullname = f"{filename}-{i:{digits}}{ext}"
-
- with open(fullname, "wb") as bfp:
- img.save(bfp, format=fmt)
-
- print(f"Wrote animation frame to {fullname}")
-
- elif args.action == "list":
- for path in renderer.list_paths(verbose=args.verbose):
- print(path)
-
- return 0
+ return extract_txp2(
+ args.file,
+ args.dir,
+ split_textures=args.split_textures,
+ generate_mapping_overlays=args.generate_mapping_overlays,
+ write_mappings=args.write_mappings,
+ write_raw=args.write_raw,
+ write_binaries=args.write_binaries,
+ pretend=args.pretend,
+ verbose=args.verbose,
+ )
+ elif args.action == "update":
+ return update_txp2(args.file, args.dir, pretend=args.pretend, verbose=args.verbose)
+ elif args.action == "print":
+ return print_txp2(args.file, decompile_bytecode=args.decompile_bytecode, verbose=args.verbose)
+ elif args.action == "parseafp":
+ return parse_afp(args.afp, args.bsi, decompile_bytecode=args.decompile_bytecode, verbose=args.verbose)
+ elif args.action == "decompile":
+ return decompile_afp(args.afp, args.bsi, args.directory, decompile_bytecode=args.decompile_bytecode, verbose=args.verbose)
+ elif args.action == "parsegeo":
+ return parse_geo(args.geo, verbose=args.verbose)
+ elif args.action == "list":
+ return list_paths(args.container, verbose=args.verbose)
+ elif args.action == "render":
+ return render_path(
+ args.container,
+ args.path,
+ args.output,
+ disable_threads=args.disable_threads,
+ background_color=args.background_color,
+ background_image=args.background_image,
+ force_aspect_ratio=args.force_aspect_ratio,
+ scale_width=args.scale_width,
+ scale_height=args.scale_height,
+ only_depths=args.only_depths,
+ only_frames=args.only_frames,
+ verbose=args.verbose,
+ )
+ else:
+ raise Exception(f"Invalid action {args.action}!")
if __name__ == "__main__":