#!/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"