390 lines
14 KiB
Python
390 lines
14 KiB
Python
#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()
|