mirror of
https://github.com/OpenSolo/OpenSolo.git
synced 2025-04-29 22:24:32 +02:00
322 lines
9.8 KiB
Python
Executable File
322 lines
9.8 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# convert assets to binary formats that
|
|
# can be understood by firmware
|
|
#
|
|
|
|
import sys, datetime, os, ConfigParser
|
|
import itertools
|
|
from cStringIO import StringIO
|
|
from PIL import Image, ImageFont, ImageDraw
|
|
|
|
DISPLAY_WIDTH = 320
|
|
DISPLAY_HEIGHT = 240
|
|
|
|
|
|
class Palette:
|
|
''' our look up table of 16-bit pixel vals '''
|
|
|
|
MAX_LUT_SIZE = 256
|
|
ALPHA_KEY = 0x3D3D
|
|
|
|
def __init__(self, name):
|
|
self.lut = [self.ALPHA_KEY]
|
|
self.name = name
|
|
|
|
def ili9341_pixel(self, p):
|
|
'''
|
|
convert to ili9341 5-6-5 rgb pixel format,
|
|
taking the high-order bits of each 8-bit color channel
|
|
'''
|
|
r, g, b, a = p[0], p[1], p[2], p[3]
|
|
if a == 0:
|
|
return self.ALPHA_KEY
|
|
return (b & 0xf8) << 8 | ((g & 0xfc) << 3) | ((r & 0xf8) >> 3)
|
|
|
|
def pix_idx(self, p):
|
|
'''
|
|
return the index in our palette for the given pixel.
|
|
if we don't have an index for this pixel yet, add it to the palette.
|
|
'''
|
|
pixel = self.ili9341_pixel(p)
|
|
if pixel not in self.lut:
|
|
if len(self.lut) == self.MAX_LUT_SIZE:
|
|
raise ValueError("Palette too large - can only support %d entries" % self.MAX_LUT_SIZE)
|
|
self.lut.append(pixel)
|
|
return self.lut.index(pixel) & 0xff
|
|
|
|
def write_to(self, cpp, hdr):
|
|
|
|
if hdr:
|
|
visibility = "extern"
|
|
hdr.write("extern const Gfx::ImagePalette %s;\n" % self.name)
|
|
else:
|
|
visibility = "static"
|
|
|
|
cpp.write("static const uint16_t %s_data[] = {" % self.name)
|
|
for i, p in enumerate(self.lut):
|
|
if i % 10 == 0:
|
|
cpp.write("\n ")
|
|
cpp.write("0x%04x, " % p)
|
|
cpp.write("\n};\n\n")
|
|
|
|
cpp.write("%s const Gfx::ImagePalette %s = {\n" % (visibility, self.name))
|
|
cpp.write(" %s_data,\n" % self.name)
|
|
cpp.write(" %d, // maxIdx\n" % (len(self.lut) - 1))
|
|
cpp.write("};\n\n")
|
|
|
|
|
|
def rle(imgdata, asset_name, palette):
|
|
'''
|
|
run length encode this image.
|
|
|
|
each chunk of data is specified by 1-byte header:
|
|
0 to 127: Copy the next n + 1 symbols verbatim
|
|
-127 to -1: Repeat the next symbol 1 - n times
|
|
-128: Do nothing (EOF)
|
|
'''
|
|
|
|
MAX_RUN = 128
|
|
EOF = -128
|
|
rle_bytes = []
|
|
single_runs = []
|
|
|
|
palette_start_size = len(palette.lut)
|
|
color_set = set()
|
|
|
|
# get a list of (count, pixel) tuples
|
|
runs = [(len(list(group)), name) for name, group in itertools.groupby(imgdata)]
|
|
|
|
def flush_single_runs():
|
|
rle_bytes.append((len(single_runs) - 1) & 0xff)
|
|
rle_bytes.extend(single_runs)
|
|
|
|
for run in runs:
|
|
|
|
runlen = run[0]
|
|
runval = palette.pix_idx(run[1]) & 0xff
|
|
color_set.add(runval)
|
|
|
|
if runlen == 1:
|
|
single_runs.append(runval)
|
|
if len(single_runs) == MAX_RUN:
|
|
flush_single_runs()
|
|
single_runs = []
|
|
else:
|
|
if len(single_runs) > 0:
|
|
flush_single_runs()
|
|
single_runs = []
|
|
|
|
while runlen > 0:
|
|
runsegment = min(MAX_RUN, runlen)
|
|
runlen -= runsegment
|
|
|
|
rle_bytes.append((1 - runsegment) & 0xff)
|
|
rle_bytes.append(runval)
|
|
|
|
# any left over single runs?
|
|
if len(single_runs) > 0:
|
|
flush_single_runs()
|
|
|
|
rle_bytes.append(EOF & 0xff)
|
|
|
|
if opt_show_stats:
|
|
palette_diff = len(palette.lut) - palette_start_size
|
|
print "%s - %d colors (%d total, %d new), %d bytes" % (asset_name, len(color_set), len(palette.lut), palette_diff, len(rle_bytes))
|
|
|
|
return rle_bytes
|
|
|
|
|
|
def parse_opt_str(opt_str):
|
|
'''
|
|
helper to extract key/val pairs from a string in the form: "key=val, key2=val2, ..."
|
|
'''
|
|
opts = {}
|
|
|
|
for opt in opt_str.split(','):
|
|
vals = opt.strip().split('=')
|
|
if len(vals) != 2:
|
|
raise ValueError("bad font option format. must be 'k=v(,)'")
|
|
opts[vals[0]] = vals[1]
|
|
|
|
return opts
|
|
|
|
|
|
def convertFont(cfg_path, fontname, font_opts_str, palettes, cpp, hdr):
|
|
"""
|
|
process a font file.
|
|
the height for all glyphs is constant, given by the sum
|
|
of ascent and descent provided by font.getmetrics().
|
|
each glyph has its own width.
|
|
"""
|
|
|
|
opts = parse_opt_str(font_opts_str)
|
|
|
|
# fonts with the same bg/color combo share palettes.
|
|
# XXX: could also be better to simply limit the palette size that
|
|
# pillow uses when generating fonts, but have not taken the
|
|
# time to figure out how to do that yet...
|
|
|
|
fontpath = os.path.join(os.path.dirname(cfg_path), opts['path'])
|
|
|
|
font = ImageFont.truetype(fontpath, int(opts['size']))
|
|
|
|
start_ch = opts.get('start_ch', ' ')
|
|
end_ch = opts.get('end_ch', 'z')
|
|
|
|
color = opts.get('color', "#fff")
|
|
bg = opts.get('bg', "#000")
|
|
|
|
color_key = "Palette_%s_%s" % (color.replace("#", ""), bg.replace("#", ""))
|
|
matching_palette = [p for p in palettes if p.name == color_key]
|
|
if matching_palette:
|
|
palette = matching_palette[0]
|
|
else:
|
|
palette = Palette(color_key)
|
|
palettes.append(palette)
|
|
|
|
glyphs = map(chr, range(ord(start_ch), ord(end_ch)+1))
|
|
|
|
# buffer the array of font glyphs so we don't need to traverse twice
|
|
font_glyphs_str = StringIO()
|
|
font_glyphs_str.write("static const Gfx::GlyphAsset %s_glyphs[] = {" % fontname)
|
|
|
|
for g in glyphs:
|
|
w, h = font.getsize(g)
|
|
im = Image.new("RGBA", (w, h))
|
|
draw = ImageDraw.Draw(im)
|
|
draw.rectangle([(0,0), im.size], fill=bg)
|
|
draw.text((0,0), g, font=font, fill=color)
|
|
|
|
glyph_name = "%s_glyph_%02x" % (fontname, ord(g))
|
|
cpp.write("static const uint8_t %s_data[] = {" % (glyph_name))
|
|
writeImageBytes(rle(im.getdata(), glyph_name, palette), cpp)
|
|
cpp.write("\n};\n\n")
|
|
|
|
font_glyphs_str.write("\n { %s_data, %d, %d }," % (glyph_name, w, h))
|
|
|
|
cpp.write(font_glyphs_str.getvalue())
|
|
cpp.write("\n};\n\n")
|
|
|
|
ascent, descent = font.getmetrics()
|
|
|
|
hdr.write("extern const Gfx::FontAsset %s;\n" % fontname)
|
|
cpp.write("extern const Gfx::FontAsset %s = {\n" % fontname)
|
|
cpp.write(" %d, // glyphCount\n" % len(glyphs))
|
|
cpp.write(" %d, // ascent\n" % ascent)
|
|
cpp.write(" %d, // descent\n" % descent)
|
|
cpp.write(" Gfx::ImgFmtRle, // format\n")
|
|
cpp.write(" '%c', // start_ch\n" % start_ch)
|
|
cpp.write(" 0, // res0\n")
|
|
cpp.write(" 0, // res1\n")
|
|
cpp.write(" %s_glyphs,\n" % fontname)
|
|
cpp.write(" &%s,\n" % palette.name)
|
|
cpp.write("};\n\n")
|
|
|
|
|
|
def convertFile(imgname, fin, palette, cpp, hdr):
|
|
|
|
img = Image.open(fin).convert("RGBA")
|
|
w, h = img.size
|
|
|
|
if w > DISPLAY_WIDTH or h > DISPLAY_HEIGHT:
|
|
raise ValueError("error: image size", img.size, "is larger than display")
|
|
|
|
hdr.write("extern const Gfx::ImageAsset %s;\n" % imgname)
|
|
cpp.write("static const uint8_t %s_data[] = {" % imgname)
|
|
|
|
writeImageBytes(rle(img.getdata(), imgname, palette), cpp)
|
|
|
|
cpp.write("\n};\n\n")
|
|
|
|
cpp.write("extern const Gfx::ImageAsset %s = {\n" % imgname)
|
|
cpp.write(" %d, // width\n" % w)
|
|
cpp.write(" %d, // height\n" % h)
|
|
cpp.write(" Gfx::ImgFmtRle, // format\n")
|
|
cpp.write(" 0, // reserved\n")
|
|
cpp.write(" %s_data,\n" % imgname)
|
|
cpp.write(" &%s,\n" % palette.name)
|
|
cpp.write("};\n\n")
|
|
|
|
|
|
def writeImageBytes(bytes, f):
|
|
for i, b in enumerate(bytes):
|
|
if i % 10 == 0:
|
|
f.write("\n ")
|
|
f.write("0x%02x, " % b)
|
|
|
|
|
|
def writeWarning(f):
|
|
f.write("/*\n")
|
|
f.write(" * WARNING - this file generated by assetgen.py\n")
|
|
f.write(" * do not edit, it will be replaced during the next build.\n")
|
|
f.write(" */\n\n")
|
|
|
|
|
|
def checkPilVersion():
|
|
import pkg_resources
|
|
try:
|
|
v = pkg_resources.get_distribution("pillow").version
|
|
maj, min, patch = [int(c) for c in v.split(".")]
|
|
if maj < 2 or (maj == 2 and min < 6):
|
|
print "err: pillow installation is older than 2.6.x, try `pip install --upgrade pillow` to get the latest"
|
|
sys.exit(1)
|
|
except pkg_resources.DistributionNotFound:
|
|
print "pillow is not installed - check ReadMe.md for installation info"
|
|
sys.exit(1)
|
|
|
|
#
|
|
# main
|
|
#
|
|
# accept search and output dirs on command line,
|
|
# scan for PNGs and convert them
|
|
#
|
|
|
|
checkPilVersion()
|
|
|
|
cfgfile = os.path.join(os.getcwd(), sys.argv[1])
|
|
outdir = os.path.join(os.getcwd(), sys.argv[2])
|
|
|
|
opt_show_stats = '--show-stats' in sys.argv
|
|
|
|
config = ConfigParser.ConfigParser()
|
|
config.optionxform = str # don't lower case all option names
|
|
config.read(cfgfile)
|
|
|
|
palettes = []
|
|
|
|
with open(os.path.join(outdir, "resources-gen.h"), "w") as hdrout:
|
|
|
|
hdrout.write("#ifndef _RESOURCES_GEN_H\n")
|
|
hdrout.write("#define _RESOURCES_GEN_H\n\n")
|
|
writeWarning(hdrout)
|
|
hdrout.write('#include "gfx.h"\n\n')
|
|
|
|
with open(os.path.join(outdir, "resources-gen.cpp"), "w") as cppout:
|
|
|
|
cppout.write('#include "ui.h"\n')
|
|
cppout.write('#include "resources-gen.h"\n\n')
|
|
writeWarning(cppout)
|
|
|
|
# process image assets
|
|
# there can be multiple asset groups, each of which gets its own palette
|
|
for s in config.sections():
|
|
if s.startswith("Images "):
|
|
pal = Palette(s[len("Images "):])
|
|
palettes.append(pal)
|
|
for name, value in config.items(s):
|
|
f = os.path.join(os.path.dirname(cfgfile), value)
|
|
convertFile(name, f, pal, cppout, hdrout)
|
|
|
|
# and font assets
|
|
for name, font_opts in config.items('Fonts'):
|
|
convertFont(cfgfile, name, font_opts, palettes, cppout, hdrout)
|
|
|
|
for p in palettes:
|
|
if opt_show_stats:
|
|
print "palette: %s - %d colors" % (p.name, len(p.lut))
|
|
p.write_to(cppout, hdrout)
|
|
|
|
hdrout.write("\n#endif // _RESOURCES_GEN_H\n")
|
|
|