From 53af4e21e9063c0dc1fcc50cd328715726ca034d Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 12 Jun 2013 12:38:28 -0400 Subject: gfx: handling for <4-color pngs when converting to 2bpp without a .pal file as reference, palettes are sorted by luminance. pokemon crystal reads palettes exactly 4 colors in length. if an image used fewer than 4 colors, invalid palettes were produced. instead, dummy colors are inserted to pad out the palette. original-commit-id: 5cf1754b08aa6903f01b839c917169e6dc8c260f --- gfx.py | 77 +++++++++++++++++++++++++++++++----------------------------------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/gfx.py b/gfx.py index 54cd04c..8083cb1 100644 --- a/gfx.py +++ b/gfx.py @@ -1183,6 +1183,12 @@ def dmg2rgb(word): blue = word & 0b11111 alpha = 255 return ((red<<3)+0b100, (green<<3)+0b100, (blue<<3)+0b100, alpha) + +def rgb_to_dmg(color): + word = (color['r'] / 8) << 10 + word += (color['g'] / 8) << 5 + word += (color['b'] / 8) + return word def png_pal(filename): @@ -1303,17 +1309,13 @@ def to_2bpp(filein, fileout=None, palout=None): greyscale = info[3]['greyscale'] - # commented out for the moment - padding = { 'left': 0, 'right': 0, 'top': 0, 'bottom': 0, } - #if width % 8 != 0: # padding['left'] = int(ceil((width / 8 + 8 - width) / 2)) # padding['right'] = int(floor((width / 8 + 8 - width) / 2)) - #if height % 8 != 0: # padding['top'] = int(ceil((height / 8 + 8 - height) / 2)) # padding['bottom'] = int(floor((height / 8 + 8 - height) / 2)) @@ -1322,7 +1324,7 @@ def to_2bpp(filein, fileout=None, palout=None): # turn the flat values into something more workable pixel_length = 4 # rgba - image = [] + image = [] # while we're at it, let's size up the palette @@ -1331,53 +1333,49 @@ def to_2bpp(filein, fileout=None, palout=None): for line in rgba: newline = [] for pixel in range(len(line)/pixel_length): - i = pixel*pixel_length + i = pixel * pixel_length color = { 'r': line[i ], 'g': line[i+1], 'b': line[i+2], 'a': line[i+3], } - newline.append(color) - if color not in palette: palette.append(color) + newline += [color] + if color not in palette: palette += [color] image.append(newline) + # pad out any small palettes + hues = { + 'white': { 'r': 0xff, 'g': 0xff, 'b': 0xff, 'a': 0xff }, + 'black': { 'r': 0x00, 'g': 0x00, 'b': 0x00, 'a': 0xff }, + 'grey': { 'r': 0x55, 'g': 0x55, 'b': 0x55, 'a': 0xff }, + 'gray': { 'r': 0xaa, 'g': 0xaa, 'b': 0xaa, 'a': 0xff }, + } + while len(palette) < 4: + for hue in hues.values(): + if not any(color is hue for color in palette): + palette += [hue] + if len(palette) >= 4: break - # sort by luminance, because we can + assert len(palette) <= 4, 'Palette should be 4 colors, is really ' + str(len(palette)) + # sort by luminance def luminance(color): # this is actually in reverse, thanks to dmg/cgb palette ordering rough = { 'r': 4.7, 'g': 1.4, 'b': 13.8, } return sum(color[key] * -rough[key] for key in rough.keys()) - - palette = sorted(palette, key = lambda x:luminance(x)) - - # no palette fixing for now - - assert len(palette) <= 4, 'Palette should be 4 colors, is really ' + str(len(palette)) - - - # spit out new palette (disabled for now) - - def rgb_to_dmg(color): - word = (color['r'] / 8) << 10 - word += (color['g'] / 8) << 5 - word += (color['b'] / 8) - return word - - palout = None - - if palout != None: - output = [] - for color in palette[1:3]: - word = rgb_to_dmg(color) - output.append(word>>8) - output.append(word&0xff) - to_file(palout, output) - - - # create a new map consisting of quaternary color ids - + palette = sorted(palette, key=luminance) + + # spit out new .pal file + #if palout != None: + # output = [] + # for color in palette[1:3]: + # word = rgb_to_dmg(color) + # output += [word & 0xff] + # output += [word >> 8] + # to_file(palout, output) + + # create a new map of quaternary color ids map = [] if padding['top']: map += [0] * (width + padding['left'] + padding['right']) * padding['top'] for line in image: @@ -1388,12 +1386,9 @@ def to_2bpp(filein, fileout=None, palout=None): if padding['bottom']: map += [0] * (width + padding['left'] + padding['right']) * padding['bottom'] # split it into strips of 8, and make them planar - num_columns = width / 8 num_rows = height / 8 - tile = 8 * 8 - image = [] for row in range(num_rows): for column in range(num_columns): -- cgit v1.2.3 From dc268dfbc6fe1e92f4053d9f28f601cf7a315b27 Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 12 Jun 2013 14:25:36 -0400 Subject: fix palette functions and output palettes in 2bpp conversion original-commit-id: 5d204ce5692c06f9b48bbad0fe1e42107bdf5231 --- gfx.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/gfx.py b/gfx.py index 8083cb1..2622707 100644 --- a/gfx.py +++ b/gfx.py @@ -1185,23 +1185,24 @@ def dmg2rgb(word): return ((red<<3)+0b100, (green<<3)+0b100, (blue<<3)+0b100, alpha) def rgb_to_dmg(color): - word = (color['r'] / 8) << 10 + word = (color['r'] / 8) word += (color['g'] / 8) << 5 - word += (color['b'] / 8) + word += (color['b'] / 8) << 10 return word def png_pal(filename): palette = [] - palette.append((255,255,255,255)) 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) - for word in dmg_pals: - palette.append(dmg2rgb(word)) - palette.append((000,000,000,255)) + white = (255,255,255,255) + black = (000,000,000,255) + for word in dmg_pals: palette += [dmg2rgb(word)] + if white not in dmg_pals and len(palette) < 4: palette = [white] + palette + if black not in dmg_pals and len(palette) < 4: palette += [black] return palette @@ -1366,14 +1367,18 @@ def to_2bpp(filein, fileout=None, palout=None): return sum(color[key] * -rough[key] for key in rough.keys()) palette = sorted(palette, key=luminance) - # spit out new .pal file - #if palout != None: - # output = [] - # for color in palette[1:3]: - # word = rgb_to_dmg(color) - # output += [word & 0xff] - # output += [word >> 8] - # to_file(palout, output) + # spit out a new .pal file + # disable this if it causes problems with paletteless images + if palout == None: + if os.path.exists(os.path.splitext(fileout)[0]+'.pal'): + palout = os.path.splitext(fileout)[0]+'.pal' + if palout != None: + output = [] + for color in palette: + word = rgb_to_dmg(color) + output += [word & 0xff] + output += [word >> 8] + to_file(palout, output) # create a new map of quaternary color ids map = [] -- cgit v1.2.3 From 1db2941c7df70433e05988edc0c7733c9c421077 Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 12 Jun 2013 14:38:38 -0400 Subject: gfx: palette checking in to_png original-commit-id: b06c96e4d2d9e365544f8abba9392752178a8fcd --- gfx.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gfx.py b/gfx.py index 2622707..4be7b87 100644 --- a/gfx.py +++ b/gfx.py @@ -1269,6 +1269,9 @@ def to_png(filein, fileout=None, pal_file=None, height=None, width=None): 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 @@ -1437,10 +1440,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': - if os.path.splitext(name)[0]+'.pal' in files: - to_png(os.path.join(root, name), None, os.path.join(root, os.path.splitext(name)[0]+'.pal')) - else: - to_png(os.path.join(root, name)) + to_png(os.path.join(root, name)) os.utime(os.path.join(root, name), None) # only monster and trainer pics for now @@ -1458,7 +1458,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), None, os.path.join(root, name[:-5]+'.pal')) + to_png(os.path.join(root, name)) os.utime(os.path.join(root, name), None) -- cgit v1.2.3 From 7ccbbcced852f9d4189d3fa4e4b7aaa979b1e105 Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 12 Jun 2013 15:51:13 -0400 Subject: gfx: workable command line functions original-commit-id: 5803db714c348574e09e428b79aa2c66531e74f5 --- gfx.py | 142 +++++++++++++++++++++++++++++------------------------------------ 1 file changed, 64 insertions(+), 78 deletions(-) diff --git a/gfx.py b/gfx.py index 4be7b87..03d5e49 100644 --- a/gfx.py +++ b/gfx.py @@ -1518,101 +1518,87 @@ def dump_tileset_pngs(): tileset_filename = "../gfx/tilesets/" + str(tileset_id).zfill(2) + ".lz" lz_to_png_by_file(tileset_filename) -if __name__ == "__main__": +def decompress_frontpic(lz_file): + """ + Convert the pic portion of front.lz to front.2bpp + """ + lz = open(lz_file, 'rb').read() + to_file(Decompressed(lz).pic, os.path.splitext(filein)[0] + '.2bpp') +def decompress_frontpic_anim(lz_file): + """ + Convert the animation tile portion of front.lz to tiles.2bpp + """ + lz = open(lz_file, 'rb').read() + to_file(Decompressed(lz).animtiles, 'tiles.2bpp') + + +if __name__ == "__main__": debug = False + + argv = [None] * 5 + for i, arg in enumerate(sys.argv): + argv[i] = arg - if sys.argv[1] == 'dump-pngs': + if argv[1] == 'dump-pngs': mass_to_colored_png() - elif sys.argv[1] == 'lz-to-png': - lz_to_png_by_file(sys.argv[2]) + elif argv[1] == 'front-to-2bpp': + decompress_frontpic(argv[2]) - elif sys.argv[1] == 'png-to-lz': - # python gfx.py png-to-lz [--front anim(2bpp) | --vert] [png] + elif argv[1] == 'anim-from-front': + decompress_frontpic_anim(argv[2]) - # python gfx.py png-to-lz --front [anim(2bpp)] [png] - if sys.argv[2] == '--front': + elif argv[1] == 'lz-to-2bpp': + name = os.path.splitext(argv[3])[0] + lz = open(name+'.lz', 'rb').read() + if argv[2] == '--vert': + to_file(name+'.2bpp', Decompressed(lz, 'vert').output) + else: + to_file(name+'.2bpp', Decompressed(lz).output) - # front.png and tiles.png are combined before compression, - # so we have to pass in things like anim file and pic size - name = os.path.splitext(sys.argv[4])[0] + elif argv[1] == 'lz-to-png': + if argv[2] == '--vert': + name = os.path.splitext(argv[3])[0] + 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)) + else: + lz_to_png_by_file(argv[2]) + elif argv[1] == 'png-to-lz': + # python gfx.py png-to-lz [--front anim(2bpp) | --vert] [png] + if argv[2] == '--front': + # front.2bpp and tiles.2bpp are combined before compression, + # so we have to pass in the anim file and pic size + name = os.path.splitext(argv[4])[0] to_2bpp(name+'.png', name+'.2bpp') pic = open(name+'.2bpp', 'rb').read() - anim = open(sys.argv[3], 'rb').read() + anim = open(argv[3], 'rb').read() size = int(sqrt(len(pic)/16)) # assume square pic to_file(name+'.lz', Compressed(pic + anim, 'vert', size).output) - - - # python gfx.py png-to-lz --vert [png] - elif sys.argv[2] == '--vert': - - # others are vertically oriented (frontpics are always vertical) - - name = os.path.splitext(sys.argv[3])[0] - + elif argv[2] == '--vert': + name = os.path.splitext(argv[3])[0] to_2bpp(name+'.png', name+'.2bpp') pic = open(name+'.2bpp', 'rb').read() to_file(name+'.lz', Compressed(pic, 'vert').output) - - - # python gfx.py png-to-lz [png] else: + png_to_lz(argv[2]) - # standard usage - - png_to_lz(sys.argv[2]) - - elif sys.argv[1] == 'png-to-2bpp': - to_2bpp(sys.argv[2]) - - - elif sys.argv[1] == 'de': - # python gfx.py de [addr] [fileout] [mode] - - rom = load_rom() - - addr = int(sys.argv[2],16) - fileout = sys.argv[3] - mode = sys.argv[4] - decompress_from_address(addr, fileout, mode) - if debug: print 'decompressed to ' + sys.argv[3] + ' from ' + hex(int(sys.argv[2],16)) + '!' + elif argv[1] == 'png-to-2bpp': + to_2bpp(argv[2]) - elif sys.argv[1] == 'lz': - # python gfx.py lz [filein] [fileout] [mode] - filein = sys.argv[2] - fileout = sys.argv[3] - mode = sys.argv[4] - compress_file(filein, fileout, mode) - if debug: print 'compressed ' + filein + ' to ' + fileout + '!' - - elif sys.argv[1] == 'lzf': - # python gfx.py lzf [id] [fileout] - compress_monster_frontpic(int(sys.argv[2]), sys.argv[3]) - - elif sys.argv[1] == 'un': - # python gfx.py un [address] [num_tiles] [filename] - rom = load_rom() - get_uncompressed_gfx(int(sys.argv[2],16), int(sys.argv[3]), sys.argv[4]) - - elif sys.argv[1] == 'pal': - # python gfx.py pal [address] [length] - rom = load_rom() - print grab_palettes(int(sys.argv[2],16), int(sys.argv[3])) - - elif sys.argv[1] == 'png': - - if '.2bpp' in sys.argv[2]: - if sys.argv[4] == 'greyscale': - to_png(sys.argv[2], sys.argv[3]) - else: - to_png(sys.argv[2], sys.argv[3], sys.argv[4]) - - elif '.png' in sys.argv[2]: - to_2bpp(sys.argv[2], sys.argv[3]) - - elif sys.argv[1] == 'mass-decompress': - mass_decompress() - if debug: print 'decompressed known gfx to pokecrystal/gfx/!' + elif argv[1] == '2bpp-to-lz': + if argv[2] == '--vert': + filein = argv[3] + fileout = argv[4] + compress_file(filein, fileout, 'vert') + else: + filein = argv[2] + fileout = argv[3] + compress_file(filein, fileout) + + elif argv[1] == '2bpp-to-png': + to_png(argv[2]) -- cgit v1.2.3 From 3cadc09cc031fdb37d3703b4df56d6021dab01f5 Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 12 Jun 2013 21:31:45 -0400 Subject: expand shortened palettes rather than keep up inconsistent palette formats, just incbin a portion of each original-commit-id: 6ba758aa53bbf14e2c152fd88f786a501f6bb029 --- gfx.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/gfx.py b/gfx.py index 03d5e49..1667bb4 100644 --- a/gfx.py +++ b/gfx.py @@ -1532,6 +1532,26 @@ def decompress_frontpic_anim(lz_file): lz = open(lz_file, 'rb').read() to_file(Decompressed(lz).animtiles, 'tiles.2bpp') +def expand_pic_palettes(): + """ + Add white and black to palette files with fewer than 4 colors. + + Pokemon Crystal only defines two colors for a pic palette to + save space, filling in black/white at runtime. + Instead of managing palette files of varying length, black + and white are added to pic palettes and excluded from incbins. + """ + for root, dirs, files in os.walk('../gfx/'): + if 'gfx/pics' in root or 'gfx/trainers' in root: + for name in files: + if os.path.splitext(name)[1] == '.pal': + filename = os.path.join(root, name) + palette = bytearray(open(filename, 'rb').read()) + w = bytearray([0xff, 0x7f]) + b = bytearray([0x00, 0x00]) + if len(palette) == 4: + with open(filename, 'wb') as out: + out.write(w + palette + b) if __name__ == "__main__": debug = False -- cgit v1.2.3