aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolas Schodet2024-08-27 23:33:54 +0200
committerNicolas Schodet2024-08-27 23:33:54 +0200
commit5c4d2f2fc85823293cf521f602ae2cda458a2e56 (patch)
tree84f00ecbacc1d902e139b10223fc8a06405bc20f
parent4f606da544f8677e57b10ab6da3719f7a6c36a54 (diff)
Add a tool to convert resources files to readable format
-rw-r--r--data/Makefile36
-rwxr-xr-xtools/txt2img222
2 files changed, 258 insertions, 0 deletions
diff --git a/data/Makefile b/data/Makefile
new file mode 100644
index 0000000..65e3e6c
--- /dev/null
+++ b/data/Makefile
@@ -0,0 +1,36 @@
+BASE = ..
+SRCDIR = $(BASE)/src
+TOOLSDIR = $(BASE)/tools
+
+BITMAPS = Cursor Display Fail Info LowBattery Ok Wait \
+ RCXintro_1 RCXintro_2 RCXintro_3 RCXintro_4 RCXintro_5 RCXintro_6 \
+ RCXintro_7 RCXintro_8 RCXintro_9 RCXintro_10 RCXintro_11 RCXintro_12 \
+ RCXintro_13 RCXintro_14 RCXintro_15 RCXintro_16 \
+ Test1 Test2
+
+ICONS = Connections Devices Font Icons Port Running Status Step
+
+MENUS = Mainmenu \
+ Submenu01 Submenu02 Submenu03 Submenu04 Submenu05 Submenu06 Submenu07
+
+ALL = $(BITMAPS:%=bitmaps/%.png) $(ICONS:%=icons/%.png) $(MENUS:%=menus/%.toml)
+
+all: $(ALL)
+
+vpath %.txt $(SRCDIR)
+vpath %.rms $(SRCDIR)
+
+bitmaps/%.png: %.txt
+ mkdir -p bitmaps
+ $(TOOLSDIR)/txt2img $< bitmaps/$*
+
+icons/%.png: %.txt
+ mkdir -p icons
+ $(TOOLSDIR)/txt2img $< icons/$*
+
+menus/%.toml: %.rms
+ mkdir -p menus
+ $(TOOLSDIR)/txt2img $< menus/$*
+
+clean:
+ rm -f $(ALL) $(BITMAPS:%=bitmaps/%.toml) $(ICONS:%=icons/%.toml)
diff --git a/tools/txt2img b/tools/txt2img
new file mode 100755
index 0000000..7a89007
--- /dev/null
+++ b/tools/txt2img
@@ -0,0 +1,222 @@
+#!/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"