From 5c4d2f2fc85823293cf521f602ae2cda458a2e56 Mon Sep 17 00:00:00 2001 From: Nicolas Schodet Date: Tue, 27 Aug 2024 23:33:54 +0200 Subject: Add a tool to convert resources files to readable format --- data/Makefile | 36 ++++++++++ tools/txt2img | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 data/Makefile create mode 100755 tools/txt2img 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" -- cgit v1.2.3