diff options
author | yenatch <yenatch@gmail.com> | 2013-11-17 18:23:49 -0500 |
---|---|---|
committer | yenatch <yenatch@gmail.com> | 2013-11-18 04:35:48 -0500 |
commit | 2ffe762d173522095d08b2302bef2dd4769e16e7 (patch) | |
tree | 1f2d4ec7d3b148fdce50499ac04390b97d0116a4 | |
parent | 3acbf5023728d924b252ab8b15c0c740aab8645a (diff) |
gfx: rewrite lots
-rw-r--r-- | pokemontools/gfx.py | 303 |
1 files changed, 116 insertions, 187 deletions
diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py index 2bc613a..30a9fb1 100644 --- a/pokemontools/gfx.py +++ b/pokemontools/gfx.py @@ -12,114 +12,60 @@ import trainers if __name__ != "__main__": rom = crystal.load_rom() -def hex_dump(input, debug=True): + +def split(list_, interval): """ - Display hex dump in rows of 16 bytes. + Split a list by length. """ + for i in xrange(0, len(list_), interval): + j = min(i + interval, len(list_)) + yield list_[i:j] - dump = '' - output = '' - stream = '' - address = 0x00 - margin = 2 + len(hex(len(input))[2:]) - - # dump - for byte in input: - cool = hex(byte)[2:].zfill(2) - dump += cool + ' ' - if debug: stream += cool - - # convenient for testing quick edits in bgb - if debug: output += stream + '\n' - - # get dump info - bytes_per_line = 16 - chars_per_byte = 3 # '__ ' - chars_per_line = bytes_per_line * chars_per_byte - num_lines = int(ceil(float(len(dump)) / float(chars_per_line))) - - # top - # margin - for char in range(margin): - output += ' ' - - for byte in range(bytes_per_line): - output += hex(byte)[2:].zfill(2) + ' ' - output = output[:-1] # last space - - # print hex - for line in range(num_lines): - # address - output += '\n' + hex(address)[2:].zfill(margin - 2) + ': ' - # contents - start = line * chars_per_line - end = chars_per_line + start - 1 # ignore last space - output += dump[start:end] - address += 0x10 - return output +def hex_dump(data, length=0x10): + """ + just use hexdump -C + """ + margin = len('%x' % len(data)) + output = [] + address = 0 + for line in split(data, length): + output += [ + hex(address)[2:].zfill(margin) + + ' | ' + + ' '.join('%.2x' % byte for byte in line) + ] + address += length + return '\n'.join(output) def get_tiles(image): """ Split a 2bpp image into 8x8 tiles. """ - tiles = [] - tile = [] - bytes_per_tile = 16 - - cur_byte = 0 - for byte in image: - # build tile - tile.append(byte) - cur_byte += 1 - # done building? - if cur_byte >= bytes_per_tile: - # push completed tile - tiles.append(tile) - tile = [] - cur_byte = 0 - return tiles - + return list(split(image, 0x10)) def connect(tiles): """ Combine 8x8 tiles into a 2bpp image. """ - out = [] - for tile in tiles: - for byte in tile: - out.append(byte) - return out - + return [byte for tile in tiles for byte in tile] -def transpose(tiles): +def transpose(tiles, width=None): """ - Transpose a tile arrangement along line y=x. + Transpose a tile arrangement along line y=-x. + + 00 01 02 03 04 05 00 06 0c 12 18 1e + 06 07 08 09 0a 0b 01 07 0d 13 19 1f + 0c 0d 0e 0f 10 11 <-> 02 08 0e 14 1a 20 + 12 13 14 15 16 17 03 09 0f 15 1b 21 + 18 19 1a 1b 1c 1d 04 0a 10 16 1c 22 + 1e 1f 20 21 22 23 05 0b 11 17 1d 23 """ - - # horizontal <-> vertical - # 00 01 02 03 04 05 00 06 0c 12 18 1e - # 06 07 08 09 0a 0b 01 07 0d 13 19 1f - # 0c 0d 0e 0f 10 11 <-> 02 08 0e 14 1a 20 - # 12 13 14 15 16 17 <-> 03 09 0f 15 1b 21 - # 18 19 1a 1b 1c 1d 04 0a 10 16 1c 22 - # 1e 1f 20 21 22 23 05 0b 11 17 1d 23 - # etc - - flipped = [] - t = 0 # which tile we're on - w = int(sqrt(len(tiles))) # assume square image - for tile in tiles: - flipped.append(tiles[t]) - t += w - # end of row? - if t >= w*w: - # wrap around - t -= w*w - # next row - t += 1 - return flipped + if width == None: + width = int(sqrt(len(tiles))) # assume square image + tiles = sorted(enumerate(tiles), key= lambda (i, tile): i % width) + return [tile for i, tile in tiles] def to_file(filename, data): @@ -1171,13 +1117,16 @@ def flatten(planar): Flatten planar 2bpp image data into a quaternary pixel map. """ strips = [] - for pair in range(len(planar)/2): - bottom = ord(planar[(pair*2) ]) - top = ord(planar[(pair*2)+1]) - strip = [] - for i in range(7,-1,-1): - color = ((bottom >> i) & 1) + (((top >> i-1) if i > 0 else (top << 1-i)) & 2) - strip.append(color) + for bottom, top in split(planar, 2): + bottom = ord(bottom) + top = ord(top) + strip = [] + for i in xrange(7,-1,-1): + color = ( + (bottom >> i & 1) + + (top *2 >> i & 2) + ) + strip += [color] strips += strip return strips @@ -1186,47 +1135,52 @@ def to_lines(image, width): """ Convert a tiled quaternary pixel map to lines of quaternary pixels. """ - - tile = 8 * 8 - - # so we know how many strips of 8px we're putting into a line - num_columns = width / 8 - # number of lines + tile_width = 8 + tile_height = 8 + num_columns = width / tile_width height = len(image) / width lines = [] - for cur_line in range(height): - tile_row = int(cur_line / 8) + for cur_line in xrange(height): + tile_row = cur_line / tile_height line = [] - for column in range(num_columns): - anchor = num_columns*tile_row*tile + column*tile + (cur_line%8)*8 - line += image[anchor:anchor+8] - lines.append(line) + for column in xrange(num_columns): + anchor = ( + num_columns * tile_row * tile_width * tile_height + + column * tile_width * tile_height + + cur_line % tile_height * tile_width + ) + line += image[anchor : anchor + tile_width] + lines += [line] return lines + def dmg2rgb(word): - red = word & 0b11111 - word >>= 5 - green = word & 0b11111 - word >>= 5 - blue = word & 0b11111 + def shift(value): + while True: + yield value & (2**5 - 1) + value >>= 5 + word = shift(word) + # distribution is less even w/ << 3 + red, green, blue = [int(color * 8.25) for color in [word.next() for _ in xrange(3)]] alpha = 255 - return ((red<<3)+0b100, (green<<3)+0b100, (blue<<3)+0b100, alpha) + return (red, green, blue, alpha) + def rgb_to_dmg(color): word = (color['r'] / 8) - word += (color['g'] / 8) << 5 + word += (color['g'] / 8) << 5 word += (color['b'] / 8) << 10 return word def png_pal(filename): - palette = [] with open(filename, 'rb') as pal_data: words = pal_data.read() - dmg_pals = [] - for word in range(len(words)/2): - dmg_pals.append(ord(words[word*2]) + ord(words[word*2+1])*0x100) + dmg_pals = [] + for word in range(len(words)/2): + dmg_pals.append(ord(words[word*2]) + ord(words[word*2+1])*0x100) + palette = [] white = (255,255,255,255) black = (000,000,000,255) for word in dmg_pals: palette += [dmg2rgb(word)] @@ -1235,90 +1189,65 @@ def png_pal(filename): return palette -def to_png(filein, fileout=None, pal_file=None, height=None, width=None): - """ - Take a planar 2bpp graphics file and converts it to png. - """ - - if fileout == None: fileout = '.'.join(filein.split('.')[:-1]) + '.png' - +def export_2bpp_to_png(filein, fileout=None, pal_file=None, height=0, width=0): + if fileout == None: + fileout = os.path.splitext(filein)[0] + '.png' image = open(filein, 'rb').read() - num_pixels = len(image) * 4 + if pal_file == None: + if os.path.exists(os.path.splitext(fileout)[0]+'.pal'): + pal_file = os.path.splitext(fileout)[0]+'.pal' - if num_pixels == 0: return 'empty image!' + width, height, palette, greyscale, bitdepth, px_map = convert_2bpp_to_png(image, width=width, height=height, pal_file=pal_file) + w = png.Writer(width, height, palette=palette, compression=9, greyscale=greyscale, bitdepth=bitdepth) + with open(fileout, 'wb') as f: + w.write(f, px_map) - # unless the pic is square, at least one dimension should be given - if width == None and height == None: - width = int(sqrt(num_pixels)) - height = width +def convert_2bpp_to_png(image, width=0, height=0, pal_file=None): + """ + Convert a planar 2bpp graphic to png. + """ + num_pixels = len(image) * 4 + assert num_pixels > 0, 'empty image!' - elif height == None: + # at least one dimension should be given + if height == 0 and width != 0: height = num_pixels / width - - elif width == None: - width = num_pixels / height - - - # but try to see if it can be made rectangular + elif width == 0 and height != 0: + width = num_pixels / height if width * height != num_pixels: - # look for possible combos of width/height that would form a rectangle matches = [] - - # this is pretty inefficient, and there is probably a simpler way - for width in range(8,256+1,8): # we only want dimensions that fit in tiles - height = num_pixels / width - if height % 8 == 0: - matches.append((width, height)) - + for w in range(8, num_pixels / 2 + 1, 8): + h = num_pixels / w + if w * h == num_pixels: + matches += [(w, h)] # go for the most square image - width, height = sorted(matches, key=lambda (x,y): x+y)[0] # favors height - - - # if it can't, the only option is a width of 1 tile + width, height = sorted(matches, key= lambda (w, h): w + h)[0] # favor height + # if it still isn't rectangular then the image isn't made of tiles if width * height != num_pixels: - width = 8 - height = num_pixels / width - - - # if this still isn't rectangular, then the image isn't made of tiles - - # for now we'll just spit out a warning - if width * height != num_pixels: - print 'Warning! ' + fileout + ' is ' + width + 'x' + height + '(' + width*height + ' pixels),\n' +\ - 'but ' + filein + ' is ' + num_pixels + ' pixels!' - - - # map it out + raise Exception, 'Image can\'t be divided into tiles (%d px)!' % (num_pixels) + # convert tiles to lines lines = to_lines(flatten(image), width) if pal_file == None: - if os.path.exists(os.path.splitext(fileout)[0]+'.pal'): - pal_file = os.path.splitext(fileout)[0]+'.pal' - - if pal_file == None: palette = None greyscale = True bitdepth = 2 - inverse = { 0:3, 1:2, 2:1, 3:0 } - map = [[inverse[pixel] for pixel in line] for line in lines] + px_map = [[3 - pixel for pixel in line] for line in lines] else: # gbc color palette = png_pal(pal_file) greyscale = False bitdepth = 8 - map = [[pixel for pixel in line] for line in lines] - + px_map = [[pixel for pixel in line] for line in lines] - w = png.Writer(width, height, palette=palette, compression = 9, greyscale = greyscale, bitdepth = bitdepth) - with open(fileout, 'wb') as file: - w.write(file, map) + return width, height, palette, greyscale, bitdepth, px_map def export_png_to_2bpp(filein, fileout=None, palout=None): @@ -1489,7 +1418,7 @@ def mass_to_png(debug=False): for name in files: if debug: print os.path.splitext(name), os.path.join(root, name) if os.path.splitext(name)[1] == '.2bpp': - to_png(os.path.join(root, name)) + export_2bpp_to_png(os.path.join(root, name)) def mass_to_colored_png(debug=False): # greyscale, unless a palette is detected @@ -1498,7 +1427,7 @@ def mass_to_colored_png(debug=False): for name in files: if debug: print os.path.splitext(name), os.path.join(root, name) if os.path.splitext(name)[1] == '.2bpp': - to_png(os.path.join(root, name)) + export_2bpp_to_png(os.path.join(root, name)) os.utime(os.path.join(root, name), None) # only monster and trainer pics for now @@ -1507,16 +1436,16 @@ def mass_to_colored_png(debug=False): if debug: print os.path.splitext(name), os.path.join(root, name) if os.path.splitext(name)[1] == '.2bpp': if 'normal.pal' in files: - to_png(os.path.join(root, name), None, os.path.join(root, 'normal.pal')) + export_2bpp_to_png(os.path.join(root, name), None, os.path.join(root, 'normal.pal')) else: - to_png(os.path.join(root, name)) + export_2bpp_to_png(os.path.join(root, name)) os.utime(os.path.join(root, name), None) for root, dirs, files in os.walk('./gfx/trainers/'): for name in files: if debug: print os.path.splitext(name), os.path.join(root, name) if os.path.splitext(name)[1] == '.2bpp': - to_png(os.path.join(root, name)) + export_2bpp_to_png(os.path.join(root, name)) os.utime(os.path.join(root, name), None) @@ -1556,7 +1485,7 @@ def append_terminator_to_lzs(directory): new.write(data) new.close() -def lz_to_png_by_file(filename): +def export_lz_to_png(filename): """ Convert a lz file to png. Dump a 2bpp file too. """ @@ -1565,7 +1494,7 @@ def lz_to_png_by_file(filename): bpp = Decompressed(lz_data).output bpp_filename = filename.replace(".lz", ".2bpp") to_file(bpp_filename, bpp) - to_png(bpp_filename) + export_2bpp_to_png(bpp_filename) def dump_tileset_pngs(): """ @@ -1575,7 +1504,7 @@ def dump_tileset_pngs(): """ for tileset_id in range(37): tileset_filename = "./gfx/tilesets/" + str(tileset_id).zfill(2) + ".lz" - lz_to_png_by_file(tileset_filename) + export_lz_to_png(tileset_filename) def decompress_frontpic(lz_file): """ @@ -1645,9 +1574,9 @@ if __name__ == "__main__": lz = open(name+'.lz', 'rb').read() to_file(name+'.2bpp', Decompressed(lz, 'vert').output) pic = open(name+'.2bpp', 'rb').read() - to_file(name+'.png', to_png(pic)) + to_file(name+'.png', export_2bpp_to_png(pic)) else: - lz_to_png_by_file(argv[2]) + export_lz_to_png(argv[2]) elif argv[1] == 'png-to-lz': # python gfx.py png-to-lz [--front anim(2bpp) | --vert] [png] @@ -1682,4 +1611,4 @@ if __name__ == "__main__": compress_file(filein, fileout) elif argv[1] == '2bpp-to-png': - to_png(argv[2]) + export_2bpp_to_png(argv[2]) |