aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rwxr-xr-xtools/img2src156
-rwxr-xr-xtools/menu2src161
-rwxr-xr-xtools/txt2img222
3 files changed, 317 insertions, 222 deletions
diff --git a/tools/img2src b/tools/img2src
new file mode 100755
index 0000000..c279f4d
--- /dev/null
+++ b/tools/img2src
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+#
+"""Convert image to NXT data source file."""
+#
+# Copyright (C) 2024 Nicolas Schodet
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+import argparse
+import os
+import os.path
+import struct
+import sys
+
+import tomllib
+from PIL import Image, ImageChops
+
+
+def print_hex(indent, bytes_or_fmt, *args, wrap=8, file=None):
+ """Print data as hexadecimal, use struct format."""
+ if not args:
+ data = bytes_or_fmt
+ else:
+ data = struct.pack(bytes_or_fmt, *args)
+ for i in range(0, len(data), wrap):
+ line = ", ".join(f"{b:#04x}" for b in data[i : i + wrap])
+ print(indent + line + ",", file=file)
+
+
+def data_from_image(img):
+ """Create data in NXT bitmap format from image.
+
+ Pixels in NXT are organized as packets of 8 pixel high columns. PIL does not write
+ this format, so just transpose the image then transpose every bytes.
+ """
+ if img.height % 8 != 0:
+ raise RuntimeError("Height must be multiple of 8")
+ # Transpose.
+ img = img.transpose(Image.Transpose.TRANSPOSE)
+ # Extract data
+ data = img.tobytes("raw", "1;IR")
+ # Split in lines (corresponding to columns).
+ data = [data[i : i + img.width // 8] for i in range(0, len(data), img.width // 8)]
+ # Transpose.
+ data = zip(*data)
+ # Paste everything together.
+ data = bytes(val for band in data for val in band)
+ return data
+
+
+def crop_image(img):
+ """Crop borders to reduce image size, return cropped image and x and y offsets as
+ a tuple.
+ """
+ print(img.width, img.height)
+ imginv = ImageChops.invert(img)
+ bbox = imginv.getbbox()
+ if bbox is None:
+ # Special case for Test2 image.
+ return img, (0, 0)
+ left, upper, right, lower = bbox
+ print(left, upper, right, lower)
+ # Round down to multiple of 8.
+ upper = upper // 8 * 8
+ lower = (lower + 7) // 8 * 8
+ return img.crop((left, upper, right, lower)), (left, upper)
+
+
+def convert_bitmap(info, img_file, out_file, crop=False):
+ """Convert to BMPMAP format."""
+ img = Image.open(img_file)
+ if crop:
+ img, (crop_x, crop_y) = crop_image(img)
+ else:
+ crop_x, crop_y = 0, 0
+ data = data_from_image(img)
+ start_x = info["start_x"]
+ start_y = info["start_y"]
+ basename = os.path.basename(os.path.splitext(img_file)[0])
+ with open(out_file, "w") as f:
+ print(f"#define {basename}_size {len(data)+8}", file=f)
+ print(f"const BMPMAP {basename} =", file=f)
+ print("{", file=f)
+ print_hex(" ", ">H", 0x0200, file=f)
+ print_hex(" ", ">H", len(data), file=f)
+ print_hex(" ", "B", start_x + crop_x, file=f)
+ print_hex(" ", "B", start_y + crop_y, file=f)
+ print_hex(" ", "B", img.width, file=f)
+ print_hex(" ", "B", img.height, file=f)
+ print(" {", file=f)
+ print_hex(" ", data, file=f)
+ print(" }", file=f)
+ print("};", file=f)
+
+
+def convert_icon(info, img_file, out_file):
+ """Convert to ICON format."""
+ img = Image.open(img_file)
+ data = data_from_image(img)
+ item_pixels_x = info["item_pixels_x"]
+ item_pixels_y = info["item_pixels_y"]
+ basename = os.path.basename(os.path.splitext(out_file)[0])
+ with open(out_file, "w") as f:
+ print(f"const ICON {basename} =", file=f)
+ print("{", file=f)
+ print_hex(" ", ">H", 0x0400, file=f)
+ print_hex(" ", ">H", len(data), file=f)
+ print_hex(" ", "B", img.width // item_pixels_x, file=f)
+ print_hex(" ", "B", img.height // item_pixels_y, file=f)
+ print_hex(" ", "B", item_pixels_x, file=f)
+ print_hex(" ", "B", item_pixels_y, file=f)
+ print(" {", file=f)
+ print_hex(" ", data, file=f)
+ print(" }", file=f)
+ print("};", file=f)
+
+
+p = argparse.ArgumentParser(description=__doc__)
+p.add_argument("info", help="input TOML file")
+p.add_argument("image", help="input image")
+p.add_argument("-o", "--output", metavar="FILE", help="output header file")
+options = p.parse_args()
+
+try:
+ with open(options.info, "rb") as f:
+ info = tomllib.load(f)
+
+ if info["format"] == "bitmap":
+ convert_bitmap(info, options.image, options.output)
+ elif info["format"] == "icon":
+ convert_icon(info, options.image, options.output)
+ else:
+ raise RuntimeError("Unknown format")
+except Exception as e:
+ try:
+ os.remove(options.output)
+ except FileNotFoundError:
+ pass
+ print(e, file=sys.stderr)
+ sys.exit(1)
diff --git a/tools/menu2src b/tools/menu2src
new file mode 100755
index 0000000..cdfe466
--- /dev/null
+++ b/tools/menu2src
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+#
+"""Convert menu to NXT data source file."""
+#
+# Copyright (C) 2024 Nicolas Schodet
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+import argparse
+import os
+import os.path
+import struct
+import sys
+
+import tomllib
+
+
+def print_hex(indent, bytes_or_fmt, *args, wrap=8, file=None):
+ """Print data as hexadecimal, use struct format."""
+ if not args:
+ data = bytes_or_fmt
+ else:
+ data = struct.pack(bytes_or_fmt, *args)
+ for i in range(0, len(data), wrap):
+ line = ", ".join(f"{b:#04x}" for b in data[i : i + wrap])
+ print(indent + line + ",", file=file)
+
+
+def encode_special(flags):
+ """Encode menu special flags."""
+ special_mask = 0
+ if "skip_this_mother_id" in flags and flags["skip_this_mother_id"]:
+ special_mask |= 0x00000001
+ special_mask |= flags["skip_this_mother_id"] << 28
+ if "enter_act_as_exit" in flags and flags["enter_act_as_exit"]:
+ special_mask |= 0x00000004
+ if "back_twice" in flags and flags["back_twice"]:
+ special_mask |= 0x00000008
+ if "exit_act_as_enter" in flags and flags["exit_act_as_enter"]:
+ special_mask |= 0x00000010
+ if "leave_background" in flags and flags["leave_background"]:
+ special_mask |= 0x00000020
+ if "exit_calls_with_ff" in flags and flags["exit_calls_with_ff"]:
+ special_mask |= 0x00000040
+ if "exit_leaves_menufile" in flags and flags["exit_leaves_menufile"]:
+ special_mask |= 0x00000080
+ if "init_calls_with_0" in flags and flags["init_calls_with_0"]:
+ special_mask |= 0x00000100
+ if "left_right_as_call" in flags and flags["left_right_as_call"]:
+ special_mask |= 0x00000200
+ if "enter_only_calls" in flags and flags["enter_only_calls"]:
+ special_mask |= 0x00000400
+ if "exit_only_calls" in flags and flags["exit_only_calls"]:
+ special_mask |= 0x00000800
+ if "auto_press_enter" in flags and flags["auto_press_enter"]:
+ special_mask |= 0x00001000
+ if "enter_leaves_menufile" in flags and flags["enter_leaves_menufile"]:
+ special_mask |= 0x00002000
+ if "init_calls" in flags and flags["init_calls"]:
+ special_mask |= 0x00004000
+ if "accept_incoming_request" in flags and flags["accept_incoming_request"]:
+ special_mask |= 0x00008000
+ if "back_three_times" in flags and flags["back_three_times"]:
+ special_mask |= 0x00010000
+ if "exit_disable" in flags and flags["exit_disable"]:
+ special_mask |= 0x00020000
+ if "exit_load_pointer" in flags and flags["exit_load_pointer"]:
+ special_mask |= 0x00040000
+ special_mask |= flags["exit_load_pointer"] << 24
+ if "exit_calls" in flags and flags["exit_calls"]:
+ special_mask |= 0x00080000
+ if "init_calls_with_1" in flags and flags["init_calls_with_1"]:
+ special_mask |= 0x00100000
+ if "exit_load_menu" in flags and flags["exit_load_menu"]:
+ special_mask |= 0x00200000
+ if "only_bt_on" in flags and flags["only_bt_on"]:
+ special_mask |= 0x00400000
+ if "only_datalog_enabled" in flags and flags["only_datalog_enabled"]:
+ special_mask |= 0x00800000
+ return special_mask
+
+
+def convert_menu(info, out_file):
+ """Convert to MENU format."""
+ item_size = 0x1D
+ items = info["items"]
+ data_size = item_size * len(items)
+ item_pixels_x = info["item_pixels_x"]
+ item_pixels_y = info["item_pixels_y"]
+ basename = os.path.basename(os.path.splitext(out_file)[0])
+ with open(out_file, "w") as f:
+ print(f"const UBYTE {basename}[] =", file=f)
+ print("{", file=f)
+ print_hex(" ", ">H", 0x0700, file=f)
+ print_hex(" ", ">H", data_size, file=f)
+ print_hex(" ", "B", item_size, file=f)
+ print_hex(" ", "B", len(items), file=f)
+ print_hex(" ", "B", item_pixels_x, file=f)
+ print_hex(" ", "B", item_pixels_y, file=f)
+ for i in items:
+ print("", file=f)
+ if i["icon_text"].strip():
+ print(f" // {i['icon_text']}", file=f)
+ special_mask = 0
+ if "flags" in i:
+ special_mask = encode_special(i["flags"])
+ print_hex(
+ " ",
+ ">LLBBBB",
+ i["item_id"],
+ special_mask,
+ i["function_index"],
+ i["function_parameter"],
+ i["file_load_no"],
+ i["next_menu"],
+ wrap=4,
+ file=f,
+ )
+ print_hex(
+ " ", f"{item_size - 13}s", i["icon_text"].encode("ascii"), file=f
+ )
+ print_hex(" ", "B", i["icon_image_no"], file=f)
+ print("};", file=f)
+
+
+p = argparse.ArgumentParser(description=__doc__)
+p.add_argument("info", help="input TOML file")
+p.add_argument("-o", "--output", metavar="FILE", help="output header file")
+options = p.parse_args()
+
+try:
+ with open(options.info, "rb") as f:
+ info = tomllib.load(f)
+
+ if info["format"] == "menu":
+ convert_menu(info, options.output)
+ else:
+ raise RuntimeError("Unknown format")
+except Exception as e:
+ try:
+ os.remove(options.output)
+ except FileNotFoundError:
+ pass
+ print(e, file=sys.stderr)
+ sys.exit(1)
diff --git a/tools/txt2img b/tools/txt2img
deleted file mode 100755
index 7a89007..0000000
--- a/tools/txt2img
+++ /dev/null
@@ -1,222 +0,0 @@
-#!/usr/bin/env python3
-#
-"""Convert NXT data source files to readable files."""
-#
-# Copyright (C) 2024 Nicolas Schodet
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-#
-import argparse
-import os.path
-import struct
-
-import tomli_w
-from PIL import Image
-
-
-def image_from_data(w, h, data):
- """Create image from data.
-
- Pixels in NXT are organized as packets of 8 pixel high columns. PIL does not read
- this format, so just transpose every bytes then transpose the resulting image.
- """
- assert h % 8 == 0
- assert h / 8 * w == len(data)
- # Split in w pixel wide, 8 pixel high bands.
- data = [data[i : i + w] for i in range(0, len(data), w)]
- # Transpose.
- data = zip(*data)
- # Paste everything together.
- data = bytes(val for band in data for val in band)
- # Create the image.
- i = Image.frombytes("1", (h, w), data, "raw", "1;IR")
- # Transpose again.
- return i.transpose(Image.Transpose.TRANSPOSE)
-
-
-def decode_special(special_mask):
- """Decode menu special flags."""
- d = dict()
- if special_mask & 0x00000001:
- d["skip_this_mother_id"] = (special_mask >> 28) & 0xF
- if special_mask & 0x00000004:
- d["enter_act_as_exit"] = True
- if special_mask & 0x00000008:
- d["back_twice"] = True
- if special_mask & 0x00000010:
- d["exit_act_as_enter"] = True
- if special_mask & 0x00000020:
- d["leave_background"] = True
- if special_mask & 0x00000040:
- d["exit_calls_with_ff"] = True
- if special_mask & 0x00000080:
- d["exit_leaves_menufile"] = True
- if special_mask & 0x00000100:
- d["init_calls_with_0"] = True
- if special_mask & 0x00000200:
- d["left_right_as_call"] = True
- if special_mask & 0x00000400:
- d["enter_only_calls"] = True
- if special_mask & 0x00000800:
- d["exit_only_calls"] = True
- if special_mask & 0x00001000:
- d["auto_press_enter"] = True
- if special_mask & 0x00002000:
- d["enter_leaves_menufile"] = True
- if special_mask & 0x00004000:
- d["init_calls"] = True
- if special_mask & 0x00008000:
- d["accept_incoming_request"] = True
- if special_mask & 0x00010000:
- d["back_three_times"] = True
- if special_mask & 0x00020000:
- d["exit_disable"] = True
- if special_mask & 0x00040000:
- d["exit_load_pointer"] = (special_mask >> 24) & 0xF
- if special_mask & 0x00080000:
- d["exit_calls"] = True
- if special_mask & 0x00100000:
- d["init_calls_with_1"] = True
- if special_mask & 0x00200000:
- d["exit_load_menu"] = True
- if special_mask & 0x00400000:
- d["only_bt_on"] = True
- if special_mask & 0x00800000:
- d["only_datalog_enabled"] = True
- return d
-
-
-p = argparse.ArgumentParser(description=__doc__)
-p.add_argument("input", help="input file")
-p.add_argument("output_basename", nargs="?", help="output base file name")
-options = p.parse_args()
-
-if options.output_basename is None:
- options.output_basename = os.path.basename(os.path.splitext(options.input)[0])
-
-typ = None
-name = None
-data = []
-
-# Parse input file.
-with open(options.input) as i:
- for line in i:
- line = line.strip()
- if not line:
- continue
- if line.startswith("#define") or line.startswith("{") or line.startswith("}"):
- continue
- if line.startswith("//"):
- continue
- if typ is None:
- _, typ, name, _ = line.split()
- else:
- line = line.split("//")[0]
- data.extend(d.strip() for d in line.strip().rstrip(",").split(","))
-
-# Pack data.
-data = bytes(ord(d[1]) if d.startswith("'") else int(d, 16) for d in data)
-
-# Dump.
-if typ == "BMPMAP":
- fmt = ">HHBBBB"
- s = struct.calcsize(fmt)
- form, data_bytes, start_x, start_y, pixels_x, pixels_y = struct.unpack(
- fmt, data[0:s]
- )
- data = data[s:]
- info = dict(
- format="bitmap",
- start_x=start_x,
- start_y=start_y,
- )
- assert form == 0x0200
- # This field is garbage.
- # assert data_bytes == len(data), f"data_bytes is {data_bytes}, data is {len(data)}"
- with open(options.output_basename + ".toml", "wb") as o:
- tomli_w.dump(info, o)
- i = image_from_data(pixels_x, pixels_y, data)
- i.save(options.output_basename + ".png")
-elif typ == "ICON":
- fmt = ">HHBBBB"
- s = struct.calcsize(fmt)
- form, data_bytes, items_x, items_y, item_pixels_x, item_pixels_y = struct.unpack(
- fmt, data[0:s]
- )
- data = data[s:]
- info = dict(
- format="icon",
- item_pixels_x=item_pixels_x,
- item_pixels_y=item_pixels_y,
- )
- assert form == 0x0400
- assert data_bytes == len(data), f"data_bytes is {data_bytes}, data is {len(data)}"
- with open(options.output_basename + ".toml", "wb") as o:
- tomli_w.dump(info, o)
- i = image_from_data(items_x * item_pixels_x, items_y * item_pixels_y, data)
- i.save(options.output_basename + ".png")
-elif typ == "UBYTE": # Menu
- fmt = ">HHBBBB"
- s = struct.calcsize(fmt)
- form, data_bytes, item_size, items, item_pixels_x, item_pixels_y = struct.unpack(
- fmt, data[0:s]
- )
- data = data[s:]
- info = dict(
- format="menu",
- item_pixels_x=item_pixels_x,
- item_pixels_y=item_pixels_y,
- )
- assert form == 0x0700
- assert item_size == 0x1D
- assert data_bytes == len(data)
- assert items * item_size == len(data)
- items_data = [data[i : i + item_size] for i in range(0, len(data), item_size)]
- items = []
- for item_data in items_data:
- (
- item_id,
- special_mask,
- function_index,
- function_parameter,
- file_load_no,
- next_menu,
- icon_text,
- icon_image_no,
- ) = struct.unpack(f">LLBBBB{item_size - 13}sB", item_data)
- icon_text = icon_text.rstrip(b"\0").decode("ascii")
- items.append(
- dict(
- item_id=item_id,
- function_index=function_index,
- function_parameter=function_parameter,
- file_load_no=file_load_no,
- next_menu=next_menu,
- icon_text=icon_text,
- icon_image_no=icon_image_no,
- )
- )
- flags = decode_special(special_mask)
- if flags:
- items[-1]["flags"] = flags
- info["items"] = items
- with open(options.output_basename + ".toml", "wb") as o:
- tomli_w.dump(info, o)
-else:
- assert False, "unknown format"