An AI image that looks like a GBA sprite is not automatically a sprite you can ship. The preview may be high resolution, the background may be opaque, antialiasing may introduce hundreds of colors, and frames may not share a consistent origin.
This tutorial turns those hidden assumptions into a deterministic build check. The public Angel 624 GBA-style generator page on Voor AI supplies a real visual fixture; the code below validates the exported asset you intend to commit.
Define the asset contract
For one idle sprite, a compact contract might be:
{
"cell_width": 32,
"cell_height": 32,
"frames": 1,
"max_colors": 16,
"require_alpha": true,
"anchor": "bottom-center"
}
Do not write “GBA style” and leave the rest implicit. Dimensions, grid, palette, alpha, and anchor are build properties, not aesthetic preferences.
Validate dimensions and frame count
from pathlib import Path
from PIL import Image
CELL_W = CELL_H = 32
EXPECTED_FRAMES = 1
im = Image.open(Path("angel-idle.png")).convert("RGBA")
w, h = im.size
assert h == CELL_H, f"height {h} != {CELL_H}"
assert w % CELL_W == 0, f"width {w} is not a {CELL_W}px grid"
assert w // CELL_W == EXPECTED_FRAMES, "unexpected frame count"
If your generator gives you a large concept sheet, this assertion should fail. That is correct: downsample with nearest-neighbor, redraw, and export an exact cell sheet before it enters the build.
Reject blended colors and accidental opacity
pixels = list(im.getdata())
opaque_rgb = {tuple(rgb) for (*rgb, a) in pixels if a > 0}
alpha_values = {a for (*_, a) in pixels}
assert len(opaque_rgb) <= 16, f"palette grew to {len(opaque_rgb)} colors"
assert 0 in alpha_values, "no transparent background found"
assert alpha_values <= {0, 255}, "partial alpha indicates antialiasing"
This is intentionally strict. If your engine allows partial alpha, change the policy explicitly instead of accepting it accidentally.
Check a shared bottom-center anchor
For each frame, find non-transparent pixels and compare the lowest occupied row plus horizontal center. A one-pixel vertical jump between idle frames becomes a visible bounce in-game.
def bounds(frame):
alpha = frame.getchannel("A")
return alpha.getbbox()
anchors = []
for i in range(EXPECTED_FRAMES):
f = im.crop((i*CELL_W, 0, (i+1)*CELL_W, CELL_H))
box = bounds(f)
assert box, f"frame {i} is empty"
left, top, right, bottom = box
anchors.append(((left + right - 1) / 2, bottom - 1))
assert len(set(anchors)) == 1, f"anchor drift: {anchors}"
Preserve source and model state in the fixture note
The current page shows GPT Image 2, Text to image, 4:3, Public watermarked visibility, and 45 credits. Those are current UI facts, not permanent pricing. The public example is a visual starting point, not a claim that it already meets your 32×32 build contract.
Your CI artifact should report dimensions, frame count, palette size, alpha policy, anchors, source URL, and the checksum of the exact exported PNG. That makes failures reproducible instead of subjective.
Use the public example as the visual fixture, then let the validator—not the zoomed preview—decide whether the asset is ready for your repository.


Top comments (0)