Skip to content

Commit 28242b5

Browse files
authored
feat: Add default back sprites for gen 9 (#233)
* feat: Add default back sprites for gen 9 * fix: Fix sprite audit report issues, crc/chunk issues and wrong size
1 parent 3eee52b commit 28242b5

235 files changed

Lines changed: 549 additions & 723 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,6 @@ This folder contains the official B&W sprites on top of custom sprites designed
208208

209209
- The sprites with IDs greater than 650 are thus not official. You can find the source of these sprites at this [Smogon thread](https://www.smogon.com/forums/threads/sword-shield-sprite-project.3647722/) and also at this [Google Sheet](https://docs.google.com/spreadsheets/d/1acgzAjh0dnFRQnjZu8kSjS177rKCzpFfEHRLtwuuXRU/edit#gid=0). We thank leParagon, Blaquaza, TheAetherPlayer, G.E.Z., KingOfThe-X-Roads, Spook, Cynda, Involuntary Twitch, mjco, Z-nogyroP, PumpkinPastel, RadicalCharizard, HM100, N-Kin, Zerudez, MyMarshlands, Wobblebuns, princessofmusic, aXl, fishbowlsoul90, HealnDeal, Espeon Scientist, AMVictory, Mega-Pokebattlerz, Layell, GeoisEvil, Quanyails, RedRooster, Wyverii, Basic Vanillite, Larryturbo, TheCynicalPoet, Arkeis, paintseagull, Branflakes325, Siiilver, Noscium, Sleet, Zermonious, Bynine, Corson, Legitimate Username, TrainerSplash, Farriella, MrDollSteak, TeraVolt, Dleep, WPS, Brylark, KattenK, Travis, SpheX, SelenaArmorclaw and Hematite, who are the spriters that created these custom B&W sprites.
210210

211-
- Special thanks to [KingOfThe-X-Roads](https://www.deviantart.com/kingofthe-x-roads) for providing the front_default sprites for generation 9.
212-
213211
| Front | Back | Front female | Front shiny | Back female | Back shiny | Shiny female |
214212
| --- | --- | --- | --- | --- | --- | --- |
215213
| PNG<br>_96x96_ | PNG<br>_96x96_ | PNG<br>_96x96_ | PNG<br>_96x96_ | PNG<br>_96x96_ | PNG<br>_96x96_ | PNG<br>_96x96_ |
@@ -300,6 +298,8 @@ Animated
300298
| PNG<br>_256x256_ |
301299
| <img src="sprites/pokemon/versions/generation-ix/scarlet-violet/25.png" width="100"/> |
302300

303-
## Thanks
301+
## Special Thanks
304302

305-
We would like to thank the [Smogon community](https://www.smogon.com/) for allowing us to use and serve their custom B&W-style sprites for the Pokemon with IDs greater than 650. Check out their free and open-source Pokemon Battle Simulator at [Pokémon Showdown](https://github.com/smogon/pokemon-showdown)
303+
- We would like to thank the [Smogon community](https://www.smogon.com/) for allowing us to use and serve their custom B&W-style sprites for the Pokemon with IDs greater than 650. Check out their free and open-source Pokemon Battle Simulator at [Pokémon Showdown](https://github.com/smogon/pokemon-showdown)
304+
- [KingOfThe-X-Roads](https://www.deviantart.com/kingofthe-x-roads) for providing the front_default sprites for generation 9.
305+
- [x.com/DoveKyle](https://x.com/DoveKyle)/[github.com/kyledovey](https://github.com/kyledovey) for providing the back_default sprites for generation 9.

scripts/sprite_audit.py

Lines changed: 106 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import pandas as pd
2-
from pathlib import Path
3-
from PIL import Image
1+
import argparse
2+
import os
3+
import shutil
4+
import tempfile
45
from collections import Counter
6+
from pathlib import Path
7+
8+
import pandas as pd
9+
from PIL import Image, ImageFile
510

611
# CONFIGURATION
712
GITHUB_BASE_URL = "https://raw.githubusercontent.com/PokeAPI/pokeapi/master/data/v2/csv"
@@ -10,7 +15,6 @@
1015
VG_CSV_URL = f"{GITHUB_BASE_URL}/version_groups.csv"
1116

1217
# Local Sprite directories relative to this script
13-
# .parent.parent refers to /Parent/ (going up from /Parent/sprites/scripts/)
1418
SCRIPT_DIR = Path(__file__).resolve().parent
1519
BASE_PATH = SCRIPT_DIR.parent / "sprites" / "pokemon"
1620

@@ -31,12 +35,12 @@ def get_standard_dimension():
3135
for folder in PATHS.values():
3236
if not folder.exists():
3337
continue
34-
for file in folder.glob("*"):
38+
for file in sorted(folder.glob("*")):
3539
if file.suffix.lower() in valid_ext:
3640
try:
3741
with Image.open(file) as img:
3842
all_sizes.append(img.size)
39-
except:
43+
except Exception:
4044
continue
4145

4246
if not all_sizes:
@@ -47,7 +51,78 @@ def get_standard_dimension():
4751
return most_common
4852

4953

50-
def check_assets():
54+
def scan_entries(pokemon_entries, standard_size, collect_corrupts: bool = False):
55+
"""Scan entries, return (report_list, corrupt_set).
56+
57+
If collect_corrupts=True unreadable files are added to corrupt_set for a later repair pass.
58+
"""
59+
report = []
60+
corrupt_paths = set()
61+
62+
for pokemon in pokemon_entries:
63+
p_id = pokemon["pokemon_id"]
64+
s_id = pokemon["species_id"]
65+
name = pokemon["identifier"]
66+
gen = int(pokemon["generation"]) if pd.notnull(pokemon["generation"]) else "Unknown"
67+
filename = f"{p_id}.png"
68+
69+
for label, folder in PATHS.items():
70+
file_path = folder / filename
71+
issue = None
72+
73+
if not file_path.exists():
74+
issue = "missing_file"
75+
else:
76+
try:
77+
with Image.open(file_path) as img:
78+
if img.size != standard_size:
79+
issue = f"wrong_size_{img.size[0]}x{img.size[1]}"
80+
except Exception as e:
81+
print(f"⚠️ Error opening {file_path}: {e}")
82+
if collect_corrupts:
83+
corrupt_paths.add(file_path)
84+
issue = "corrupt_file"
85+
86+
if issue:
87+
report.append(
88+
{
89+
"pokemon_id": p_id,
90+
"identifier": name,
91+
"species_id": s_id,
92+
"sprite_type": label.lower().replace(" ", "_"),
93+
"generation": gen,
94+
"issue": issue,
95+
}
96+
)
97+
98+
return report, corrupt_paths
99+
100+
101+
def attempt_repair(path: Path) -> bool:
102+
"""Try to repair an unreadable image by loading with truncated-images enabled and re-saving.
103+
104+
Returns True if the file was replaced with a re-saved copy, False otherwise.
105+
"""
106+
try:
107+
ImageFile.LOAD_TRUNCATED_IMAGES = True
108+
with Image.open(path) as im:
109+
im.load()
110+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tf:
111+
tmpname = tf.name
112+
im.save(tmpname, format="PNG")
113+
shutil.move(tmpname, str(path))
114+
return True
115+
except Exception as ex:
116+
print(f" repair failed: {ex}")
117+
try:
118+
if "tmpname" in locals() and Path(tmpname).exists():
119+
Path(tmpname).unlink()
120+
except Exception:
121+
pass
122+
return False
123+
124+
125+
def check_assets(repair_enabled: bool = False):
51126
print("\n" + "=" * 50)
52127
print("🔍 POKÉMON SPRITE AUDIT")
53128
print(f"📂 Script: {Path(__file__).name}")
@@ -84,54 +159,30 @@ def check_assets():
84159
how="left",
85160
)
86161

87-
pokemon_entries = (
162+
df_entries = (
88163
df_merged[["id_x", "species_id", "identifier", "generation_id"]]
89164
.rename(columns={"id_x": "pokemon_id", "generation_id": "generation"})
90-
.to_dict("records")
91165
)
92166

167+
# Ensure deterministic ordering by pokemon_id
168+
df_entries = df_entries.sort_values(by=["pokemon_id"])
169+
pokemon_entries = df_entries.to_dict("records")
170+
93171
# 4. Deep Scan (Existence + Dimensions)
94-
print(
95-
f"🧪 Scanning {len(pokemon_entries)} entries across {len(PATHS)} categories..."
96-
)
97-
report_data = []
172+
print(f"🧪 Scanning {len(pokemon_entries)} entries across {len(PATHS)} categories...")
98173

99-
for pokemon in pokemon_entries:
100-
p_id = pokemon["pokemon_id"]
101-
s_id = pokemon["species_id"]
102-
name = pokemon["identifier"]
103-
gen = (
104-
int(pokemon["generation"])
105-
if pd.notnull(pokemon["generation"])
106-
else "Unknown"
107-
)
108-
filename = f"{p_id}.png"
174+
# First pass: collect corrupt files (if any)
175+
report_data, corrupt_set = scan_entries(pokemon_entries, standard_size, collect_corrupts=True)
109176

110-
for label, folder in PATHS.items():
111-
file_path = folder / filename
112-
issue = None
177+
if repair_enabled and corrupt_set:
178+
print(f"\nFound {len(corrupt_set)} corrupt/unopenable files; attempting repair...")
179+
for path in sorted(corrupt_set):
180+
print(f" → Repairing {path}...")
181+
success = attempt_repair(path)
182+
print(f" {'success' if success else 'failed'}: {path}")
113183

114-
if not file_path.exists():
115-
issue = "missing_file"
116-
else:
117-
try:
118-
with Image.open(file_path) as img:
119-
if img.size != standard_size:
120-
issue = f"wrong_size_{img.size[0]}x{img.size[1]}"
121-
except Exception:
122-
issue = "corrupt_file"
123-
124-
if issue:
125-
report_data.append(
126-
{
127-
"pokemon_id": p_id,
128-
"identifier": name,
129-
"species_id": s_id,
130-
"sprite_type": label.lower().replace(" ", "_"),
131-
"generation": gen,
132-
"issue": issue,
133-
}
134-
)
184+
# Re-scan after repair attempts
185+
report_data, _ = scan_entries(pokemon_entries, standard_size, collect_corrupts=False)
135186

136187
# 5. Output Report & Preview
137188
if report_data:
@@ -159,5 +210,12 @@ def check_assets():
159210
print("\n✨ Audit complete: 0 issues found. Your sprite collection is perfect!")
160211

161212

213+
def parse_args_and_run():
214+
parser = argparse.ArgumentParser(description="Audit Pokémon sprites")
215+
parser.add_argument("--repair", action="store_true", help="Attempt to repair corrupt images by re-saving via Pillow")
216+
args = parser.parse_args()
217+
check_assets(repair_enabled=args.repair)
218+
219+
162220
if __name__ == "__main__":
163-
check_assets()
221+
parse_args_and_run()

0 commit comments

Comments
 (0)