mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-24 12:49:03 -05:00
Shown at 40 px, the 96 px PNG logos were 136 KiB of the home page. WebP at twice the display size is 27 KiB for the twelve raster marks. SVG stays as each project publishes it.
176 lines
6.1 KiB
Python
176 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Download the platform logos and system icons the site shows, once.
|
|
|
|
The site served these from other hosts: GitHub avatars and files on the
|
|
``master`` branch of upstream repositories. A rename upstream broke an image
|
|
in silence, and every page view hit third-party hosts. This tool copies them
|
|
into ``docs_assets/logos/`` and ``docs_assets/system-icons/``, which are
|
|
committed, and records where each came from in a ``sources.json`` beside
|
|
them. The site build only reads those copies (``sitediagrams.localize_images``).
|
|
|
|
Raster logos are normalised to an 80 px square WebP, twice the 40 px they
|
|
are shown at, and system icons to a 48 px PNG, so every tile has the same
|
|
weight. SVG is kept as published.
|
|
Normalising needs Pillow. This is a maintenance tool run by hand when a
|
|
logo changes, not a build step: the build stays on the standard library
|
|
and PyYAML.
|
|
|
|
python scripts/fetch_site_images.py # logos and icons
|
|
python scripts/fetch_site_images.py --only logos
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import io
|
|
import json
|
|
import sys
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
|
|
LOGO_DIR = ROOT / "docs_assets" / "logos"
|
|
ICON_DIR = ROOT / "docs_assets" / "system-icons"
|
|
LOGO_SIZE = 80
|
|
ICON_SIZE = 48
|
|
AVATAR_HOST = "avatars.githubusercontent.com"
|
|
USER_AGENT = "retrobios-site-images"
|
|
|
|
def fetch(url: str) -> bytes:
|
|
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
with urllib.request.urlopen(request, timeout=60) as response:
|
|
return response.read()
|
|
|
|
|
|
def square_image(data: bytes, size: int, kind: str = "PNG") -> bytes:
|
|
try:
|
|
from PIL import Image
|
|
except ImportError as exc:
|
|
raise SystemExit("Pillow is needed to normalise raster images: pip install Pillow") from exc
|
|
image = Image.open(io.BytesIO(data)).convert("RGBA")
|
|
image.thumbnail((size, size), Image.LANCZOS)
|
|
tile = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
|
tile.paste(image, ((size - image.width) // 2, (size - image.height) // 2), image)
|
|
out = io.BytesIO()
|
|
if kind == "WEBP":
|
|
# Quality 90: half the bytes of lossless, edges still clean at 40 px.
|
|
tile.save(out, "WEBP", quality=90, method=6)
|
|
else:
|
|
tile.save(out, "PNG", optimize=True)
|
|
return out.getvalue()
|
|
|
|
|
|
def is_svg(data: bytes) -> bool:
|
|
head = data[:512].lstrip().lower()
|
|
return head.startswith(b"<?xml") or head.startswith(b"<svg")
|
|
|
|
|
|
def safe_svg(data: bytes, url: str) -> bytes:
|
|
lowered = data.lower()
|
|
for token in (b"<script", b"javascript:", b"onload=", b"<foreignobject"):
|
|
if token in lowered:
|
|
raise SystemExit(f"{url}: SVG carries {token.decode()}, not copied")
|
|
return data
|
|
|
|
|
|
def write_manifest(directory: Path, note: str, entries: list[dict]) -> None:
|
|
entries.sort(key=lambda entry: entry["file"])
|
|
document = {"note": note, "images": entries}
|
|
(directory / "sources.json").write_text(
|
|
json.dumps(document, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
|
)
|
|
|
|
|
|
def fetch_logos() -> None:
|
|
from siteguides import GUIDES
|
|
|
|
with open(ROOT / "platforms" / "_registry.yml", encoding="utf-8") as handle:
|
|
platforms = yaml.safe_load(handle)["platforms"]
|
|
LOGO_DIR.mkdir(parents=True, exist_ok=True)
|
|
entries = []
|
|
for name, entry in sorted(platforms.items()):
|
|
url = entry.get("logo")
|
|
if not url:
|
|
continue
|
|
download = f"{url}?s={LOGO_SIZE * 2}" if AVATAR_HOST in url else url
|
|
data = fetch(download)
|
|
if is_svg(data):
|
|
file_name, payload = f"{name}.svg", safe_svg(data, url)
|
|
else:
|
|
file_name, payload = f"{name}.webp", square_image(data, LOGO_SIZE, "WEBP")
|
|
for stale in LOGO_DIR.glob(f"{name}.*"):
|
|
if stale.name != file_name:
|
|
stale.unlink()
|
|
(LOGO_DIR / file_name).write_bytes(payload)
|
|
# Who owns each mark: logo_owner and logo_licence in the guides.
|
|
guide = GUIDES.get(name)
|
|
owner = (guide and guide.logo_owner) or name
|
|
licence = (guide and guide.logo_licence) or "ownership not recorded"
|
|
entries.append({
|
|
"file": file_name, "platform": name, "source": url,
|
|
"owner": owner, "licence": licence,
|
|
})
|
|
print(f" {file_name} from {url}")
|
|
write_manifest(
|
|
LOGO_DIR,
|
|
"Each platform's own logo, owned by its project and shown only to name that "
|
|
"platform. Copied once by scripts/fetch_site_images.py.",
|
|
entries,
|
|
)
|
|
|
|
|
|
def fetch_icons() -> None:
|
|
from generate_site import _group_by_manufacturer, load_database
|
|
from siterender import SYSTEM_ICON_BASE, _icon_name
|
|
|
|
db = load_database(str(ROOT / "database.json"))
|
|
names = sorted({
|
|
_icon_name(manufacturer, console)
|
|
for manufacturer, consoles in _group_by_manufacturer(db).items()
|
|
for console in consoles
|
|
})
|
|
ICON_DIR.mkdir(parents=True, exist_ok=True)
|
|
entries = []
|
|
for name in names:
|
|
url = f"{SYSTEM_ICON_BASE}/{urllib.parse.quote(name)}.png"
|
|
try:
|
|
data = fetch(url)
|
|
except urllib.error.HTTPError as exc:
|
|
if exc.code == 404:
|
|
continue
|
|
raise
|
|
file_name = f"{name}.png"
|
|
(ICON_DIR / file_name).write_bytes(square_image(data, ICON_SIZE))
|
|
entries.append({"file": file_name, "source": url})
|
|
print(f" {file_name}")
|
|
write_manifest(
|
|
ICON_DIR,
|
|
"System icons from libretro/retroarch-assets (xmb/systematic), "
|
|
"licensed CC-BY-4.0 by the libretro team. Resized to 48 px.",
|
|
entries,
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0], allow_abbrev=False)
|
|
parser.add_argument("--only", choices=("logos", "icons"), help="fetch one kind only")
|
|
args = parser.parse_args()
|
|
if args.only in (None, "logos"):
|
|
print("Platform logos:")
|
|
fetch_logos()
|
|
if args.only in (None, "icons"):
|
|
print("System icons:")
|
|
fetch_icons()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
from common import run_cli
|
|
|
|
run_cli(main)
|