Files
game-and-watch-patch/patches/tileset.py

146 lines
3.7 KiB
Python

from math import ceil
import numpy as np
from PIL import Image
_BLOCK_SIZE = 16
_BLOCK_PIXEL = _BLOCK_SIZE * _BLOCK_SIZE
PALETTE_OFFSETS = [
0xB_EC68,
0xB_EDA8,
0xB_EEE8,
0xB_F028,
0xB_F168,
]
def bytes_to_tilemap(data, palette=None, bpp=8, width=256):
"""
Parameters
----------
palette : bytes
320 long RGBA (80 colors). Alpha is ignored.
Returns
-------
PIL.Image
Rendered RGB image.
"""
# assert bpp in [4, 8]
if bpp < 8:
nibbles = bytearray()
# offset = 0x0
for b in data:
shift = 8 - bpp
while shift >= 0:
nibbles.append(b >> shift & (2 ** bpp - 1))
shift -= bpp
# nibbles.append((b >> 4) | (offset << 4))
# nibbles.append((b & 0xF) | (offset << 4))
data = bytes(nibbles)
del nibbles
# Assemble bytes into an index-image
h, w = int(ceil(len(data) / width / _BLOCK_SIZE) * _BLOCK_SIZE), width
canvas = np.zeros((h, w), dtype=np.uint8)
i_sprite = 0
for i in range(0, len(data), _BLOCK_PIXEL):
sprite = data[i : i + _BLOCK_PIXEL]
x = i_sprite * _BLOCK_SIZE % w
y = _BLOCK_SIZE * (i_sprite * _BLOCK_SIZE // w)
view = canvas[y : y + _BLOCK_SIZE, x : x + _BLOCK_SIZE]
sprite_block = np.frombuffer(sprite, dtype=np.uint8).reshape(
_BLOCK_SIZE, _BLOCK_SIZE
)
view[:] = sprite_block
i_sprite += 1
if palette is None:
return Image.fromarray(canvas, "L")
# Apply palette to index-image
p = np.frombuffer(palette, dtype=np.uint8).reshape((80, 4))
p = p[:, :3]
p = np.fliplr(p) # BGR->RGB
im = Image.fromarray(canvas, "P")
im.putpalette(p)
return im
def rgb_to_index(tilemap, palette):
if isinstance(tilemap, Image.Image):
tilemap = tilemap.convert("RGB")
tilemap = np.array(tilemap)
elif isinstance(tilemap, np.ndarray):
pass
else:
raise TypeError(f"Don't know how to handle tilemap type {type(tilemap)}")
# Convert rgb tilemap to index image
p = np.frombuffer(palette, dtype=np.uint8).reshape((80, 4))
p = p[:, :3]
p = np.fliplr(p) # BGR->RGB
p = p[None, None].transpose(0, 1, 3, 2) # (1, 1, 3, 80)
# Find closest color
diff = tilemap[..., None] - p
dist = np.linalg.norm(diff, axis=2)
tilemap = np.argmin(dist, axis=-1).astype(np.uint8)
return tilemap
def tilemap_to_bytes(tilemap, palette=None, bpp=8):
"""
Parameters
----------
tilemap : PIL.Image.Image or numpy.ndarray
RGB data
palette : bytes
320 long RGBA (80 colors). Alpha is ignored.
Returns
-------
bytes
Bytes representation of index image
"""
if isinstance(tilemap, Image.Image):
tilemap = tilemap.convert("RGB")
tilemap = np.array(tilemap)
elif isinstance(tilemap, np.ndarray):
pass
else:
raise TypeError(f"Don't know how to handle tilemap type {type(tilemap)}")
if palette is not None:
tilemap = rgb_to_index(tilemap, palette)
# Need to undo the tiling now.
out = []
for i in range(0, tilemap.shape[0], _BLOCK_SIZE):
for j in range(0, tilemap.shape[1], _BLOCK_SIZE):
sprite = tilemap[i : i + _BLOCK_SIZE, j : j + _BLOCK_SIZE]
sprite_bytes = sprite.tobytes()
out.append(sprite_bytes)
out = b"".join(out)
if bpp == 4:
out_packed = bytearray()
assert len(out) % 2 == 0
for i in range(0, len(out), 2):
b1, b2 = out[i], out[i + 1]
b1 &= 0xF
b2 &= 0xF
out_packed.append((b1 << 4) | b2)
out = bytes(out_packed)
return out