#!/usr/bin/env python # # extract metadata from uxn rom files. # # see https://wiki.xxiivv.com/site/manifest.html # # (also converts the icon to a monochrome BMP file.) from sys import argv, exit # convert two bytes into an address, subtracting for zero page def toaddr(x, y): return x * 256 + y - 256 # parse null-terminated string def parsestr(data, start): end = start + data[start:].find(b'\0') return end + 1, data[start:end].decode('ascii') # emit 24-bit integer (big endian) def emit24(n): return bytes([(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]) # convert 64x64 icn to bmp # since we are always dealing with a 1-bit 64x64 # image we can just hardcode the BMP header data def tobmp(icn, light, dark): bmp = b"BM" # 0: bitmap file bmp += b"\x20\x10\x00\x00" # 2: file size: 4128 (32 + 4096) bmp += b"\x00\x00" # 6: reserved: 0 bmp += b"\x00\x00" # 8: reserved: 0 bmp += b"\x20\x00\x00\x00" # 10: pixel data offset: 32 bmp += b"\x0c\x00\x00\x00" # 14: header size: 12 bmp += b"\x40\x00" # 18: width in pixels: 64 bmp += b"\x40\x00" # 20: height in pixels: 64 bmp += b"\x01\x00" # 22: color planes: 1 bmp += b"\x01\x00" # 24: bits per pixel: 1 bmp += emit24(light) # 26: color 0 bmp += emit24(dark) # 29: color 1 # 32: start of pixel data # ICN is composed of a grid of 8x8 tiles. for BMP we need # to create 64 rows of 64 pixels each. rows = [] for offset in range(0, 512, 64): for y in range(0, 8): rows.append([]) for x in range(0, 64, 8): rows[-1].append(icn[offset + y + x]) for row in reversed(rows): bmp += bytes(row) return bmp # read the icon from the given binary data and address def parseicon(path, data, pos0, light, dark): if len(data[pos0:]) < 512: print('needed 512 bytes, only had %d' % len(data[pos0:])) icn = data[pos0:pos0+512] bmp = tobmp(icn, light, dark) return bmp # try to parse LIT2 x y .System/[rgb] DEO2 # returns (shift, tint) # shift 16=red, 8=green, 0=blue def parsecolor(data, pos0): a,b,c,d,e,f = data[pos0:pos0+6] if a == 0xa0 and d == 0x80 and f == 0x37: tint = b * 256 + c # we only care about 'b' i.e. the first two colors if e == 0x08: return (16, tint) if e == 0x0a: return (8, tint) if e == 0x0c: return (0, tint) return None # parse the metadata from the given binary data and address def parsemeta(path, data, pos0): a,b,c,d = data[pos0:pos0+4] endaddr = toaddr(a, b) iconaddr = toaddr(c, d) pos1, name = parsestr(data, pos0 + 4) pos2, version = parsestr(data, pos1) pos3, desc = parsestr(data, pos2) pos4, author = parsestr(data, pos3) if pos4 != endaddr: print('%s: data ended by %04x but expected %04x' % (path, pos4, endaddr)) meta = {'name': name, 'version': version, 'desc': desc, 'author': author, 'icon': iconaddr} return meta # parse metadata from the given uxn rom def parse(path, data): if len(data) < 6: print("%s: file too small (%d bytes)" % (path, len(data))) exit(1) a,x,y,d,e,f = data[:6] if a != 0xa0 or d != 0x80 or e != 0xf0 or f != 0x37: print('%s: no metadata detected' % path) exit(1) return parsemeta(path, data, toaddr(x, y)) # go for it! def main(): if len(argv[1:]) != 1: print('usage: %s ROMFILE' % argv[0]) exit(1) path = argv[1] f = open(path, 'rb') data = f.read() f.close() meta = parse(path, data) i = 6 colors = [0, 0, 0, 0] while True: res = parsecolor(data, i) i += 6 if res is None: break shift, tint = res colors[0] |= ((((tint >> 12) & 0xf) * 0x11) << shift) colors[1] |= ((((tint >> 8) & 0xf) * 0x11) << shift) colors[2] |= ((((tint >> 4) & 0xf) * 0x11) << shift) colors[3] |= ((((tint >> 0) & 0xf) * 0x11) << shift) if colors == [0, 0, 0, 0]: dark, light = 0x000000, 0xffffff else: dark, light = colors[0], colors[1] iconpath = path + '.bmp' bmp = parseicon(path, data, meta['icon'], light, dark) with open(iconpath, 'wb') as f: f.write(bmp) print('%s:' % path) print(' Name: %s' % meta['name']) print(' Version: %s' % meta['version']) print(' Description: %s' % meta['desc']) print(' Author: %s' % meta['author']) print('') print(' Icon: %s' % iconpath) print('') if __name__ == "__main__": main()