#FX data build tool version 1.15 by Mr.Blinky May 2021 - Mar.2023 VERSION = '1.15' import sys import os import re import platform constants = [ #normal bitmap modes ("dbmNormal", 0x00), ("dbmOverwrite", 0x00), ("dbmWhite", 0x01), ("dbmReverse", 0x08), ("dbmBlack", 0x0D), ("dbmInvert", 0x02), #masked bitmap modes for frame ("dbmMasked", 0x10), ("dbmMasked_dbmWhite", 0x11), ("dbmMasked_dbmReverse", 0x18), ("dbmMasked_dbmBlack", 0x1D), ("dbmMasked_dbmInvert", 0x12), #bitmap modes for last bitmap in a frame ("dbmNormal_end", 0x40), ("dbmOverwrite_end", 0x40), ("dbmWhite_end", 0x41), ("dbmReverse_end", 0x48), ("dbmBlack_end", 0x4D), ("dbmInvert_end", 0x42), #masked bitmap modes for last bitmap in a frame ("dbmMasked_end", 0x50), ("dbmMasked_dbmWhite_end", 0x51), ("dbmMasked_dbmReverse_end", 0x58), ("dbmMasked_dbmBlack_end", 0x5D), ("dbmMasked_dbmInvert_end", 0x52), #bitmap modes for last bitmap of the last frame ("dbmNormal_last", 0x80), ("dbmOverwrite_last", 0x80), ("dbmWhite_last", 0x81), ("dbmReverse_last", 0x88), ("dbmBlack_last", 0x8D), ("dbmInvert_last", 0x82), #masked bitmap modes for last bitmap in a frame ("dbmMasked_last", 0x90), ("dbmMasked_dbmWhite_last", 0x91), ("dbmMasked_dbmReverse_last", 0x98), ("dbmMasked_dbmBlack_last", 0x9D), ("dbmMasked_dbmInvert_last", 0x92), ] def print(s): sys.stdout.write(s + '\n') sys.stdout.flush() print('FX data build tool version {} by Mr.Blinky May 2021 - Jan 2023\nUsing Python version {}'.format(VERSION,platform.python_version())) bytes = bytearray() symbols = [] header = [] label = '' indent ='' blkcom = False namespace = False include = False try: toolspath = os.path.dirname(os.path.abspath(sys.argv[0])) sys.path.insert(0, toolspath) from PIL import Image except Exception as e: sys.stderr.write(str(e) + "\n") sys.stderr.write("PILlow python module not found or wrong version.\n") sys.stderr.write("Make sure the correct module is installed or placed at {}\n".format(toolspath)) sys.exit(-1) def rawData(filename): global path with open(path + filename,"rb") as file: bytes = bytearray(file.read()) file.close() return bytes def includeFile(filename): global path print("Including file {}".format(path + filename)) with open(path + filename,"r") as file: lines = file.readlines() file.close() return lines def imageData(filename): global path, symbols filename = path + filename ## parse filename ## FILENAME_[WxH]_[S].[EXT]" spriteWidth = 0 spriteHeight = 0 spacing = 0 elements = os.path.basename(os.path.splitext(filename)[0]).split("_") lastElement = len(elements)-1 #get width and height from filename i = lastElement while i > 0: subElements = list(filter(None,elements[i].split('x'))) if len(subElements) == 2 and subElements[0].isnumeric() and subElements[1].isnumeric(): spriteWidth = int(subElements[0]) spriteHeight = int(subElements[1]) if i < lastElement and elements[i+1].isnumeric(): spacing = int(elements[i+1]) break else: i -= 1 #load image img = Image.open(filename).convert("RGBA") pixels = list(img.getdata()) #check for transparency transparency = False for i in pixels: if i[3] < 255: transparency = True break # check for multiple frames/tiles if spriteWidth > 0: hframes = (img.size[0] - spacing) // (spriteWidth + spacing) else: spriteWidth = img.size[0] - 2 * spacing hframes = 1 if spriteHeight > 0: vframes = (img.size[1] - spacing) // (spriteHeight + spacing) else: spriteHeight = img.size[1] - 2* spacing vframes = 1 #create byte array for bin file size = (spriteHeight+7) // 8 * spriteWidth * hframes * vframes if transparency: size += size bytes = bytearray([spriteWidth >> 8, spriteWidth & 0xFF, spriteHeight >> 8, spriteHeight & 0xFF]) bytes += bytearray(size) i = 4 b = 0 m = 0 fy = spacing frames = 0 for v in range(vframes): fx = spacing for h in range(hframes): for y in range (0,spriteHeight,8): line = " " for x in range (0,spriteWidth): for p in range (0,8): b = b >> 1 m = m >> 1 if (y + p) < spriteHeight: #for heights that are not a multiple of 8 pixels if pixels[(fy + y + p) * img.size[0] + fx + x][1] > 64: b |= 0x80 #white pixel if pixels[(fy + y + p) * img.size[0] + fx + x][3] > 64: m |= 0x80 #opaque pixel else: b &= 0x7F #for transparent pixel clear possible white pixel bytes[i] = b i += 1 if transparency: bytes[i] = m i += 1 frames += 1 fx += spriteWidth + spacing fy += spriteHeight + spacing label = symbols[-1][0] if label.upper() == label: writeHeader('{}constexpr uint16_t {}_WIDTH = {};'.format(indent,label,spriteWidth)) writeHeader('{}constexpr uint16_t {}HEIGHT = {};'.format(indent,label,spriteHeight)) if frames > 1: writeHeader('{}constexpr uint8_t {}_FRAMES = {};'.format(indent,label,frames)) elif '_' in label: writeHeader('{}constexpr uint16_t {}_width = {};'.format(indent,label,spriteWidth)) writeHeader('{}constexpr uint16_t {}_height = {};'.format(indent,label,spriteHeight)) if frames > 1: writeHeader('{}constexpr uint8_t {}_frames = {};'.format(indent,label,frames)) else: writeHeader('{}constexpr uint16_t {}Width = {};'.format(indent,label,spriteWidth)) writeHeader('{}constexpr uint16_t {}Height = {};'.format(indent,label,spriteHeight)) if frames > 255: writeHeader('{}constexpr uint16_t {}Frames = {};'.format(indent,label,frames)) elif frames > 1: writeHeader('{}constexpr uint8_t {}Frames = {};'.format(indent,label,frames)) writeHeader('') return bytes def addLabel(label,length): global symbols symbols.append((label,length)) writeHeader('{}constexpr uint24_t {} = 0x{:06X};'.format(indent,label,length)) def writeHeader(s): global header header.append(s) ################################################################################ if (len(sys.argv) != 2) or (os.path.isfile(sys.argv[1]) != True) : sys.stderr.write("FX data script file not found.\n") sys.exit(-1) filename = os.path.abspath(sys.argv[1]) datafilename = os.path.splitext(filename)[0] + '-data.bin' savefilename = os.path.splitext(filename)[0] + '-save.bin' devfilename = os.path.splitext(filename)[0] + '.bin' headerfilename = os.path.splitext(filename)[0] + '.h' path = os.path.dirname(filename) + os.sep saveStart = -1 with open(filename,"r") as file: lines = file.readlines() file.close() print("Building FX data using {}".format(filename)) lineNr = 0 while lineNr < len(lines): parts = [p for p in re.split("([ ,]|[\\'].*[\\'])", lines[lineNr]) if p.strip() and p != ','] for i in range (len(parts)): part = parts[i] #strip unwanted chars if part[:1] == '\t' : part = part[1:] if part[:1] == '{' : part = part[1:] if part[-1:] == '\n': part = part[:-1] if part[-1:] == ';' : part = part[:-1] if part[-1:] == '}' : part = part[:-1] if part[-1:] == ';' : part = part[:-1] if part[-1:] == '.' : part = part[:-1] if part[-1:] == ',' : part = part[:-1] if part[-2:] == '[]': part = part[:-2] #handle comments if blkcom == True: p = part.find('*/',2) if p >= 0: part = part[p+2:] blkcom = False else: if part[:2] == '//': break elif part[:2] == '/*': p = part.find('*/',2) if p >= 0: part = part[p+2:] else: blkcom = True; #handle types elif part == '=' : pass elif part == 'const' : pass elif part == 'PROGMEM' : pass elif part == 'align' : t = 0 elif part == 'int8_t' : t = 1 elif part == 'uint8_t' : t = 1 elif part == 'int16_t' : t = 2 elif part == 'uint16_t': t = 2 elif part == 'int24_t' : t = 3 elif part == 'uint24_t': t = 3 elif part == 'int32_t' : t = 4 elif part == 'uint32_t': t = 4 elif part == 'image_t' : t = 5 elif part == 'raw_t' : t = 6 elif part == 'String' : t = 7 elif part == 'string' : t = 7 elif part == 'include' : include = True elif part == 'datasection' : pass elif part == 'savesection' : saveStart = len(bytes) #handle namespace elif part == 'namespace': namespace = True elif namespace == True: namespace = False writeHeader("namespace {}\n{{".format(part)) indent += ' ' elif part == 'namespace_end': indent = indent[:-2] writeHeader('}\n') namespace = False #handle strings elif (part[:1] == "'") or (part[:1] == '"'): if part[:1] == "'": part = part[1:part.rfind("'")] else: part = part[1:part.rfind('"')] #handle include if include == True: lines[lineNr+1:lineNr+1] = includeFile(part) include = False elif t == 1: bytes += part.encode('utf-8').decode('unicode_escape').encode('utf-8') elif t == 5: bytes += imageData(part) elif t == 6: bytes += rawData(part) elif t == 7: bytes += part.encode('utf-8').decode('unicode_escape').encode('utf-8') + b'\x00' else: sys.stderr.write('ERROR in line {}: unsupported string for type\n'.format(lineNr)) sys.exit(-1) #handle values elif part[:1].isnumeric() or (part[:1] == '-' and part[1:2].isnumeric()): n = int(part,0) if t == 4: bytes.append((n >> 24) & 0xFF) if t >= 3: bytes.append((n >> 16) & 0xFF) if t >= 2: bytes.append((n >> 8) & 0xFF) if t >= 1: bytes.append((n >> 0) & 0xFF) #handle align if t == 0: align = len(bytes) % n if align: bytes += b'\xFF' * (n - align) #handle labels elif part[:1].isalpha(): for j in range(len(part)): if part[j] == '=': addLabel(label,len(bytes)) label = '' part = part[j+1:] parts.insert(i+1,part) break elif part[j].isalnum() or part[j] == '_': label += part[j] else: sys.stderr.write('ERROR in line {}: Bad label: {}\n'.format(lineNr,label)) sys.exit(-1) if (label != '') and (i < len(parts) - 1) and (parts[i+1][:1] == '='): addLabel(label,len(bytes)) label = '' #handle included constants if label != '': for symbol in constants: if symbol[0] == label: if t == 4: bytes.append((symbol[1] >> 24) & 0xFF) if t >= 3: bytes.append((symbol[1] >> 16) & 0xFF) if t >= 2: bytes.append((symbol[1] >> 8) & 0xFF) if t >= 1: bytes.append((symbol[1] >> 0) & 0xFF) label = '' break #handle symbol values if label != '': for symbol in symbols: if symbol[0] == label: if t == 4: bytes.append((symbol[1] >> 24) & 0xFF) if t >= 3: bytes.append((symbol[1] >> 16) & 0xFF) if t >= 2: bytes.append((symbol[1] >> 8) & 0xFF) if t >= 1: bytes.append((symbol[1] >> 0) & 0xFF) label = '' break if label != '': sys.stderr.write('ERROR in line {}: Undefined symbol: {}\n'.format(lineNr,label)) sys.exit(-1) elif len(part) > 0: sys.stderr.write('ERROR unable to parse {} in element: {}\n'.format(part,str(parts))) sys.exit(-1) lineNr += 1 if saveStart >= 0: dataSize = saveStart dataPages = (dataSize + 255) // 256 saveSize = len(bytes) - saveStart savePages = (saveSize + 4095) // 4096 * 16 else: dataSize = len(bytes) dataPages = (dataSize + 255) // 256 saveSize = 0 savePages = 0 savePadding = 0 dataPadding = dataPages * 256 - dataSize savePadding = savePages * 256 - saveSize print("Saving FX data header file {}".format(headerfilename)) with open(headerfilename,"w") as file: file.write('#pragma once\n\n') file.write('/**** FX data header generated by fxdata-build.py tool version {} ****/\n\n'.format(VERSION)) file.write('using uint24_t = __uint24;\n\n') file.write('// Initialize FX hardware using FX::begin(FX_DATA_PAGE); in the setup() function.\n\n') file.write('constexpr uint16_t FX_DATA_PAGE = 0x{:04x};\n'.format(65536 - dataPages - savePages)) file.write('constexpr uint24_t FX_DATA_BYTES = {};\n\n'.format(dataSize)) if saveSize > 0: file.write('constexpr uint16_t FX_SAVE_PAGE = 0x{:04x};\n'.format(65536 - savePages)) file.write('constexpr uint24_t FX_SAVE_BYTES = {};\n\n'.format(saveSize)) for line in header: file.write(line + '\n') file.close() print("Saving {} bytes FX data to {}".format(dataSize,datafilename)) with open(datafilename,"wb") as file: file.write(bytes[0:dataSize]) file.close() if saveSize > 0: print("Saving {} bytes FX savedata to {}".format(saveSize,savefilename)) with open(savefilename,"wb") as file: file.write(bytes[saveStart:len(bytes)]) file.close() print("Saving FX development data to {}".format(devfilename)) with open(devfilename,"wb") as file: file.write(bytes[0:dataSize]) if dataPadding > 0: file.write(b'\xFF' * dataPadding) if saveSize > 0: file.write(bytes[saveStart:len(bytes)]) if savePadding > 0: file.write(b'\xFF' * savePadding) file.close()