Skip to content

Instantly share code, notes, and snippets.

@charliegreen
Created September 2, 2017 00:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save charliegreen/b45b856b51b21e226446c80e7272d48d to your computer and use it in GitHub Desktop.
Save charliegreen/b45b856b51b21e226446c80e7272d48d to your computer and use it in GitHub Desktop.
A quick, messy script to take PNG spritesheets and convert them to Uzebox-usable includable files
#!/usr/bin/env python3
# tile_converter.py: a quick, messy script to take PNG spritesheets and convert them to
# Uzebox-usable includable files.
#
# Usage: python3 tile_converter.py PATH
#
# If PATH is, for example, 'data/foo.png', this will create a file 'data/foo.inc' that may be
# #include'd by Uzebox programs. foo.inc includes:
# * TILETAB_FOO_LENGTH: a macro with the number of tiles defined
# * TILETAB_FOO: a const char[] of tiles
#
# If 'data/foo.map.json' exists, it will be used to also add tilemap declarations. This foo.map.json:
#
# {
# "maps": {
# "bar": {
# "x": 0,
# "y": 1,
# "w": 2,
# "h": 3
# }
# }
# }
#
# Will result in foo.inc also including a declaration for MAP_BAR, a spritemap 2 tiles wide and 3
# tall, consisting of the tiles read from foo.png in x=[0,1],y=[1,2,3].
#
# If WRITE_HEADER_FILE is True, then another file, foo.h, will be generated, so that C files may
# include the definitions in foo.inc without repeating the declaration.
import sys
import png
WRITE_HEADER_FILE = True
TILE_W, TILE_H = 8, 8
INDENT = 4
def load_png(filepath):
reader = png.Reader(filepath)
return reader.read_flat()
def load_map_data(filepath):
import json
with open(filepath, 'r') as f:
return json.load(f)
def load_tiles(pixels):
"""Load tiles from image pixels.
:returns: `tiles`: a list of arrays of pixel data
:returns: `image`: a dict of (col, row) tuples to integer indices into `tiles`
"""
tiles = []
image = {}
for row in range(H):
for col in range(W):
tile = []
for vpixel in range(TILE_H):
base = (row*TILE_H+vpixel)*PNG_W_PX + col*TILE_W
tile.append(pixels[base:base+TILE_W])
if tile in tiles:
# print("({}, {}): repeat".format(row, col))
image[(col, row)] = tiles.index(tile)
continue
# print("({}, {}): adding new tile (#{})".format(row, col, len(tiles)))
image[(col, row)] = len(tiles)
tiles.append(tile)
return tiles, image
def write_tilemap(f, tiles):
varname = 'TILETAB_'+get_filename().upper()
f.write("#define {} {}\n\n".format(varname+'_LENGTH', len(tiles)))
f.write("const char {}[] PROGMEM = {{".format(varname))
for i in range(len(tiles)):
f.write("\n"+INDENT*' '+"// tile #{}\n".format(i))
for row in range(len(tiles[i])):
f.write(INDENT*' '+', '.join("0x{:02x}".format(x) for x in tiles[i][row])+',\n')
f.write("};\n")
def write_maps(f, map_data, image):
data = map_data['maps']
for name in sorted(data.keys()):
m = data[name]
f.write("\nconst char MAP_{}[] PROGMEM = {{\n".format(name))
f.write(INDENT*' '+"{}, {},\n".format(m['w'], m['h'])+INDENT*' ')
vals = []
for row in range(m['y'], m['y']+m['h']):
vals.append([])
for col in range(m['x'], m['x']+m['w']):
vals[-1].append("{}".format(image[(col, row)]))
f.write((',\n'+INDENT*' ').join(', '.join(line) for line in vals))
f.write("\n};\n")
def write_header_file(f, map_data):
guard_name = get_filename().upper()+'_H'
f.write("#ifndef {0}\n#define {0}\n\n".format(guard_name))
varname = 'TILETAB_'+get_filename().upper()
f.write("#define {} {}\n\n".format(varname+'_LENGTH', len(tiles)))
f.write("extern const char {}[];\n".format(varname))
data = map_data['maps'] if map_data is not None else {}
for name in sorted(data.keys()):
f.write("extern const char MAP_{}[];\n".format(name))
f.write('\n#endif\n')
def get_filepath(suffix, output=False):
if output:
return OUTPUT_DIR+get_filename()+suffix
return FILEPATH[0:-4]+suffix
def get_filename():
p = get_filepath('')
if '/' in p:
return p.split('/')[-1]
return p
if __name__ == '__main__':
if len(sys.argv) != 3:
print("Bad number of arguments")
exit(1)
FILEPATH = sys.argv[1]
if not FILEPATH.endswith('.png'):
print("Not running on non-PNG file '{}'".format(FILEPATH))
exit(1)
OUTPUT_DIR = sys.argv[2]
if not OUTPUT_DIR.endswith('/'):
OUTPUT_DIR += '/'
PNG_W_PX, PNG_H_PX, pixels, metadata = load_png(FILEPATH)
W, H = int(PNG_W_PX/TILE_W), int(PNG_H_PX/TILE_H) # width, height of our image, in tiles
tiles, image = load_tiles(pixels)
filepath_inc = get_filepath('.inc', True)
# print("Writing file '{}'".format(filepath_inc))
with open(filepath_inc, 'w') as f:
f.write("// Generated by tile_converter.py\n")
f.write("// picture: {}\n".format(FILEPATH))
f.write("// Horizontal Tiles={}\n".format(W))
f.write("// Vertical Tiles={}\n".format(H))
f.write("// Tile Width={}\n".format(TILE_W))
f.write("// Tile Height={}\n\n".format(TILE_H))
write_tilemap(f, tiles)
filepath_map = get_filepath('.map.json')
map_data = None
try:
map_data = load_map_data(filepath_map)
write_maps(f, map_data, image)
except FileNotFoundError:
pass
if WRITE_HEADER_FILE:
filepath_h = get_filepath('.h', True)
# print("Writing file '{}'".format(filepath_h))
with open(filepath_h, 'w') as f:
write_header_file(f, map_data)
s = " Added {} tiles".format(len(tiles))
if map_data is not None:
s += " and {} tilemaps".format(len(map_data['maps'].keys()))
print(s)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment