From 86f9bda4ef716e9fb82abb8aa0cff794ff326baf Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:06:10 +0200 Subject: [PATCH] chore: minify the svg logos svgo drops the editor markup, and the PNG inside RetroDECK's icon is cut to the 80 px tile: 12.7 KB down to 5.1 KB. --- docs_assets/logos/lakka.svg | 37 +--------------------- docs_assets/logos/retroarch.svg | 13 +------- docs_assets/logos/retrodeck.svg | 54 +-------------------------------- scripts/fetch_site_images.py | 46 ++++++++++++++++++++++++++-- 4 files changed, 47 insertions(+), 103 deletions(-) diff --git a/docs_assets/logos/lakka.svg b/docs_assets/logos/lakka.svg index fc89ee3c..ca13e9b3 100644 --- a/docs_assets/logos/lakka.svg +++ b/docs_assets/logos/lakka.svg @@ -1,36 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs_assets/logos/retroarch.svg b/docs_assets/logos/retroarch.svg index a35b434a..a7f0e558 100644 --- a/docs_assets/logos/retroarch.svg +++ b/docs_assets/logos/retroarch.svg @@ -1,12 +1 @@ - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/docs_assets/logos/retrodeck.svg b/docs_assets/logos/retrodeck.svg index 185f9d63..f5e30ab2 100644 --- a/docs_assets/logos/retrodeck.svg +++ b/docs_assets/logos/retrodeck.svg @@ -1,53 +1 @@ - - + \ No newline at end of file diff --git a/scripts/fetch_site_images.py b/scripts/fetch_site_images.py index 783f911c..740eff63 100644 --- a/scripts/fetch_site_images.py +++ b/scripts/fetch_site_images.py @@ -10,7 +10,8 @@ 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. +weight. SVG is minified by svgo, and a raster an SVG embeds is cut to the +tile size. 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. @@ -22,8 +23,11 @@ and PyYAML. from __future__ import annotations import argparse +import base64 import io import json +import re +import subprocess import sys import urllib.error import urllib.parse @@ -79,6 +83,44 @@ def safe_svg(data: bytes, url: str) -> bytes: return data +# A raster inside an SVG logo: RetroDECK's icon carried a 441 x 334 PNG, +# 9 KiB of a mark drawn 40 px wide. It never needs more pixels than the +# whole tile. +EMBEDDED_RASTER = re.compile(rb'data:image/(?:png|jpeg|webp);base64,([A-Za-z0-9+/=\s]+)') + + +def shrink_embedded(data: bytes) -> bytes: + """Each raster an SVG embeds, at most LOGO_SIZE pixels on its longest side.""" + + def shrink(match: re.Match[bytes]) -> 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(base64.b64decode(re.sub(rb"\s", b"", match.group(1))))).convert("RGBA") + if max(image.size) <= LOGO_SIZE: + return match.group(0) + image.thumbnail((LOGO_SIZE, LOGO_SIZE), Image.LANCZOS) + out = io.BytesIO() + image.save(out, "PNG", optimize=True) + return b"data:image/png;base64," + base64.b64encode(out.getvalue()) + + return EMBEDDED_RASTER.sub(shrink, data) + + +# Editor markup (Inkscape's namedview, sodipodi attributes, metadata) was +# most of the bytes of two logos. svgo comes with the site's packages. +SVGO = ROOT / "website" / "node_modules" / ".bin" / "svgo" + + +def minify_svg(data: bytes) -> bytes: + if not SVGO.is_file(): + raise SystemExit("svgo is needed to minify SVG logos: npm ci --prefix website") + result = subprocess.run([str(SVGO), "--multipass", "-i", "-", "-o", "-"], + input=data, capture_output=True, check=True) + return result.stdout + + def write_manifest(directory: Path, note: str, entries: list[dict]) -> None: entries.sort(key=lambda entry: entry["file"]) document = {"note": note, "images": entries} @@ -101,7 +143,7 @@ def fetch_logos() -> None: 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) + file_name, payload = f"{name}.svg", minify_svg(shrink_embedded(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}.*"):