Last active
May 21, 2025 04:59
-
-
Save redraw/ff06fe6ffa14e3b8d478d56a0cbaa84d to your computer and use it in GitHub Desktop.
c64 img to sprite converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env -S uv run -s | |
# /// script | |
# requires-python = ">=3.12" | |
# dependencies = [ | |
# "pillow", | |
# ] | |
# /// | |
import argparse | |
import struct | |
from pathlib import Path | |
from PIL import Image | |
def parse_args(): | |
parser = argparse.ArgumentParser( | |
description="Convert image to .bin or .spd format (24x21 multiples)." | |
) | |
parser.add_argument("input_image", type=Path, help="Input image file") | |
parser.add_argument("output_file", type=Path, nargs="?", help="Output file path") | |
parser.add_argument( | |
"-a", | |
"--address", | |
type=lambda x: int(x, 16), | |
help="Load address in hex", | |
) | |
parser.add_argument( | |
"-f", | |
"--format", | |
choices=["bin", "spd"], | |
default="bin", | |
help="Output format", | |
) | |
parser.add_argument( | |
"-i", | |
"--invert", | |
action="store_true", | |
help="Invert image colors", | |
) | |
return parser.parse_args() | |
def validate_image_size(img): | |
width, height = img.size | |
if width % 24 != 0 or height % 21 != 0: | |
raise ValueError(f"Image size {img.size} is not a multiple of 24x21") | |
def extract_sprites(img): | |
width, height = img.size | |
for y in range(0, height, 21): | |
for x in range(0, width, 24): | |
yield img.crop((x, y, x + 24, y + 21)).tobytes() | |
def build_bin_data(img, address): | |
data = bytearray() | |
if address is not None: | |
data += struct.pack("<H", address) | |
for sprite_bytes in extract_sprites(img): | |
data += sprite_bytes | |
return data | |
def build_spd_data(img): | |
""" | |
SPD file format information | |
bytes 00,01,02 = "SPD" | |
byte 03 = version number of spritepad | |
byte 04 = number of sprites | |
byte 05 = number of animations | |
byte 06 = color transparent | |
byte 07 = color multicolor 1 | |
byte 08 = color multicolor 2 | |
byte 09 = start of sprite data | |
byte 73 = 0-3 color, 4 overlay, 7 multicolor/singlecolor | |
bytes xx = "00", "00", "01", "00" added at the end of file (SpritePad animation info) | |
""" | |
num_sprites = (img.width // 24) * (img.height // 21) | |
data = bytearray(b"SPD\x00") # Header with version 0 | |
data += struct.pack("<B", num_sprites) # Number of sprites | |
data += struct.pack("<B", 0) # Number of animations (0) | |
# Default colors for SpritePad | |
data += struct.pack("<B", 0) # Transparent color (black) | |
data += struct.pack("<B", 1) # Multicolor 1 (white) | |
data += struct.pack("<B", 2) # Multicolor 2 (red) | |
# Add sprite data | |
for sprite_bytes in extract_sprites(img): | |
data += sprite_bytes | |
# Add sprite attributes for each sprite (at byte 73 of each sprite) | |
# 0-3: color (using 1 = white) | |
# 4: overlay (0 = no) | |
# 7: multicolor (0 = no) | |
data += struct.pack("<B", 1) # White color, no overlay, single color mode | |
# Add default animation info | |
data += struct.pack("<BBBB", 0, 0, 1, 0) | |
return data | |
def main(): | |
args = parse_args() | |
img = Image.open(args.input_image).convert("1") | |
if args.invert: | |
img = img.point(lambda p: 255 - p) | |
validate_image_size(img) | |
if args.format == "bin": | |
data = build_bin_data(img, args.address) | |
else: | |
data = build_spd_data(img) | |
output_path = args.output_file or args.input_image.with_suffix(f".{args.format}") | |
with output_path.open("wb") as f: | |
f.write(data) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment