Skip to content

Instantly share code, notes, and snippets.

@sethmlarson
Created December 31, 2025 18:05
Show Gist options
  • Select an option

  • Save sethmlarson/92dabc8420b0d2e2e6e8c688269fe5fd to your computer and use it in GitHub Desktop.

Select an option

Save sethmlarson/92dabc8420b0d2e2e6e8c688269fe5fd to your computer and use it in GitHub Desktop.
Script for cutting spritesheets like cookies using Python and Pillow
#!/usr/bin/env python
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "Pillow",
# "tqdm"
# ]
# ///
# License: MIT
# Copyright 2025, Seth Larson
import os.path
import math
from PIL import Image
import tqdm
# Parameters
spritesheet = "" # Path to spritesheet.
masks = {} # Set of 3-tuples for RGB.
min_dim = 10 # Min and max dimensions in pixels.
max_dim = 260
img = Image.open(spritesheet)
if img.mode == "RGB": # Ensure an alpha channel.
alpha = Image.new("L", img.size, 255)
img.putalpha(alpha)
output_prefix = os.path.splitext(os.path.basename(spritesheet))[0]
data = img.getdata()
visited = set()
shapes = set()
reroll_shapes = set()
def getpixel(x, y) -> tuple[int, int, int, int]:
return data[x + (img.width * y)]
def make_2n(value: int) -> int:
return 2 ** int(math.ceil(math.log2(value)))
with tqdm.tqdm(
desc="Cutting cookies",
total=int(img.width * img.height),
unit="pixels",
) as t:
for x in range(img.width):
for y in range(img.height):
xy = (x, y)
if xy in visited:
continue
inshape = set()
candidates = {(x, y)}
def add_candidates(cx, cy):
global candidates
candidates |= {(cx - 1, cy), (cx + 1, cy), (cx, cy - 1), (cx, cy + 1)}
while candidates:
cx, cy = candidates.pop()
if (
(cx, cy) in visited
or cx < 0
or cx >= img.width
or cy < 0
or cy >= img.height
or abs(cx - x) > max_dim
or abs(cy - y) > max_dim
):
continue
visited.add((cx, cy))
rgba = r, g, b, a = getpixel(cx, cy)
if a == 0 or (r, g, b) in masks:
continue
else:
inshape.add((cx, cy))
add_candidates(cx, cy)
if inshape:
shapes.add(tuple(inshape))
t.update(img.height)
max_width = 0
max_height = 0
shapes_and_offsets = []
for shape in sorted(shapes):
min_x = img.width + 2
min_y = img.height + 2
max_x = -1
max_y = -1
for x, y in shape:
max_x = max(x, max_x)
max_y = max(y, max_y)
min_x = min(x, min_x)
min_y = min(y, min_y)
width = max_x - min_x + 1
height = max_y - min_y + 1
# Too small! We have to reroll this
# potentially into another shape.
if width < min_dim or height < min_dim:
reroll_shapes.add(shape)
continue
max_width = max(max_width, width)
max_height = max(max_height, height)
shapes_and_offsets.append((shape, (width, height), (min_x, min_y)))
# Make them powers of two!
max_width = make_2n(max_width)
max_height = make_2n(max_height)
sprite_number = 0
with tqdm.tqdm(
desc="Baking cookies",
total=len(shapes_and_offsets),
unit="sprites"
) as t:
for shape, (width, height), (offset_x, offset_y) in shapes_and_offsets:
new_img = Image.new(mode="RGBA", size=(max_width, max_height))
margin_x = (max_width - width) // 2
margin_y = (max_height - height) // 2
for rx in range(max_width):
for ry in range(max_height):
x = rx + offset_x
y = ry + offset_y
if (x, y) not in shape:
continue
new_img.putpixel((rx + margin_x, ry + margin_y), getpixel(x, y))
new_img.save(f"images/{output_prefix}-{sprite_number}.png")
sprite_number += 1
t.update(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment