From b20c96e3665135846e8dcd817358bc34bd0c62fe Mon Sep 17 00:00:00 2001 From: d6 Date: Mon, 14 Nov 2022 00:06:01 -0500 Subject: [PATCH] metadata --- uxnmeta | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100755 uxnmeta diff --git a/uxnmeta b/uxnmeta new file mode 100755 index 0000000..3241256 --- /dev/null +++ b/uxnmeta @@ -0,0 +1,112 @@ +#!/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') + +# 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): + 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 += b"\xff\xff\xff" # 26: color 0 + bmp += b"\x00\x00\x00" # 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): + if len(data[pos0:]) < 512: + print('needed 512 bytes, only had %d' % len(data[pos0:])) + icn = data[pos0:pos0+512] + bmp = tobmp(icn) + return bmp + +# 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)) + bmp = parseicon(path, data, iconaddr) + return {'name': name, 'version': version, 'desc': desc, 'author': author, 'icon': bmp} + +# parse metadata from the given uxn rom +def parse(path): + f = open(path, 'rb') + data = f.read() + f.close() + + 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] + meta = parse(path) + + iconpath = path + '.bmp' + with open(iconpath, 'wb') as f: + f.write(meta['icon']) + + 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()