Created
September 2, 2017 00:12
A quick, messy script to take PNG spritesheets and convert them to Uzebox-usable includable files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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