mirror of
https://github.com/Xeeynamo/sotn-decomp.git
synced 2024-11-26 22:40:33 +00:00
8287841ccd
Ubuntu/Debian and Python recommend using virtual environments for project-specific Python dependencies and as of Ubuntu 24 this is lightly enforced when installing packages via pip. This is due to pip and the system package manager installing files to the same location which may cause either's internal state to no longer reflect what is actually installed. This updates the project to use a Python `virtualenv` for project dependencies and updates internal scripts to support both global and virtualenvs, but favors virtualenvs for new workspaces. All tools that hardcode `/usr/bin/python3` now use `env(1)` to find the first `python3` in the path. For those with a virtualenv configured, this will be the Python managed there. For everyone else, this should be the system Python or whatever other scheme they may have used previously (assuming `python3` already existed in their `PATH`). The `Makefile` has been updated to prepend `.venv/bin` to the `PATH` and use `python3` from there if it exists and has been configured. It also has a new `python-dependencies` target which will configure the venv and install all python dependnecies there. The `Dockerfile` has been updated to create an external `.venv` outside of the project directory. Python's `virtualenv`s are not relocatable and may hardcode paths which are mounted differently in the container and host. To deal with differences in paths between the container (which mounts the host's project directory to `/sotn`) host which may be at an arbitarary directory the `VENV_PATH` environment variable is used to override paths in the `Makefile`. GitHub workflows have been updated to use `.venv`.
290 lines
9.7 KiB
Python
Executable File
290 lines
9.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import json
|
|
import png
|
|
import os
|
|
|
|
|
|
def ensure_dir(file_path: str):
|
|
dir_path = os.path.dirname(file_path)
|
|
if not os.path.exists(dir_path):
|
|
os.makedirs(dir_path)
|
|
|
|
|
|
def get_num(param: int | str | None) -> int:
|
|
if not param:
|
|
return 0
|
|
if isinstance(param, int):
|
|
return int(param)
|
|
elif isinstance(param, str):
|
|
if param.startswith("0x"):
|
|
return int(param[2:], 16)
|
|
else:
|
|
return int(param)
|
|
else:
|
|
raise Exception(f"type '{type(param)}' for '{param}' unhandled")
|
|
|
|
|
|
def encode_from_png(
|
|
in_file, out_file_bitmap, name_bitmap, out_file_pal, name_pal, is_append
|
|
):
|
|
def encode_pal(dst, idx, palette):
|
|
for i in range(0, len(palette)):
|
|
color = palette[i]
|
|
r = color[0]
|
|
g = color[1]
|
|
b = color[2]
|
|
a = 0x8000 if color[3] >= 64 else 0x0000
|
|
c = (r >> 3) | ((g >> 3) << 5) | ((b >> 3) << 10) | a
|
|
dst[idx + i * 2 + 0] = c & 0xFF
|
|
dst[idx + i * 2 + 1] = (c >> 8) & 0xFF
|
|
|
|
def encode_bitmap(data):
|
|
out = bytearray(int(len(data) / 2))
|
|
for i in range(0, len(out)):
|
|
out[i] = data[i * 2 + 0]
|
|
out[i] |= data[i * 2 + 1] << 4
|
|
return out
|
|
|
|
mode = "a" if is_append else "w"
|
|
img = png.Reader(in_file).read()
|
|
data = encode_bitmap(bytearray().join(img[2]))
|
|
|
|
info = img[3]
|
|
if info["planes"] != 1:
|
|
return f"'{in_file}' must be an indexed image"
|
|
|
|
palette = info["palette"]
|
|
if len(palette) > 16:
|
|
return f"'{in_file}' palette must be of 16 colors or less but found {len(palette)} colors instead"
|
|
|
|
with open(out_file_bitmap, mode) as f_out:
|
|
f_out.write(f".section .data\n")
|
|
f_out.write(f".global {name_bitmap}\n")
|
|
f_out.write(f"{name_bitmap}:\n")
|
|
for x in data:
|
|
f_out.write(f".byte 0x{x:02X}\n")
|
|
f_out.write("\n")
|
|
|
|
pal_data = bytearray(0x10 * 2)
|
|
encode_pal(pal_data, 0, palette)
|
|
with open(out_file_pal, mode) as f_out:
|
|
f_out.write(f".section .data\n")
|
|
f_out.write(f".global {name_pal}\n")
|
|
f_out.write(f"{name_pal}:\n")
|
|
f_out.write(f".short 0x{pal_data[0x01]:02X}{pal_data[0x00]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x03]:02X}{pal_data[0x02]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x05]:02X}{pal_data[0x04]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x07]:02X}{pal_data[0x06]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x09]:02X}{pal_data[0x08]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x0B]:02X}{pal_data[0x0A]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x0D]:02X}{pal_data[0x0C]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x0F]:02X}{pal_data[0x0E]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x11]:02X}{pal_data[0x10]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x13]:02X}{pal_data[0x12]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x15]:02X}{pal_data[0x14]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x17]:02X}{pal_data[0x16]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x19]:02X}{pal_data[0x18]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x1B]:02X}{pal_data[0x1A]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x1D]:02X}{pal_data[0x1C]:02X}\n")
|
|
f_out.write(f".short 0x{pal_data[0x1F]:02X}{pal_data[0x1E]:02X}\n")
|
|
f_out.write("\n")
|
|
|
|
return None
|
|
|
|
|
|
def decode_to_png(bmp_name, png_name, x, y, w, h, bpp, stride, bmp_offset, pal):
|
|
size = int(stride * h)
|
|
with open(bmp_name, "rb") as f_bmp:
|
|
f_bmp.seek(get_num(bmp_offset) + stride * y)
|
|
bmp_data = f_bmp.read(size)
|
|
|
|
pixels = []
|
|
if bpp == 4:
|
|
src = bmp_data
|
|
x >>= 1
|
|
for j in range(0, h):
|
|
row = []
|
|
for i in range(0, w >> 1):
|
|
lo = src[x + i] & 15
|
|
hi = src[x + i] >> 4
|
|
row.extend([lo, hi])
|
|
src = src[stride:]
|
|
pixels.append(row)
|
|
else:
|
|
src = bmp_data
|
|
for j in range(0, h):
|
|
pixels.append(src[:w])
|
|
src = src[w:]
|
|
|
|
if pal:
|
|
pal_name, pal_offset, alpha = pal
|
|
with open(pal_name, "rb") as f_pal:
|
|
f_pal.seek(get_num(pal_offset))
|
|
pal_data = f_pal.read(0x20 if bpp == 4 else 0x200)
|
|
palette = []
|
|
for i in range(0, int(len(pal_data) / 2)):
|
|
color = pal_data[i * 2] + pal_data[i * 2 + 1] * 256
|
|
r = (color & 31) << 3
|
|
g = ((color >> 5) & 31) << 3
|
|
b = ((color >> 10) & 31) << 3
|
|
r |= r >> 5 # enrich the colors by
|
|
g |= g >> 5 # filling the lowest three bits
|
|
b |= b >> 5 # with the upper bits
|
|
a = 0 if color < 0x8000 and alpha == True else 255
|
|
palette.append((r, g, b, a))
|
|
else:
|
|
if bpp == 4:
|
|
palette = [(i * 16 + i, i * 16 + i, i * 16 + i) for i in range(16)]
|
|
elif bpp == 8:
|
|
palette = [(i, i, i) for i in range(0, 256)]
|
|
|
|
with open(png_name, "wb") as f_png:
|
|
writer = png.Writer(width=w, height=h, bitdepth=bpp, palette=palette)
|
|
writer.write(f_png, pixels)
|
|
return None
|
|
|
|
|
|
def decode_batch(input, source_dir, output):
|
|
with open(input, "r") as f:
|
|
config = json.load(f)
|
|
|
|
source = os.path.join(source_dir, config["source"])
|
|
clut = get_num(config["clut"])
|
|
stride = get_num(config["stride"])
|
|
for item in config["items"]:
|
|
name = os.path.join(output, item["name"] + ".png")
|
|
ensure_dir(name)
|
|
|
|
if "palette" in item and "brute" not in item["palette"]:
|
|
palette = item["palette"]
|
|
if "source" in palette:
|
|
pal_source = os.path.join(source_dir, palette["source"])
|
|
offset = get_num(palette["offset"])
|
|
else:
|
|
pal_source = source
|
|
offset = clut + get_num(palette["offset"]) * 0x20
|
|
use_alpha = "alpha" in palette and palette["alpha"] == True
|
|
pal = (pal_source, offset, use_alpha)
|
|
else:
|
|
pal = None
|
|
|
|
err = decode_to_png(
|
|
source,
|
|
name,
|
|
get_num(item["x"]) if "x" in item else 0,
|
|
get_num(item["y"]) if "y" in item else 0,
|
|
item["width"],
|
|
item["height"],
|
|
item["bpp"],
|
|
stride,
|
|
get_num(item["offset"]),
|
|
pal,
|
|
)
|
|
|
|
if "palette" in item and "brute" in item["palette"]:
|
|
for i in range(0, 256):
|
|
err = decode_to_png(
|
|
source,
|
|
os.path.join(output, item["name"] + f"_{i}" + ".png"),
|
|
get_num(item["x"]) if "x" in item else 0,
|
|
get_num(item["y"]) if "y" in item else 0,
|
|
item["width"],
|
|
item["height"],
|
|
item["bpp"],
|
|
stride,
|
|
get_num(item["offset"]),
|
|
(source, clut + i * 0x20, False),
|
|
)
|
|
|
|
if err:
|
|
return err
|
|
return None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description="convert a PNG to an assemblable GNU-friendly data file"
|
|
)
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
encode_parser = subparsers.add_parser(
|
|
"encode",
|
|
description="Encode a PNG file and its palette into binary or assemblable data",
|
|
)
|
|
encode_parser.add_argument("input")
|
|
encode_parser.add_argument("output_bmp")
|
|
encode_parser.add_argument("name_bmp")
|
|
encode_parser.add_argument("output_pal")
|
|
encode_parser.add_argument("name_pal")
|
|
|
|
decode_parser = subparsers.add_parser(
|
|
"decode",
|
|
description="Decode a binary file into a PNG file",
|
|
)
|
|
decode_parser.add_argument(
|
|
"input",
|
|
type=str,
|
|
help="the binary file that contains the bitmap to decode as PNG",
|
|
)
|
|
decode_parser.add_argument("output", type=str, help="output name of the PNG")
|
|
decode_parser.add_argument(
|
|
"width", type=int, help="width in pixel of the expected PNG output"
|
|
)
|
|
decode_parser.add_argument(
|
|
"height", type=int, help="height in pixel of the expected PNG output"
|
|
)
|
|
decode_parser.add_argument(
|
|
"depth", type=int, choices=[4, 8], help="how many bits per pixel"
|
|
)
|
|
decode_parser.add_argument(
|
|
"offset", help="source of the bitmap image to convert from"
|
|
)
|
|
decode_parser.add_argument(
|
|
"-p",
|
|
"--palette",
|
|
nargs=3,
|
|
metavar=("palette.bin", "offset", "alpha"),
|
|
help="load palette from the specified file and a given offset",
|
|
)
|
|
|
|
bdecode_parser = subparsers.add_parser(
|
|
"bdecode",
|
|
description="Decode assets in batch using a configuration file",
|
|
)
|
|
bdecode_parser.add_argument("input", type=str, help="configuration file name")
|
|
bdecode_parser.add_argument("source", type=str, help="location of original assets")
|
|
bdecode_parser.add_argument("output", type=str, help="output directory")
|
|
|
|
args = parser.parse_args()
|
|
if args.command == "encode":
|
|
err = encode_from_png(
|
|
args.input,
|
|
args.output_bmp,
|
|
args.name_bmp,
|
|
args.output_pal,
|
|
args.name_pal,
|
|
False,
|
|
)
|
|
elif args.command == "decode":
|
|
stride = args.width
|
|
if args.depth == 4:
|
|
stride >>= 1
|
|
err = decode_to_png(
|
|
args.input,
|
|
args.output,
|
|
0,
|
|
0,
|
|
args.width,
|
|
args.height,
|
|
args.depth,
|
|
stride,
|
|
args.offset,
|
|
args.palette,
|
|
)
|
|
elif args.command == "bdecode":
|
|
err = decode_batch(args.input, args.source, args.output)
|
|
if err:
|
|
raise Exception(err)
|