OpenSolo/artoo/tools/assetgen.py

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