summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pokemontools/gfx.py106
-rw-r--r--pokemontools/pic.py488
2 files changed, 570 insertions, 24 deletions
diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py
index a1a7bcf..456b105 100644
--- a/pokemontools/gfx.py
+++ b/pokemontools/gfx.py
@@ -112,6 +112,18 @@ def deinterleave_tiles(image, width):
return connect(deinterleave(get_tiles(image), width))
+def condense_tiles_to_map(image):
+ tiles = get_tiles(image)
+ new_tiles = []
+ tilemap = []
+ for tile in tiles:
+ if tile not in new_tiles:
+ new_tiles += [tile]
+ tilemap += [new_tiles.index(tile)]
+ new_image = connect(new_tiles)
+ return new_image, tilemap
+
+
def to_file(filename, data):
file = open(filename, 'wb')
for byte in data:
@@ -1282,6 +1294,11 @@ def read_filename_arguments(filename):
parsed_arguments['pic_dimensions'] = (int(w), int(h))
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
return parsed_arguments
@@ -1339,15 +1356,22 @@ def convert_2bpp_to_png(image, **kwargs):
# Pad the image by a given number of tiles if asked.
image += chr(0) * 0x10 * tile_padding
- # Frontpics are transposed independently of animation graphics.
+ # Some images are transposed in blocks.
if pic_dimensions:
w, h = pic_dimensions
- i = w * h * 0x10
- pic = ''.join(transpose_tiles(image[:i], w))
- anim = image[i:]
- image = pic + anim
- # Pad out animation tiles as well.
- image += chr(0) * 0x10 * ((w - len(get_tiles(image)) % h) % w)
+ if not width: width = w * 8
+
+ pic_length = w * h * 0x10
+
+ trailing = len(image) % pic_length
+
+ pic = []
+ for i in xrange(0, len(image) - trailing, pic_length):
+ pic += transpose_tiles(image[i:i+pic_length], w)
+ image = ''.join(pic) + image[len(image) - trailing:]
+
+ # Pad out trailing lines.
+ image += chr(0) * 0x10 * ((w - (len(image) / 0x10) % h) % w)
def px_length(img):
return len(img) * 4
@@ -1413,12 +1437,16 @@ def export_png_to_2bpp(filein, fileout=None, palout=None, tile_padding=0, pic_di
}
arguments.update(read_filename_arguments(filein))
- image, palette = png_to_2bpp(filein, **arguments)
+ image, palette, tmap = 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)
+
if palout == None:
palout = os.path.splitext(fileout)[0] + '.pal'
export_palette(palette, palout)
@@ -1433,12 +1461,12 @@ def get_image_padding(width, height, wstep=8, hstep=8):
'bottom': 0,
}
- if width % wstep:
+ if width % wstep and width >= wstep:
pad = float(width % wstep) / 2
padding['left'] = int(ceil(pad))
padding['right'] = int(floor(pad))
- if height % hstep:
+ if height % hstep and height >= hstep:
pad = float(height % hstep) / 2
padding['top'] = int(ceil(pad))
padding['bottom'] = int(floor(pad))
@@ -1454,6 +1482,8 @@ def png_to_2bpp(filein, **kwargs):
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)
with open(filein, 'rb') as data:
width, height, rgba, info = png.Reader(data).asRGBA8()
@@ -1520,15 +1550,15 @@ def png_to_2bpp(filein, **kwargs):
# Graphics are stored in tiles instead of lines
tile_width = 8
tile_height = 8
- num_columns = width / tile_width
- num_rows = height / tile_height
+ num_columns = max(width, tile_width) / tile_width
+ num_rows = max(height, tile_height) / tile_height
image = []
for row in xrange(num_rows):
for column in xrange(num_columns):
# Split it up into strips to convert to planar data
- for strip in xrange(tile_height):
+ for strip in xrange(min(tile_height, height)):
anchor = (
row * num_columns * tile_width * tile_height +
column * tile_width +
@@ -1541,13 +1571,23 @@ def png_to_2bpp(filein, **kwargs):
top += (quad /2 & 1) << (7 - bit)
image += [bottom, top]
- # Frontpics are transposed independently of animation graphics.
if pic_dimensions:
- w, h = pic_dimensions
- i = w * h * 0x10
- pic = transpose_tiles(image[:i], w)
- anim = image[i:]
- image = pic + anim
+ w, h = pic_dimensions
+
+ tiles = get_tiles(image)
+ pic_length = w * h
+ tile_width = width / 8
+ trailing = len(tiles) % pic_length
+ new_image = []
+ for block in xrange(len(tiles) / pic_length):
+ offset = (h * tile_width) * ((block * w) / tile_width) + ((block * w) % tile_width)
+ pic = []
+ for row in xrange(h):
+ index = offset + (row * tile_width)
+ pic += tiles[index:index + w]
+ new_image += transpose(pic, w)
+ new_image += tiles[len(tiles) - trailing:]
+ image = connect(new_image)
# Remove any tile padding used to make the png rectangular.
image = image[:len(image) - tile_padding * 0x10]
@@ -1555,7 +1595,12 @@ def png_to_2bpp(filein, **kwargs):
if interleave:
image = deinterleave_tiles(image, num_columns)
- return image, palette
+ if norepeat:
+ image, tmap = condense_tiles_to_map(image)
+ if not tilemap:
+ tmap = None
+
+ return image, palette, tmap
def export_palette(palette, filename):
@@ -1645,7 +1690,7 @@ def export_png_to_1bpp(filename, fileout=None):
to_file(fileout, image)
def png_to_1bpp(filename, **kwargs):
- image, palette = png_to_2bpp(filename, **kwargs)
+ image, palette, tmap = png_to_2bpp(filename, **kwargs)
return convert_2bpp_to_1bpp(image)
@@ -1789,7 +1834,7 @@ def expand_pic_palettes():
def convert_to_2bpp(filenames=[]):
for filename in filenames:
- name, extension = os.path.splitext(filename)
+ filename, name, extension = try_decompress(filename)
if extension == '.1bpp':
export_1bpp_to_2bpp(filename)
elif extension == '.2bpp':
@@ -1801,7 +1846,7 @@ def convert_to_2bpp(filenames=[]):
def convert_to_1bpp(filenames=[]):
for filename in filenames:
- name, extension = os.path.splitext(filename)
+ filename, name, extension = try_decompress(filename)
if extension == '.1bpp':
pass
elif extension == '.2bpp':
@@ -1813,7 +1858,7 @@ def convert_to_1bpp(filenames=[]):
def convert_to_png(filenames=[]):
for filename in filenames:
- name, extension = os.path.splitext(filename)
+ filename, name, extension = try_decompress(filename)
if extension == '.1bpp':
export_1bpp_to_png(filename)
elif extension == '.2bpp':
@@ -1836,6 +1881,19 @@ def decompress(filenames=[]):
data = Decompressed(lz_data).output
to_file(name, data)
+def try_decompress(filename):
+ """
+ Try to decompress a graphic when determining the filetype.
+ This skips the manual unlz step when attempting
+ to convert lz-compressed graphics to png.
+ """
+ name, extension = os.path.splitext(filename)
+ if extension == '.lz':
+ decompress([filename])
+ filename = name
+ name, extension = os.path.splitext(filename)
+ return filename, name, extension
+
def main():
ap = argparse.ArgumentParser()
diff --git a/pokemontools/pic.py b/pokemontools/pic.py
new file mode 100644
index 0000000..34e88f5
--- /dev/null
+++ b/pokemontools/pic.py
@@ -0,0 +1,488 @@
+# coding: utf-8
+
+"""
+A library for use with compressed monster and trainer pics in pokered.
+"""
+
+import os
+import sys
+import argparse
+from math import sqrt
+
+from gfx import transpose_tiles
+
+
+def bitflip(x, n):
+ r = 0
+ while n:
+ r = (r << 1) | (x & 1)
+ x >>= 1
+ n -= 1
+ return r
+
+
+class Decompressor:
+ """
+ pokered pic decompression.
+
+ Ported to python 2.7 from the python 3 code at https://github.com/magical/pokemon-sprites-rby.
+ """
+
+ table1 = [(2 << i) - 1 for i in xrange(16)]
+ table2 = [
+ [0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5, 0xf, 0xe, 0xc, 0xd, 0x8, 0x9, 0xb, 0xa],
+ [0xf, 0xe, 0xc, 0xd, 0x8, 0x9, 0xb, 0xa, 0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5], # prev ^ 0xf
+ [0x0, 0x8, 0xc, 0x4, 0xe, 0x6, 0x2, 0xa, 0xf, 0x7, 0x3, 0xb, 0x1, 0x9, 0xd, 0x5],
+ [0xf, 0x7, 0x3, 0xb, 0x1, 0x9, 0xd, 0x5, 0x0, 0x8, 0xc, 0x4, 0xe, 0x6, 0x2, 0xa], # prev ^ 0xf
+ ]
+ table3 = [bitflip(i, 4) for i in xrange(16)]
+
+ tilesize = 8
+
+
+ def __init__(self, f, mirror=False, planar=True):
+ self.bs = fbitstream(f)
+ self.mirror = mirror
+ self.planar = planar
+ self.data = None
+
+ def decompress(self):
+ rams = [[], []]
+
+ self.sizex = self._readint(4) * self.tilesize
+ self.sizey = self._readint(4)
+
+ self.size = self.sizex * self.sizey
+
+ self.ramorder = self._readbit()
+
+ r1 = self.ramorder
+ r2 = self.ramorder ^ 1
+
+ self._fillram(rams[r1])
+ mode = self._readbit()
+ if mode:
+ mode += self._readbit()
+ self._fillram(rams[r2])
+
+ rams[0] = bytearray(bitgroups_to_bytes(rams[0]))
+ rams[1] = bytearray(bitgroups_to_bytes(rams[1]))
+
+ if mode == 0:
+ self._decode(rams[0])
+ self._decode(rams[1])
+ elif mode == 1:
+ self._decode(rams[r1])
+ self._xor(rams[r1], rams[r2])
+ elif mode == 2:
+ self._decode(rams[r2], mirror=False)
+ self._decode(rams[r1])
+ self._xor(rams[r1], rams[r2])
+ else:
+ raise Exception, "Invalid deinterlace mode!"
+
+ data = []
+ if self.planar:
+ for a, b in zip(rams[0], rams[1]):
+ data += [a, b]
+ self.data = bytearray(data)
+ else:
+ for a, b in zip(bitstream(rams[0]), bitstream(rams[1])):
+ data.append(a | (b << 1))
+ self.data = bitgroups_to_bytes(data)
+
+ def _fillram(self, ram):
+ mode = ['rle', 'data'][self._readbit()]
+ size = self.size * 4
+ while len(ram) < size:
+ if mode == 'rle':
+ self._read_rle_chunk(ram)
+ mode = 'data'
+ elif mode == 'data':
+ self._read_data_chunk(ram, size)
+ mode = 'rle'
+ if len(ram) > size:
+ #ram = ram[:size]
+ raise ValueError(size, len(ram))
+
+ ram[:] = self._deinterlace_bitgroups(ram)
+
+ def _read_rle_chunk(self, ram):
+
+ i = 0
+ while self._readbit():
+ i += 1
+
+ n = self.table1[i]
+ a = self._readint(i + 1)
+ n += a
+
+ for i in xrange(n):
+ ram.append(0)
+
+ def _read_data_chunk(self, ram, size):
+ while 1:
+ bitgroup = self._readint(2)
+ if bitgroup == 0:
+ break
+ ram.append(bitgroup)
+
+ if size <= len(ram):
+ break
+
+ def _decode(self, ram, mirror=None):
+ if mirror is None:
+ mirror = self.mirror
+
+ for x in xrange(self.sizex):
+ bit = 0
+ for y in xrange(self.sizey):
+ i = y * self.sizex + x
+ a = (ram[i] >> 4) & 0xf
+ b = ram[i] & 0xf
+
+ a = self.table2[bit][a]
+ bit = a & 1
+ if mirror:
+ a = self.table3[a]
+
+ b = self.table2[bit][b]
+ bit = b & 1
+ if mirror:
+ b = self.table3[b]
+
+ ram[i] = (a << 4) | b
+
+ def _xor(self, ram1, ram2, mirror=None):
+ if mirror is None:
+ mirror = self.mirror
+
+ for i in xrange(len(ram2)):
+ if mirror:
+ a = (ram2[i] >> 4) & 0xf
+ b = ram2[i] & 0xf
+ a = self.table3[a]
+ b = self.table3[b]
+ ram2[i] = (a << 4) | b
+
+ ram2[i] ^= ram1[i]
+
+ def _deinterlace_bitgroups(self, bits):
+ l = []
+ for y in xrange(self.sizey):
+ for x in xrange(self.sizex):
+ i = 4 * y * self.sizex + x
+ for j in xrange(4):
+ l.append(bits[i])
+ i += self.sizex
+ return l
+
+
+ def _readbit(self):
+ return next(self.bs)
+
+ def _readint(self, count):
+ return readint(self.bs, count)
+
+
+def fbitstream(f):
+ while 1:
+ char = f.read(1)
+ if not char:
+ break
+ byte = ord(char)
+
+ for i in xrange(7, -1, -1):
+ yield (byte >> i) & 1
+
+def bitstream(b):
+ for byte in b:
+ for i in xrange(7, -1, -1):
+ yield (byte >> i) & 1
+
+def readint(bs, count):
+ n = 0
+ while count:
+ n <<= 1
+ n |= next(bs)
+ count -= 1
+ return n
+
+def bitgroups_to_bytes(bits):
+ l = []
+ for i in xrange(0, len(bits) - 3, 4):
+ n = ((bits[i + 0] << 6)
+ | (bits[i + 1] << 4)
+ | (bits[i + 2] << 2)
+ | (bits[i + 3] << 0))
+ l.append(n)
+ return bytearray(l)
+
+
+def bytes_to_bits(bytelist):
+ return list(bitstream(bytelist))
+
+
+class Compressor:
+ """
+ pokered pic compression.
+
+ Adapted from stag019's C compressor.
+ """
+
+ table1 = [(2 << i) - 1 for i in xrange(16)]
+ table2 = [
+ [0x0, 0x1, 0x3, 0x2, 0x6, 0x7, 0x5, 0x4, 0xc, 0xd, 0xf, 0xe, 0xa, 0xb, 0x9, 0x8],
+ [0x8, 0x9, 0xb, 0xa, 0xe, 0xf, 0xd, 0xc, 0x4, 0x5, 0x7, 0x6, 0x2, 0x3, 0x1, 0x0], # reverse
+ ]
+ table3 = [bitflip(i, 4) for i in xrange(16)]
+
+ def __init__(self, image, width=None, height=None):
+ self.image = bytearray(image)
+ self.size = len(self.image)
+
+ planar_tile = 8 * 8 / 4
+ tile_size = self.size / planar_tile
+ if height and not width: width = tile_size / height
+ elif width and not height: height = tile_size / width
+ elif not width and not height: width = height = int(sqrt(tile_size))
+ self.width, self.height = width, height
+
+ def compress(self):
+ """
+ Compress the image five times (twice for each mode, except 0)
+ and use the smallest one (in bits).
+ """
+ rams = [[],[]]
+ datas = []
+
+ for mode in xrange(3):
+
+ # Order is redundant for mode 0.
+
+ # While this seems like an optimization,
+ # it's actually required for 1:1 compression
+ # to the original compressed pics.
+
+ # This appears to be the algorithm
+ # that Game Freak's compressor used.
+
+ # Using order 0 instead of 1 breaks this feature.
+
+ for order in xrange(2):
+ if mode == 0 and order == 0:
+ continue
+ for i in xrange(2):
+ rams[i] = self.image[i::2]
+ self._interpret_compress(rams, mode, order)
+ datas += [(self.data[:], int(self.which_bit))]
+
+ # Pick the smallest pic, measured in bits.
+ datas = sorted(datas, key=lambda (data, bit): (len(data), -bit))
+ self.data, self.which_bit = datas[0]
+
+ def _interpret_compress(self, rams, mode, order):
+ self.data = []
+ self.which_bit = 0
+
+ r1 = order
+ r2 = order ^ 1
+
+ if mode == 0:
+ self._encode(rams[1])
+ self._encode(rams[0])
+ elif mode == 1:
+ self._xor(rams[r1], rams[r2])
+ self._encode(rams[r1])
+ elif mode == 2:
+ self._xor(rams[r1], rams[r2])
+ self._encode(rams[r1])
+ self._encode(rams[r2], mirror=False)
+ else:
+ raise Exception, 'invalid interlace mode!'
+
+ self._writeint(self.height, 4)
+ self._writeint(self.width, 4)
+
+ self._writebit(order)
+
+ self._fillram(rams[r1])
+ if mode == 0:
+ self._writebit(0)
+ else:
+ self._writebit(1)
+ self._writebit(mode - 1)
+ self._fillram(rams[r2])
+
+ def _fillram(self, ram):
+ rle = 0
+ nums = 0
+ bitgroups = []
+
+ for x in xrange(self.width):
+ for bit in xrange(0, 8, 2):
+ byte = x * self.height * 8
+ for y in xrange(self.height * 8):
+ bitgroup = (ram[byte] >> (6 - bit)) & 3
+ if bitgroup == 0:
+ if rle == 0:
+ self._writebit(0)
+ elif rle == 1:
+ nums += 1
+ else:
+ self._data_packet(bitgroups)
+ self._writebit(0)
+ self._writebit(0)
+ rle = 1
+ bitgroups = []
+ else:
+ if rle == 0:
+ self._writebit(1)
+ elif rle == 1:
+ self._rle(nums)
+ rle = -1
+ bitgroups += [bitgroup]
+ nums = 0
+ byte += 1
+
+ if rle == 1:
+ self._rle(nums)
+ else:
+ self._data_packet(bitgroups)
+
+ def _data_packet(self, bitgroups):
+ for bitgroup in bitgroups:
+ self._writebit((bitgroup >> 1) & 1)
+ self._writebit((bitgroup >> 0) & 1)
+
+ def _rle(self, nums):
+ nums += 1
+
+ # Get the previous power of 2.
+ # Deriving the bitcount from that seems to be
+ # faster on average than using the lookup table.
+ v = nums
+ v += 1
+ v |= v >> 1
+ v |= v >> 2
+ v |= v >> 4
+ v |= v >> 8
+ v |= v >> 16
+ v -= v >> 1
+ v -= 1
+ number = nums - v
+
+ bitcount = -1
+ while v:
+ v >>= 1
+ bitcount += 1
+
+ for j in xrange(bitcount):
+ self._writebit(1)
+ self._writebit(0)
+ for j in xrange(bitcount, -1, -1):
+ self._writebit((number >> j) & 1)
+
+ def _encode(self, ram, mirror=None):
+ a = b = 0
+ for i in xrange(len(ram)):
+ j = i / self.height
+ j += i % self.height * self.width * 8
+ if i % self.height == 0:
+ b = 0
+
+ a = (ram[j] >> 4) & 0xf
+ table = b & 1
+ code_1 = self.table2[table][a]
+
+ b = ram[j] & 0xf
+ table = a & 1
+ code_2 = self.table2[table][b]
+
+ ram[j] = (code_1 << 4) | code_2
+
+ def _xor(self, ram1, ram2):
+ for i in xrange(len(ram2)):
+ ram2[i] ^= ram1[i]
+
+ def _writebit(self, bit):
+ self.which_bit -= 1
+ if self.which_bit == -1:
+ self.which_bit = 7
+ self.data += [0]
+ if bit: self.data[-1] |= bit << self.which_bit
+
+ def _writeint(self, num, size=None):
+ bits = []
+ if size:
+ for i in xrange(size):
+ bits += [num & 1]
+ num >>= 1
+ else:
+ while num > 0:
+ bits += [num & 1]
+ num >>= 1
+ for bit in reversed(bits):
+ self._writebit(bit)
+
+
+def decompress(f, offset=None, mirror=False):
+ """
+ Decompress a pic given a file object. Return a planar 2bpp image.
+
+ Optional: offset (for roms).
+ """
+ if offset is not None:
+ f.seek(offset)
+ dcmp = Decompressor(f, mirror=mirror)
+ dcmp.decompress()
+ return dcmp.data
+
+
+def compress(f):
+ """
+ Compress a planar 2bpp into a pic.
+ """
+ comp = Compressor(f)
+ comp.compress()
+ return comp.data
+
+
+def decompress_file(filename):
+ """
+ Decompress a pic given a filename.
+ Export the resulting planar 2bpp image to
+ """
+ pic = open(filename, 'rb')
+ image = decompress(pic)
+ image = transpose_tiles(image)
+ image = bytearray(image)
+ output_filename = os.path.splitext(filename)[0] + '.2bpp'
+ with open(output_filename, 'wb') as out:
+ out.write(image)
+
+def compress_file(filename):
+ image = open(filename, 'rb').read()
+ image = transpose_tiles(image)
+ pic = compress(image)
+ pic = bytearray(pic)
+ output_filename = os.path.splitext(filename)[0] + '.pic'
+ with open(output_filename, 'wb') as out:
+ out.write(pic)
+
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('mode')
+ ap.add_argument('filenames', nargs='*')
+ args = ap.parse_args()
+
+ for filename in args.filenames:
+ if args.mode == 'decompress':
+ decompress_file(filename)
+ elif args.mode == 'compress':
+ compress_file(filename)
+
+if __name__ == '__main__':
+ main()
+