diff options
Diffstat (limited to 'pokemontools/gfx.py')
-rw-r--r-- | pokemontools/gfx.py | 770 |
1 files changed, 290 insertions, 480 deletions
diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py index dc3456a..066811a 100644 --- a/pokemontools/gfx.py +++ b/pokemontools/gfx.py @@ -5,17 +5,22 @@ import sys import png from math import sqrt, floor, ceil import argparse +import yaml +import operator import configuration config = configuration.Config() -import pokemon_constants +from pokemon_constants import pokemon_constants import trainers import romstr +from lz import Compressed, Decompressed -def load_rom(): - rom = romstr.RomStr.load(filename=config.rom_path) + + +def load_rom(filename=config.rom_path): + rom = romstr.RomStr.load(filename=filename) return bytearray(rom) def rom_offset(bank, address): @@ -124,19 +129,31 @@ def deinterleave_tiles(image, width): return connect(deinterleave(get_tiles(image), width)) -def condense_tiles_to_map(image): +def condense_tiles_to_map(image, pic=0): tiles = get_tiles(image) - new_tiles = [] - tilemap = [] - for tile in tiles: + + # Leave the first frame intact for pics. + new_tiles = tiles[:pic] + tilemap = range(pic) + + for i, tile in enumerate(tiles[pic:]): if tile not in new_tiles: new_tiles += [tile] - tilemap += [new_tiles.index(tile)] + + # Match the first frame where possible. + if tile == new_tiles[i % pic]: + tilemap += [i % pic] + else: + tilemap += [new_tiles.index(tile)] + new_image = connect(new_tiles) return new_image, tilemap def to_file(filename, data): + """ + Apparently open(filename, 'wb').write(bytearray(data)) won't work. + """ file = open(filename, 'wb') for byte in data: file.write('%c' % byte) @@ -144,425 +161,6 @@ def to_file(filename, data): -""" -A rundown of Pokemon Crystal's compression scheme: - -Control commands occupy bits 5-7. -Bits 0-4 serve as the first parameter <n> for each command. -""" -lz_commands = { - 'literal': 0, # n values for n bytes - 'iterate': 1, # one value for n bytes - 'alternate': 2, # alternate two values for n bytes - 'blank': 3, # zero for n bytes -} - -""" -Repeater commands repeat any data that was just decompressed. -They take an additional signed parameter <s> to mark a relative starting point. -These wrap around (positive from the start, negative from the current position). -""" -lz_commands.update({ - 'repeat': 4, # n bytes starting from s - 'flip': 5, # n bytes in reverse bit order starting from s - 'reverse': 6, # n bytes backwards starting from s -}) - -""" -The long command is used when 5 bits aren't enough. Bits 2-4 contain a new control code. -Bits 0-1 are appended to a new byte as 8-9, allowing a 10-bit parameter. -""" -lz_commands.update({ - 'long': 7, # n is now 10 bits for a new control code -}) -max_length = 1 << 10 # can't go higher than 10 bits -lowmax = 1 << 5 # standard 5-bit param - -""" -If 0xff is encountered instead of a command, decompression ends. -""" -lz_end = 0xff - - -class Compressed: - - def __init__(self, data=None, commands=lz_commands, debug=False): - self.data = list(bytearray(data)) - self.commands = commands - self.debug = debug - self.compress() - - def byte_at(self, address): - if address < len(self.data): - return self.data[address] - return None - - def compress(self): - """ - This algorithm is greedy. - It aims to match the compressor it's based on as closely as possible. - It doesn't, but in the meantime the output is smaller. - """ - self.address = 0 - self.end = len(self.data) - self.output = [] - self.literal = [] - - while self.address < self.end: - # Tally up the number of bytes that can be compressed - # by a single command from the current address. - self.scores = {} - for method in self.commands.keys(): - self.scores[method] = 0 - - # The most common byte by far is 0 (whitespace in - # images and padding in tilemaps and regular data). - address = self.address - while self.byte_at(address) == 0x00: - self.scores['blank'] += 1 - address += 1 - - # In the same vein, see how long the same byte repeats for. - address = self.address - self.iter = self.byte_at(address) - while self.byte_at(address) == self.iter: - self.scores['iterate'] += 1 - address += 1 - - # Do it again, but for alternating bytes. - address = self.address - self.alts = [] - self.alts += [self.byte_at(address)] - self.alts += [self.byte_at(address + 1)] - while self.byte_at(address) == self.alts[(address - self.address) % 2]: - self.scores['alternate'] += 1 - address += 1 - - # Check if we can repeat any data that the - # decompressor just output (here, the input data). - # TODO this includes the current command's output - self.matches = {} - last_matches = {} - address = self.address - min_length = 4 # minimum worthwhile length - max_length = 9 # any further and the time loss is too significant - for length in xrange(min_length, min(len(self.data) - address, max_length)): - keyword = self.data[address:address+length] - for offset, byte in enumerate(self.data[:address]): - # offset ranges are -0x80:-1 and 0:0x7fff - if offset > 0x7fff and offset < address - 0x80: - continue - if byte == keyword[0]: - # Straight repeat... - if self.data[offset:offset+length] == keyword: - if self.scores['repeat'] < length: - self.scores['repeat'] = length - self.matches['repeat'] = offset - # In reverse... - if self.data[offset-1:offset-length-1:-1] == keyword: - if self.scores['reverse'] < length: - self.scores['reverse'] = length - self.matches['reverse'] = offset - # Or bitflipped - if self.bit_flip([byte]) == self.bit_flip([keyword[0]]): - if self.bit_flip(self.data[offset:offset+length]) == self.bit_flip(keyword): - if self.scores['flip'] < length: - self.scores['flip'] = length - self.matches['flip'] = offset - if self.matches == last_matches: - break - last_matches = list(self.matches) - - # If the scores are too low, try again from the next byte. - if not any(map(lambda x: { - 'blank': 1, - 'iterate': 2, - 'alternate': 3, - 'repeat': 3, - 'reverse': 3, - 'flip': 3, - }.get(x[0], 10000) < x[1], self.scores.items())): - self.literal += [self.data[self.address]] - self.address += 1 - - else: # payload - # bug: literal [00] is a byte longer than blank 1. - # this bug exists in the target compressor as well, - # so don't fix until we've given up on replicating it. - self.do_literal() - self.do_scored() - - # unload any literals we're sitting on - self.do_literal() - self.output += [lz_end] - - def bit_flip(self, data): - return [sum(((byte >> i) & 1) << (7 - i) for i in xrange(8)) for byte in data] - - def do_literal(self): - if self.literal: - cmd = self.commands['literal'] - length = len(self.literal) - self.do_cmd(cmd, length) - # self.address has already been - # incremented in the main loop - self.literal = [] - - def do_cmd(self, cmd, length): - if length > max_length: - length = max_length - - cmd_length = length - 1 - - if length > lowmax: - output = [(self.commands['long'] << 5) + (cmd << 2) + (cmd_length >> 8)] - output += [cmd_length & 0xff] - else: - output = [(cmd << 5) + cmd_length] - - if cmd == self.commands['literal']: - output += self.literal - elif cmd == self.commands['iterate']: - output += [self.iter] - elif cmd == self.commands['alternate']: - output += self.alts - else: - for command in ['repeat', 'reverse', 'flip']: - if cmd == self.commands[command]: - offset = self.matches[command] - # negative offsets are a byte shorter - if self.address - offset <= 0x80: - offset = self.address - offset + 0x80 - if cmd == self.commands['repeat']: - offset -= 1 # this is a hack, but it seems to work - output += [offset] - else: - output += [offset / 0x100, offset % 0x100] - - if self.debug: - print ( - dict(map(reversed, self.commands.items()))[cmd], - length, '\t', - ' '.join(map('{:02x}'.format, output)) - ) - - self.output += output - return length - - def do_scored(self): - # Which command did the best? - winner, score = sorted( - self.scores.items(), - key=lambda x:(-x[1], [ - 'blank', - 'repeat', - 'reverse', - 'flip', - 'iterate', - 'alternate', - 'literal', - 'long', # hack - ].index(x[0])) - )[0] - cmd = self.commands[winner] - length = self.do_cmd(cmd, score) - self.address += length - - - -class Decompressed: - """ - Parse compressed data, usually 2bpp. - - parameters: - [compressed data] - [tile arrangement] default: 'vert' - [size of pic] default: None - [start] (optional) - - splits output into pic [size] and animation tiles if applicable - data can be fed in from rom if [start] is specified - """ - - def __init__(self, lz=None, start=0, debug=False): - # todo: play nice with Compressed - - assert lz, 'need something to decompress!' - self.lz = bytearray(lz) - - self.byte = None - self.address = 0 - self.start = start - - self.output = [] - - self.decompress() - - self.compressed_data = self.lz[self.start : self.start + self.address] - - # print tuple containing start and end address - if debug: print '(' + hex(self.start) + ', ' + hex(self.start + self.address+1) + '),' - - - def command_list(self): - """ - Print a list of commands that were used. Useful for debugging. - """ - - data = bytearray(self.lz) - address = self.address - while 1: - cmd_addr = address - byte = data[address] - address += 1 - if byte == lz_end: break - cmd = (byte >> 5) & 0b111 - if cmd == lz_commands['long']: - cmd = (byte >> 2) & 0b111 - length = (byte & 0b11) << 8 - length += data[address] - address += 1 - else: - length = byte & 0b11111 - length += 1 - name = dict(map(reversed, lz_commands.items()))[cmd] - if name == 'iterate': - address += 1 - elif name == 'alternate': - address += 2 - elif name in ['repeat', 'reverse', 'flip']: - if data[address] < 0x80: - address += 2 - else: - address += 1 - elif name == 'literal': - address += length - print name, length, '\t', ' '.join(map('{:02x}'.format, list(data)[cmd_addr:address])) - - - def decompress(self): - """ - Replica of crystal's decompression. - """ - - self.output = [] - - while True: - self.getCurByte() - - if (self.byte == lz_end): - self.address += 1 - break - - self.cmd = (self.byte & 0b11100000) >> 5 - - if self.cmd == lz_commands['long']: # 10-bit param - self.cmd = (self.byte & 0b00011100) >> 2 - self.length = (self.byte & 0b00000011) << 8 - self.next() - self.length += self.byte + 1 - else: # 5-bit param - self.length = (self.byte & 0b00011111) + 1 - - # literals - if self.cmd == lz_commands['literal']: - self.doLiteral() - elif self.cmd == lz_commands['iterate']: - self.doIter() - elif self.cmd == lz_commands['alternate']: - self.doAlt() - elif self.cmd == lz_commands['blank']: - self.doZeros() - - else: # repeaters - self.next() - if self.byte > 0x7f: # negative - self.displacement = self.byte & 0x7f - self.displacement = len(self.output) - self.displacement - 1 - else: # positive - self.displacement = self.byte * 0x100 - self.next() - self.displacement += self.byte - - if self.cmd == lz_commands['flip']: - self.doFlip() - elif self.cmd == lz_commands['reverse']: - self.doReverse() - else: # lz_commands['repeat'] - self.doRepeat() - - self.address += 1 - #self.next() # somewhat of a hack - - - def getCurByte(self): - self.byte = self.lz[self.start+self.address] - - def next(self): - self.address += 1 - self.getCurByte() - - def doLiteral(self): - """ - Copy data directly. - """ - for byte in range(self.length): - self.next() - self.output.append(self.byte) - - def doIter(self): - """ - Write one byte repeatedly. - """ - self.next() - for byte in range(self.length): - self.output.append(self.byte) - - def doAlt(self): - """ - Write alternating bytes. - """ - self.alts = [] - self.next() - self.alts.append(self.byte) - self.next() - self.alts.append(self.byte) - - for byte in range(self.length): - self.output.append(self.alts[byte&1]) - - def doZeros(self): - """ - Write zeros. - """ - for byte in range(self.length): - self.output.append(0x00) - - def doFlip(self): - """ - Repeat flipped bytes from output. - - eg 11100100 -> 00100111 - quat 3 2 1 0 -> 0 2 1 3 - """ - for byte in range(self.length): - flipped = sum(1<<(7-i) for i in range(8) if self.output[self.displacement+byte]>>i&1) - self.output.append(flipped) - - def doReverse(self): - """ - Repeat reversed bytes from output. - """ - for byte in range(self.length): - self.output.append(self.output[self.displacement-byte]) - - def doRepeat(self): - """ - Repeat bytes from output. - """ - for byte in range(self.length): - self.output.append(self.output[self.displacement+byte]) - sizes = [ @@ -930,7 +528,7 @@ def dump_monster_pals(): pal_length = 0x4 for mon in range(251): - name = pokemon_constants.pokemon_constants[mon+1].title().replace('_','') + name = pokemon_constants[mon+1].title().replace('_','') num = str(mon+1).zfill(3) dir = 'gfx/pics/'+num+'/' @@ -1088,33 +686,80 @@ def png_to_rgb(palette): return output -def read_filename_arguments(filename): - int_args = { +def read_yaml_arguments(filename, yaml_filename = os.path.join(config.path, 'gfx.yaml'), path_arguments = ['pal_file']): + + parsed_arguments = {} + + # Read arguments from gfx.yaml if it exists. + if os.path.exists(yaml_filename): + yargs = yaml.load(open(yaml_filename)) + dirs = os.path.splitext(filename)[0].split('/') + current_path = os.path.dirname(filename) + path = [] + while yargs: + for key, value in yargs.items(): + # Follow directories to the bottom while picking up keys. + # Try not to mistake other files for keys. + parsed_path = os.path.join( * (path + [key]) ) + for guessed_path in map(parsed_path.__add__, ['', '.png']): + if os.path.exists(guessed_path) or '.' in key: + if guessed_path != filename: + continue + if key in path_arguments: + value = os.path.join(current_path, value) + parsed_arguments[key] = value + if not dirs: + break + yargs = yargs.get(dirs[0], {}) + path.append(dirs.pop(0)) + + return parsed_arguments + +def read_filename_arguments(filename, yaml_filename = os.path.join(config.path, 'gfx.yaml'), path_arguments = ['pal_file']): + """ + Infer graphics conversion arguments given a filename. + + If it exists, ./gfx.yaml is traversed for arguments. + Then additional arguments within the filename (separated with ".") are grabbed. + """ + parsed_arguments = {} + + parsed_arguments.update(read_yaml_arguments( + filename, + yaml_filename = yaml_filename, + path_arguments = path_arguments + )) + + int_arguments = { 'w': 'width', 'h': 'height', 't': 'tile_padding', } - parsed_arguments = {} + # Filename arguments override yaml. arguments = os.path.splitext(filename)[0].lstrip('.').split('.')[1:] for argument in arguments: + + # Check for integer arguments first (i.e. "w128"). arg = argument[0] param = argument[1:] if param.isdigit(): - arg = int_args.get(arg, False) + arg = int_arguments.get(arg, False) if arg: parsed_arguments[arg] = int(param) - elif argument == 'interleave': - parsed_arguments['interleave'] = True - elif argument == 'norepeat': - parsed_arguments['norepeat'] = True + elif argument == 'arrange': parsed_arguments['norepeat'] = True parsed_arguments['tilemap'] = True - elif 'x' in argument: + + # Pic dimensions (i.e. "6x6"). + elif 'x' in argument and any(map(str.isdigit, argument)): w, h = argument.split('x') if w.isdigit() and h.isdigit(): parsed_arguments['pic_dimensions'] = (int(w), int(h)) + else: + parsed_arguments[argument] = True + return parsed_arguments @@ -1249,24 +894,172 @@ def convert_2bpp_to_png(image, **kwargs): return width, height, palette, greyscale, bitdepth, px_map -def export_png_to_2bpp(filein, fileout=None, palout=None, tile_padding=0, pic_dimensions=None): +def get_pic_animation(tmap, w, h): + """ + Generate pic animation data from a combined tilemap of each frame. + """ + frame_text = '' + bitmask_text = '' + + frames = list(split(tmap, w * h)) + base = frames.pop(0) + bitmasks = [] + + for i in xrange(len(frames)): + frame_text += '\tdw .frame{}\n'.format(i + 1) + + for i, frame in enumerate(frames): + bitmask = map(operator.eq, frame, base) + if bitmask not in bitmasks: + bitmasks.append(bitmask) + which_bitmask = bitmasks.index(bitmask) + + mask = iter(bitmask) + masked_frame = filter(mask.next, frame) + + frame_text += '.frame{}\n'.format(i + 1) + frame_text += '\tdb ${:02x} ; bitmask\n'.format(which_bitmask) + if masked_frame: + frame_text += '\tdb {}\n'.format(', '.join( + map('${:02x}'.format, masked_frame) + )) + frame_text += '\n' + + for i, bitmask in enumerate(bitmasks): + bitmask_text += '; {}\n'.format(i) + for byte in split(bitmask, 8): + byte = int(''.join(map(int.__repr__, reversed(byte))), 2) + bitmask_text += '\tdb %{:08b}\n'.format(byte) + + return frame_text, bitmask_text + + +def dump_pic_animations(addresses={'bitmasks': 'BitmasksPointers', 'frames': 'FramesPointers'}, pokemon=pokemon_constants, rom=None): + """ + The code to dump pic animations from rom is mysteriously absent. + Here it is again, but now it dumps images instead of text. + Said text can then be derived from the images. + """ + + if rom is None: rom = load_rom() + + # Labels can be passed in instead of raw addresses. + for which, offset in addresses.items(): + if type(offset) is str: + for line in open('pokecrystal.sym').readlines(): + if offset in line.split(): + addresses[which] = rom_offset(*map(lambda x: int(x, 16), line[:7].split(':'))) + break + + for i, name in pokemon.items(): + if name.lower() == 'unown': continue + + i -= 1 + + directory = os.path.join('gfx', 'pics', name.lower()) + size = sizes[i] + + if i > 151 - 1: + bank = 0x36 + else: + bank = 0x35 + address = addresses['frames'] + i * 2 + address = rom_offset(bank, rom[address] + rom[address + 1] * 0x100) + addrs = [] + while address not in addrs: + addr = rom[address] + rom[address + 1] * 0x100 + addrs.append(rom_offset(bank, addr)) + address += 2 + num_frames = len(addrs) + + # To go any further, we need bitmasks. + # Bitmasks need the number of frames, which we now have. + + bank = 0x34 + address = addresses['bitmasks'] + i * 2 + address = rom_offset(bank, rom[address] + rom[address + 1] * 0x100) + length = size ** 2 + num_bytes = (length + 7) / 8 + bitmasks = [] + for _ in xrange(num_frames): + bitmask = [] + bytes_ = rom[ address : address + num_bytes ] + for byte in bytes_: + bits = map(int, bin(byte)[2:].zfill(8)) + bits.reverse() + bitmask += bits + bitmasks.append(bitmask) + address += num_bytes + + # Back to frames: + frames = [] + for addr in addrs: + bitmask = bitmasks[rom[addr]] + num_tiles = len(filter(int, bitmask)) + frame = (rom[addr], rom[addr + 1 : addr + 1 + num_tiles]) + frames.append(frame) + + tmap = range(length) * (len(frames) + 1) + for i, frame in enumerate(frames): + bitmask = bitmasks[frame[0]] + tiles = (x for x in frame[1]) + for j, bit in enumerate(bitmask): + if bit: + tmap[(i + 1) * length + j] = tiles.next() + + filename = os.path.join(directory, 'front.{0}x{0}.2bpp.lz'.format(size)) + tiles = get_tiles(Decompressed(open(filename).read()).output) + new_tiles = map(tiles.__getitem__, tmap) + new_image = connect(new_tiles) + filename = os.path.splitext(filename)[0] + to_file(filename, new_image) + export_2bpp_to_png(filename) + + +def export_png_to_2bpp(filein, fileout=None, palout=None, **kwargs): arguments = { - 'tile_padding': tile_padding, - 'pic_dimensions': pic_dimensions, + 'tile_padding': 0, + 'pic_dimensions': None, + 'animate': False, + 'stupid_bitmask_hack': [], } + arguments.update(kwargs) arguments.update(read_filename_arguments(filein)) - image, palette, tmap = png_to_2bpp(filein, **arguments) + image, arguments = png_to_2bpp(filein, **arguments) if fileout == None: fileout = os.path.splitext(filein)[0] + '.2bpp' to_file(fileout, image) - if tmap != None: - mapout = os.path.splitext(fileout)[0] + '.tilemap' - to_file(mapout, tmap) + tmap = arguments.get('tmap') + + if tmap != None and arguments['animate'] and arguments['pic_dimensions']: + # Generate pic animation data. + frame_text, bitmask_text = get_pic_animation(tmap, *arguments['pic_dimensions']) + + frames_path = os.path.join(os.path.split(fileout)[0], 'frames.asm') + with open(frames_path, 'w') as out: + out.write(frame_text) + + bitmask_path = os.path.join(os.path.split(fileout)[0], 'bitmask.asm') + # The following Pokemon have a bitmask dummied out. + for exception in arguments['stupid_bitmask_hack']: + if exception in bitmask_path: + bitmasks = bitmask_text.split(';') + bitmasks[-1] = bitmasks[-1].replace('1', '0') + bitmask_text = ';'.join(bitmasks) + + with open(bitmask_path, 'w') as out: + out.write(bitmask_text) + + elif tmap != None and arguments.get('tilemap', False): + tilemap_path = os.path.splitext(fileout)[0] + '.tilemap' + to_file(tilemap_path, tmap) + + palette = arguments.get('palette') if palout == None: palout = os.path.splitext(fileout)[0] + '.pal' export_palette(palette, palout) @@ -1299,55 +1092,58 @@ def png_to_2bpp(filein, **kwargs): Convert a png image to planar 2bpp. """ - tile_padding = kwargs.get('tile_padding', 0) - pic_dimensions = kwargs.get('pic_dimensions', None) - interleave = kwargs.get('interleave', False) - norepeat = kwargs.get('norepeat', False) - tilemap = kwargs.get('tilemap', False) + arguments = { + 'tile_padding': 0, + 'pic_dimensions': False, + 'interleave': False, + 'norepeat': False, + 'tilemap': False, + } + arguments.update(kwargs) + + if type(filein) is str: + filein = open(filein) + + assert type(filein) is file - with open(filein, 'rb') as data: - width, height, rgba, info = png.Reader(data).asRGBA8() - rgba = list(rgba) - greyscale = info['greyscale'] + width, height, rgba, info = png.Reader(filein).asRGBA8() # png.Reader returns flat pixel data. Nested is easier to work with - len_px = 4 # rgba + len_px = len('rgba') image = [] palette = [] for line in rgba: newline = [] for px in xrange(0, len(line), len_px): - color = { 'r': line[px ], - 'g': line[px+1], - 'b': line[px+2], - 'a': line[px+3], } - newline += [color] + color = dict(zip('rgba', line[px:px+len_px])) if color not in palette: - palette += [color] + if len(palette) < 4: + palette += [color] + else: + # TODO Find the nearest match + print 'WARNING: %s: Color %s truncated to' % (filein, color), + color = sorted(palette, key=lambda x: sum(x.values()))[0] + print color + newline += [color] image += [newline] - assert len(palette) <= 4, 'Palette should be 4 colors, is really %d' % len(palette) + assert len(palette) <= 4, '%s: palette should be 4 colors, is really %d (%s)' % (filein, len(palette), palette) # Pad out smaller palettes with greyscale colors - hues = { - 'white': { 'r': 0xff, 'g': 0xff, 'b': 0xff, 'a': 0xff }, + greyscale = { '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 }, + 'white': { 'r': 0xff, 'g': 0xff, 'b': 0xff, 'a': 0xff }, } - for hue in hues.values(): + preference = 'white', 'black', 'grey', 'gray' + for hue in map(greyscale.get, preference): if len(palette) >= 4: break if hue not in palette: palette += [hue] - # Sort palettes by luminance - def luminance(color): - rough = { 'r': 4.7, - 'g': 1.4, - 'b': 13.8, } - return sum(color[key] * rough[key] for key in rough.keys()) - palette.sort(key=luminance) + palette.sort(key=lambda x: sum(x.values())) # Game Boy palette order palette.reverse() @@ -1391,8 +1187,16 @@ def png_to_2bpp(filein, **kwargs): top += (quad /2 & 1) << (7 - bit) image += [bottom, top] - if pic_dimensions: - w, h = pic_dimensions + dim = arguments['pic_dimensions'] + if dim: + if type(dim) in (tuple, list): + w, h = dim + else: + # infer dimensions based on width. + w = width / tile_width + h = height / tile_height + if h % w == 0: + h = w tiles = get_tiles(image) pic_length = w * h @@ -1410,17 +1214,23 @@ def png_to_2bpp(filein, **kwargs): image = connect(new_image) # Remove any tile padding used to make the png rectangular. - image = image[:len(image) - tile_padding * 0x10] + image = image[:len(image) - arguments['tile_padding'] * 0x10] + + tmap = None - if interleave: + if arguments['interleave']: image = deinterleave_tiles(image, num_columns) - if norepeat: + if arguments['pic_dimensions']: + image, tmap = condense_tiles_to_map(image, w * h) + elif arguments['norepeat']: image, tmap = condense_tiles_to_map(image) - if not tilemap: - tmap = None + if not arguments['tilemap']: + tmap = None + + arguments.update({ 'palette': palette, 'tmap': tmap, }) - return image, palette, tmap + return image, arguments def export_palette(palette, filename): @@ -1510,7 +1320,7 @@ def export_png_to_1bpp(filename, fileout=None): to_file(fileout, image) def png_to_1bpp(filename, **kwargs): - image, palette, tmap = png_to_2bpp(filename, **kwargs) + image, kwargs = png_to_2bpp(filename, **kwargs) return convert_2bpp_to_1bpp(image) |