Skip to content

Instantly share code, notes, and snippets.

@redraw
Last active May 21, 2025 04:59
Show Gist options
  • Save redraw/ff06fe6ffa14e3b8d478d56a0cbaa84d to your computer and use it in GitHub Desktop.
Save redraw/ff06fe6ffa14e3b8d478d56a0cbaa84d to your computer and use it in GitHub Desktop.
c64 img to sprite converter
#!/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