Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ sprites
\- icons (PNGs)
\- generation viii
\- icons (PNGs with female variants)
\- brilliant-diamond-shining-pearl (PNGs)
\- generation xi
\- scarlet-violet (PNGs with female)
\- default PokeAPI sprites (PNGs with back, female, shiny, back-female, back-shiny, shiny-female variants)
\- items
\- default PokeAPI items (PNGs)
Expand Down Expand Up @@ -264,6 +267,22 @@ Animated
| PNG<br>_30x30_, _24x24_ |
| <img src="sprites/items/ability-capsule.png" width="30"/> |

###### `brilliant-diamond-shining-pearl`

| Front |
| --- |
| PNG<br>_256x256_ |
| <img src="sprites/pokemon/versions/generation-viii/brilliant-diamond-shining-pearl/25.png" width="100"/> |

##### `generation ix`

###### `brilliant-diamond-shining-pearl`

| Front |
| --- |
| PNG<br>_256x256_ |
| <img src="sprites/pokemon/versions/generation-ix/scarlet-violet/25.png" width="100"/> |

## Thanks

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)
292 changes: 292 additions & 0 deletions scripts/generate_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
# python .\generate_report.py --local-path "../sprites/pokemon/versions" --output .\out\report.html

import os
import argparse
import logging
from datetime import datetime
from typing import Dict, Set, Tuple, List

import requests
import requests_cache
import natsort


# ---------------------------------------------------------------------------
# Setup caching (24h)
# ---------------------------------------------------------------------------
requests_cache.install_cache(
"api_cache",
backend="sqlite",
use_cache_dir=True,
expire_after=86400
)


# ---------------------------------------------------------------------------
# Static folder config, extend this map to test more generations/games/folders
# ---------------------------------------------------------------------------
FOLDER: Dict[str, List[str]] = {
"generation-viii": [
"brilliant-diamond-shining-pearl"
],
"generation-ix": [
"scarlet-violet"
]
}


# Typing
FileInfo = Dict[str, Tuple[str, str, Set[str]]]


# ---------------------------------------------------------------------------
# Logging configuration
# ---------------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format="[%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# API helper functions
# ---------------------------------------------------------------------------
def get_pokemon_data(api_url: str, endpoint: str, identifier: str) -> dict:
"""Request PokéAPI data for a specific endpoint."""
url = f"{api_url}/{endpoint}/{identifier}"
response = requests.get(url)

if response.status_code != 200:
logger.warning(f"API request failed: {url} (Status {response.status_code})")
return {}

return response.json()


# ---------------------------------------------------------------------------
# Local filesystem scan
# ---------------------------------------------------------------------------
def get_local_images(
local_path: str,
folder_config: Dict[str, List[str]]
) -> FileInfo:
"""Scan local directories for image files."""
all_files_by_path: FileInfo = {}

for gen_key, games in folder_config.items():
for game_key in games:
path = os.path.join(local_path, gen_key, game_key)

try:
files = {
f for f in os.listdir(path)
if os.path.isfile(os.path.join(path, f))
}

logger.info(f"Found {len(files)} files in '{path}'.")
all_files_by_path[path] = (gen_key, game_key, files)

except FileNotFoundError:
logger.warning(f"Local path '{path}' not found. Skipping.")
all_files_by_path[path] = (gen_key, game_key, set())

return all_files_by_path


# ---------------------------------------------------------------------------
# HTML generation
# ---------------------------------------------------------------------------
def create_table(
path_prefix: str,
generation_key: str,
game_key: str,
file_set: Set[str],
api_url: str
) -> str:
"""Generate the HTML table displaying comparison images."""
table_rows = ""

for filename in natsort.natsorted(file_set):
identifier = os.path.splitext(filename)[0]
base_id = identifier.split("-", 1)[0]

is_variant = "-" in identifier
variant_suffix = identifier.split("-", 1)[1] if is_variant else ""

logger.info(f"Processing '{filename}'")

final_api_identifier = base_id
error = False
found_variant_match = False

# Variant handling
if is_variant:
species_data = get_pokemon_data(api_url, "pokemon-species", base_id)

if species_data:
for variety in species_data.get("varieties", []):
api_name = variety.get("pokemon", {}).get("name", "")

if api_name.endswith(f"-{variant_suffix}"):
final_api_identifier = api_name
found_variant_match = True
error = False
break
else:
error = True
else:
error = True

# Base Pokémon fetch
pokemon = get_pokemon_data(api_url, "pokemon", final_api_identifier)
pokemon_sprites = pokemon.get("sprites") if pokemon else None

# Form fallback
if is_variant and not found_variant_match and pokemon:
for form in pokemon.get("forms", []):
form_name = form.get("name", "")
if form_name.endswith(f"-{variant_suffix}"):
form_data = get_pokemon_data(api_url, "pokemon-form", form_name)
pokemon_sprites = form_data.get("sprites", {})
error = False
break

# Extract sprites
default_sprite = (
pokemon_sprites.get("front_default", "")
if pokemon_sprites else ""
)

version_sprite = (
pokemon_sprites.get("versions", {})
.get(generation_key, {})
.get(game_key, {})
.get("front_default", "")
if pokemon_sprites else ""
)

# Add HTML row
table_rows += f"""
<div class="inline-flex flex-col items-center m-1 p-2 border-2 {'bg-red-600' if error else 'border-indigo-600'}">
<span class="text-xs text-gray-500">{identifier}</span>
<img src="{default_sprite}" style="max-width: 96px; object-fit: contain;">
<img src="{version_sprite}" style="max-width: 96px; object-fit: contain;">
</div>
"""

return f"""
<h2 class="text-xl font-bold mt-6 mb-3 border-b-2 pb-1">
Analysis for path: <code>{path_prefix}</code> ({len(file_set)} files)
</h2>
<div class="flex flex-wrap">
{table_rows}
</div>
"""


# ---------------------------------------------------------------------------
# Full report generation
# ---------------------------------------------------------------------------
def generate_report(
local_path: str,
api_url: str,
output_file: str,
cli_args: dict
) -> None:
"""Generate the entire HTML report."""
all_files_by_path = get_local_images(local_path, FOLDER)

tables_html = ""
for path, (gen_key, game_key, files) in all_files_by_path.items():
if files:
tables_html += create_table(
path,
gen_key,
game_key,
files,
api_url
)

timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# Render CLI args as HTML
cli_args_html = "<ul>" + "".join(
f"<li><strong>{k}</strong>: {v}</li>" for k, v in cli_args.items()
) + "</ul>"

html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>Sprites Report</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="p-6 bg-gray-100">
<h1 class="text-3xl font-bold mb-4">Sprites Report</h1>
<p><strong>Generated on:</strong> {timestamp}</p>
<h2 class="text-xl font-bold mt-2 mb-2">CLI Arguments:</h2>
{cli_args_html}

{tables_html}
</body>
</html>
"""

os.makedirs(os.path.dirname(output_file), exist_ok=True)

with open(output_file, "w", encoding="utf-8") as f:
f.write(html_content)

logger.info(f"Report successfully created: {os.path.abspath(output_file)}")


# ---------------------------------------------------------------------------
# CLI handling
# ---------------------------------------------------------------------------
def parse_args():
parser = argparse.ArgumentParser(
description="Generate Pokémon sprite comparison report."
)

parser.add_argument(
"--local-path",
required=True,
help="Base directory containing the local sprite folders."
)

parser.add_argument(
"--api-url",
help="Base URL of the Pokémon API.",
default="http://localhost/api/v2"
)

parser.add_argument(
"--output",
required=True,
help="Path to the generated HTML report."
)

parser.add_argument(
"--clear-cache",
action="store_true",
help="Clear requests_cache before execution.",
default=False
)

return parser.parse_args()


if __name__ == "__main__":
args = parse_args()

if args.clear_cache:
requests_cache.clear()
logger.info("Cache cleared before run.")

generate_report(
local_path=args.local_path,
api_url=args.api_url,
output_file=args.output,
cli_args=vars(args)
)
52 changes: 52 additions & 0 deletions scripts/pad_to_canvas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# python .\pad_to_canvas.py --input ../sprites/pokemon/versions/generation-viii/brilliant-diamond-shining-pearl --output ../sprites/pokemon/versions/generation-viii/brilliant-diamond-shining-pearl_padded

import os
import argparse
from PIL import Image

def pad_images(input_folder, output_folder, canvas_size):
os.makedirs(output_folder, exist_ok=True)

for filename in os.listdir(input_folder):
if not filename.lower().endswith(".png"):
continue

img_path = os.path.join(input_folder, filename)
img = Image.open(img_path).convert("RGBA") # keep transparency

# skip images that are taller than the canvas
if img.height > canvas_size:
print(f"Skipping {filename} (height {img.height}px > {canvas_size}px)")
continue

# create transparent canvas
canvas = Image.new("RGBA", (canvas_size, canvas_size), (0, 0, 0, 0))

# bottom-center placement
x = (canvas_size - img.width) // 2
y = canvas_size - img.height

canvas.paste(img, (x, y), img)

out_path = os.path.join(output_folder, filename)
canvas.save(out_path)

print(f"Saved: {out_path}")

print("Done!")

# ---------------------------------------------------------------------------
# CLI handling
# ---------------------------------------------------------------------------
def parse_args():
parser = argparse.ArgumentParser(description="Pad images to a transparent square canvas.")
parser.add_argument("--input", required=True, help="Input folder path")
parser.add_argument("--output", required=True, help="Output folder path")
parser.add_argument("--size", type=int, default=256, help="Canvas size (default: 256)")

return parser.parse_args()

if __name__ == "__main__":
args = parse_args()

pad_images(args.input, args.output, args.size)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading