Skip to content

Commit

Permalink
Major cleanup; use pypng to skip the ppm stage
Browse files Browse the repository at this point in the history
  • Loading branch information
CatTrinket committed Apr 26, 2012
1 parent 9fd47e9 commit b5beaf5
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 87 deletions.
254 changes: 169 additions & 85 deletions kaomado.py
Expand Up @@ -6,123 +6,207 @@
multilingual one) the file can be found at /FONT/kaomado.kao.
"kaomado" means "face window", as far as I can tell.
n.b. "left" and "right" are consistently used from the perspective of the
Pokémon in the portrait. This also means that the "right" sprite is the one
that appears on the right side of the screen in-game.
"""

import os
import png
from struct import unpack
from sys import argv

from tables import expressions, pokemon_ids, Pokemon

if len(argv) != 3:
print("Usage: kaomado.py /path/to/kaomado.kao output-dir")
exit(1)
def build_filename(pokemon, sprite_num):
"""Determine an output filename for a sprite given the Pokémon it
depicts and its index in that Pokémon's list of sprite pointers.
"""

kaomado = open(argv[1], 'br')
# Start with the main output directory
filename = [argv[2]]

for pokemon in range(1155):
kaomado.seek(0xa0 * pokemon)
# Stick various junk sprites in other/ for potential debug purposes
if pokemon.species == 'other':
filename.append('other')

try:
pokemon = pokemon_ids[pokemon]
except KeyError:
pokemon = Pokemon(pokemon, 'other', None, False)
if pokemon.female:
filename.append('female')

pointers = unpack('<40L', kaomado.read(0xa0))
# Sprite pointers come in pairs of two: facing left and (sometimes) right
# for a given facial expression.
expression = expressions[sprite_num // 2]
if expression != 'standard':
filename.append(expression)

for n, pointer in enumerate(pointers):
if not 0x2d1e0 <= pointer <= 0x1968c0:
# Junk
continue
if sprite_num % 2 != 0:
filename.append('right')

kaomado.seek(pointer)
# Figure out the base filename
if pokemon.form is not None:
filename.append('{0}-{1}.png'.format(pokemon.national_id, pokemon.form))
else:
filename.append('{0}.png'.format(pokemon.national_id))

return os.path.join(*filename)

def decompress(kaomado):
"""Decompress a sprite at the current offset."""

palette = []
for colour in range(0x10):
palette.append(tuple(channel >> 3 for channel in kaomado.read(3)))
if kaomado.read(5) != b'AT4PX':
raise ValueError('wrong magic bytes')

kaomado.seek(0x7, 1) # AT4PX + compressed length
controls = list(kaomado.read(9))
kaomado.seek(2, 1) # Little-endian uncompressed length (always 0x320)
kaomado.seek(2, 1) # Skip the compressed length since we don't use it

sprite = bytearray()
# The control codes used for 0-flags vary from sprite to sprite
controls = list(kaomado.read(9))

while len(sprite) < 0x320:
flags, = kaomado.read(1)
for flag in range(8):
if len(sprite) == 0x320:
break
if flags & 0x80 >> flag:
sprite += kaomado.read(1)
length, = unpack('<H', kaomado.read(2))

data = bytearray()
while len(data) < length:
flags, = kaomado.read(1)
for flag in range(8):
if flags & (0x80 >> flag):
# Flag 1: append one byte as-is
data.extend(kaomado.read(1))
else:
# Flag 0: do one of two fancy things based on the next byte's
# high and low nybbles
control, = kaomado.read(1)
high, low = control >> 4, control & 0xf

if high in controls:
# Append a pattern of four pixels. The high bits determine
# the pattern, and the low bits determine the base pixel.
control = controls.index(high)
pixels = [low] * 4

if control == 0:
pass
elif 1 <= control <= 4:
# Lower a particular pixel
if control == 1:
pixels = [low + 1] * 4
pixels[control - 1] -= 1
else:
# 5 <= control <= 8; raise a particular pixel
if control == 5:
pixels = [low - 1] * 4
pixels[control - 5] += 1

# Pack the pixels into bytes and append them
data.extend((pixels[0] << 4 | pixels[1],
pixels[2] << 4 | pixels[3]))
else:
control, = kaomado.read(1)
high, low = control >> 4, control & 0xf
# Append a sequence of bytes previously used in the sprite.
# This can overlap with the beginning of the appended bytes!
# The high bits determine the length of the sequence, and
# the low bits help determine the where the sequence starts.
offset = -0x1000
offset += (low << 8) | kaomado.read(1)[0]

if high in controls:
pixels = [low] * 4 # 2 1 4 3
control = controls.index(high)
for b in range(high + 3):
data.append(data[offset])

if 1 <= control <= 4:
if control == 1:
pixels = [low + 1] * 4
if len(data) == length:
break

pixels[control - 1] -= 1
elif 5 <= control <= 8:
if control == 5:
pixels = [low - 1] * 4
pixels[control - 5] += 1
return data

sprite.extend((pixels[0] << 4 | pixels[1],
pixels[2] << 4 | pixels[3]))
else:
offset, = kaomado.read(1)
offset -= 0x100 * (0x10 - low)
def makedirs_if_need_be(filename):
"""Create directories as needed to house the given filename, but don't
break if it turns out no directories actually need creating.
"""

try:
os.makedirs(os.path.dirname(filename))
except OSError as e:
if e.errno == 17:
# Leaf directory already exists
pass
else:
raise e

for b in range(high + 3):
sprite.append(sprite[offset])
def parse_palette(kaomado):
"""Parse an RGB palette at the current offset: sixteen colors, five bits
per channel.
filename = [argv[2]]
Each channel gets its own byte, for some reason, as opposed to the usual
NTFP color format which actually fits all three into fifteen bits.
"""

if pokemon.species == 'other':
filename.append('other')
palette = []
for color in range(0x10):
palette.append(tuple(
channel >> 3 for channel in kaomado.read(3)
))

if pokemon.female:
filename.append('female')
return palette

if n // 2 != 0:
filename.append(expressions[n // 2])
def unscramble(sprite, palette):
"""Unscramble the raw sprite data into something pypng can swallow."""

if n % 2 != 0:
filename.append('right') # Their right, not ours
# Step 1: unpack each byte into its two pixels
pixels = []
for pixel_pair in sprite:
pixels.extend((pixel_pair & 0xf, pixel_pair >> 4))

if pokemon.form is not None:
filename.append('{0}-{1}.ppm'.format(
pokemon.national_id, pokemon.form))
else:
filename.append('{0}.ppm'.format(pokemon.national_id))

filename = os.path.join(*filename)
try:
os.makedirs(os.path.dirname(filename))
except OSError as e:
if e.errno == 17:
# Leaf already exists
pass
else:
raise e
# Step 2: untile & Step 3: replace palette indices with RGB channels
# (At the time of writing, pypng's image[y][x][channel] thing doesn't work)
image = [[None for x_channel in range(120)] for y in range(40)]

output = open(filename, 'w')
for tile in range(25):
tile_x = tile % 5
tile_y = tile // 5

output.write('P3\n40 40\n31\n')
for pixel in range(64):
pixel_x = pixel % 8
pixel_y = pixel // 8

x = (tile_x * 8 + pixel_x) * 3
y = tile_y * 8 + pixel_y

image[y][x:x + 3] = palette[pixels.pop(0)]

return image


if len(argv) != 3:
print("Usage: kaomado.py /path/to/kaomado.kao output-dir")
exit(1)

kaomado = open(argv[1], 'br')

for pokemon in range(1, 1155):
kaomado.seek(0xa0 * pokemon)

try:
pokemon = pokemon_ids[pokemon]
except KeyError:
pokemon = Pokemon(pokemon, 'other', None, False)

# Each Pokémon has a sprite pointer for each facial expression and
# direction, if they're not all used
pointers = unpack('<40L', kaomado.read(0xa0))

for sprite_num, pointer in enumerate(pointers):
if not 0x2d1e0 <= pointer <= 0x1968c0:
# Nonexistent sprites have consistent junk pointers, thankfully
continue

kaomado.seek(pointer)

for y in range(40):
for x in range(0, 40, 2):
# Tile rows before this + row tiles before this + rows + in-row
pair = sprite[160 * (y // 8) + 32 * (x // 8) +
4 * (y % 8) + x % 8 // 2]
output.write(''.join('{0} {1} {2} '.format(*palette[pixel]) for
pixel in (pair & 0xf, pair >> 4)))
# Get the palette
palette = parse_palette(kaomado)

output.write('\n')
# Extract the actual sprite
sprite = decompress(kaomado)
sprite = unscramble(sprite, palette)

output.close()
# Save it as a PNG
sprite = png.from_array(sprite, mode='RGB;5')
filename = build_filename(pokemon, sprite_num)
makedirs_if_need_be(filename)
sprite.save(filename)
4 changes: 2 additions & 2 deletions rip.sh
Expand Up @@ -14,8 +14,8 @@ for form in 201-a 386-attack 412-plant 413-plant 421-overcast 422-west \
423-west 487-altered 492-land
do
id=$(echo $form | sed 's/-.*//') # dash has no <<<
for file in $(find $2 -name $form.ppm)
for file in $(find $2 -name $form.png)
do
cp $file $(dirname $file)/$id.ppm
cp $file $(dirname $file)/$id.png
done
done

0 comments on commit b5beaf5

Please sign in to comment.