Rust-for-Arduboy/Tools/Arduboy-Python-Utilities/fxdata-build.py

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()