#!/usr/bin/env python3
"""Generate a sprite sheet from grid-named images.
脚本,用于生成雪碧图
python img_sprite.py -i /document/images -o /document/output/sprite.png -s 0
images里面的图片命名是这样的,检查如果图片首尾不是以"数字_" 和 “_数字”结尾则报错:
1_home_1.png: 代表第一行的第一个图片
1_hospitol_2.png:代表第一行的第二个图片
2_store_1.png:代表第二行的第一个图片
参数说明:
-i 代表输入文件夹
-o 代表输出文件
-s 代表图片之间的间距,默认为0px, 可不传
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
from PIL import Image
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
NAME_PATTERN = re.compile(r"^(\d+)_.+_(\d+)$")
class SpriteError(Exception):
pass
def validate_and_parse(filename: str) -> tuple[int, int]:
"""Validate filename format and return (row, col)."""
stem = Path(filename).stem
if not re.match(r"^\d+_", stem):
raise SpriteError(
f"Invalid filename '{filename}': must start with '<row>_', e.g. '1_home_1.png'"
)
if not re.search(r"_\d+$", stem):
raise SpriteError(
f"Invalid filename '{filename}': must end with '_<col>', e.g. '1_home_1.png'"
)
match = NAME_PATTERN.match(stem)
if not match:
raise SpriteError(
f"Invalid filename '{filename}': expected format '<row>_<name>_<col>.ext'"
)
return int(match.group(1)), int(match.group(2))
def collect_images(input_dir: Path) -> dict[tuple[int, int], Path]:
if not input_dir.is_dir():
raise SpriteError(f"Input directory not found: {input_dir}")
images: dict[tuple[int, int], Path] = {}
for path in sorted(input_dir.iterdir()):
if not path.is_file() or path.suffix.lower() not in IMAGE_EXTENSIONS:
continue
row, col = validate_and_parse(path.name)
key = (row, col)
if key in images:
raise SpriteError(
f"Duplicate grid position ({row}, {col}): "
f"'{images[key].name}' and '{path.name}'"
)
images[key] = path
if not images:
raise SpriteError(f"No image files found in: {input_dir}")
return images
def build_sprite(
images: dict[tuple[int, int], Path],
spacing: int,
) -> Image.Image:
opened: dict[tuple[int, int], Image.Image] = {}
rows = sorted({row for row, _ in images})
cols = sorted({col for _, col in images})
try:
for (row, col), path in images.items():
opened[(row, col)] = Image.open(path).convert("RGBA")
row_heights = {
row: max(opened[(row, col)].height for col in cols if (row, col) in opened)
for row in rows
}
col_widths = {
col: max(opened[(row, col)].width for row in rows if (row, col) in opened)
for col in cols
}
sprite_width = sum(col_widths[col] for col in cols) + spacing * (len(cols) - 1)
sprite_height = sum(row_heights[row] for row in rows) + spacing * (len(rows) - 1)
sprite = Image.new("RGBA", (sprite_width, sprite_height), (0, 0, 0, 0))
y = 0
for row in rows:
x = 0
for col in cols:
key = (row, col)
if key in opened:
img = opened[key]
cell_w = col_widths[col]
cell_h = row_heights[row]
paste_x = x + (cell_w - img.width) // 2
paste_y = y + (cell_h - img.height) // 2
sprite.paste(img, (paste_x, paste_y), img)
x += col_widths[col] + spacing
y += row_heights[row] + spacing
return sprite
finally:
for img in opened.values():
img.close()
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Generate a sprite sheet from grid-named images.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Image naming convention:\n"
" <row>_<name>_<col>.png\n"
"Examples:\n"
" 1_home_1.png -> row 1, column 1\n"
" 1_hospitol_2.png -> row 1, column 2\n"
" 2_store_1.png -> row 2, column 1\n"
"\n"
"Example:\n"
" python img_sprite.py -i ./images -o ./output/sprite.png -s 0"
),
)
parser.add_argument("-i", "--input", required=True, help="Input image directory")
parser.add_argument("-o", "--output", required=True, help="Output sprite PNG path")
parser.add_argument(
"-s",
"--spacing",
type=int,
default=0,
help="Spacing between images in pixels (default: 0)",
)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
if args.spacing < 0:
print("Error: spacing must be >= 0", file=sys.stderr)
return 1
input_dir = Path(args.input).expanduser().resolve()
output_path = Path(args.output).expanduser().resolve()
try:
images = collect_images(input_dir)
sprite = build_sprite(images, args.spacing)
output_path.parent.mkdir(parents=True, exist_ok=True)
sprite.save(output_path)
print(
f"Sprite saved: {output_path} "
f"({sprite.width}x{sprite.height}, {len(images)} images, spacing={args.spacing}px)"
)
return 0
except SpriteError as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())