From d798fedf55497dcc141d70047ad0e1ff619e9b3c Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 28 May 2026 17:18:58 +0300 Subject: [PATCH] feat(icon): redesign app icon as "Beacon" and ship multi-resolution ICO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace generic Spotify-green circle with a refined "Beacon" design: squircle + deep-teal diagonal gradient (#0B3D3B → #1A6B5E) + warm parchment play triangle (#F5F1E8) with drop shadow, top sheen, and ghosted echo-chevrons hinting at broadcast/stream - Grow icon.ico from a single 16×16 frame (208 B) to a 10-frame multi-resolution ICO (16/20/24/32/40/48/64/96/128/256, ~37 KB) so Windows no longer upscales 16×16 into mush for the installer chrome, Start Menu, desktop shortcuts, Alt+Tab, and File Explorer tiles - Add scripts/generate-icon.py: SVG is the source of truth; resvg-py rasterizes every ICO size; Pillow packs the multi-resolution ICO - Update tray.py to pick a 64×64 frame from the new ICO and update its procedural fallback to the same Beacon palette so a missing ICO no longer regresses the tray to the old Spotify-green circle - Add resvg-py to [dev] deps (build-time only) Co-Authored-By: Claude Opus 4.7 --- media_server/static/icons/icon-256.png | Bin 0 -> 13818 bytes media_server/static/icons/icon.ico | Bin 208 -> 37568 bytes media_server/static/icons/icon.svg | 35 +++++-- media_server/tray.py | 129 ++++++++++++++++++------- pyproject.toml | 3 + scripts/generate-icon.py | 113 ++++++++++++++++++++++ 6 files changed, 240 insertions(+), 40 deletions(-) create mode 100644 media_server/static/icons/icon-256.png create mode 100644 scripts/generate-icon.py diff --git a/media_server/static/icons/icon-256.png b/media_server/static/icons/icon-256.png new file mode 100644 index 0000000000000000000000000000000000000000..4d1724af58265fd67ef96f95738a8e319fbbad80 GIT binary patch literal 13818 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 Z;dbKQHK_am{H6<}d(Yr*rIua9{{f>ptM&i@ literal 0 HcmV?d00001 diff --git a/media_server/static/icons/icon.ico b/media_server/static/icons/icon.ico index 41dda8512f00b8401dbdb42bc90a6a6fa13a5ed0..5cde230515251deb73ac2dfbe28ccd72f7120a04 100644 GIT binary patch literal 37568 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 igg7C~9yi+=DhV-cb delta 182 zcmX@Gl<5Ma2m=En5DEw|0D%I-E+8q!FmZ`$Jwv0Xi(^Q|oT + - - - + + + + + + + + + + + + + + - - + + + + + + + + + + diff --git a/media_server/tray.py b/media_server/tray.py index cd01a8e..8c38ede 100644 --- a/media_server/tray.py +++ b/media_server/tray.py @@ -51,55 +51,116 @@ def _confirm(title: str, message: str) -> bool: return True -def _create_icon_image(size: int = 64) -> Image.Image: - """Create a tray icon: green circle with white play triangle.""" +# Frame size we ask the multi-resolution ICO for. Most Windows tray surfaces +# show 16x16 in the notification area and 32x32 in jump lists / Alt+Tab; 64 +# gives pystray enough headroom for both without forcing it to upscale. +_TRAY_ICON_SIZE = 64 + +# Palette mirrors media_server/static/icons/icon.svg ("Beacon" design). +_BG_DARK = (11, 61, 59, 255) # #0B3D3B +_BG_LIGHT = (26, 107, 94, 255) # #1A6B5E +_FG_PARCHMENT = (245, 241, 232, 255) # #F5F1E8 + + +def _create_icon_image(size: int = _TRAY_ICON_SIZE) -> Image.Image: + """Procedural fallback when no icon file is available. + + Matches the "Beacon" palette (deep teal squircle + warm parchment play + triangle) so a missing icon.ico does not regress us back to the old + Spotify-green circle. + """ img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) - # Green circle background - padding = 2 - draw.ellipse( - [padding, padding, size - padding, size - padding], - fill=(29, 185, 84, 255), + # Squircle background. Vertical gradient approximates the diagonal one + # in the real SVG well enough for a 64px fallback. + radius = int(size * 0.225) + for y in range(size): + t = y / max(1, size - 1) + color = tuple( + round(_BG_DARK[i] + (_BG_LIGHT[i] - _BG_DARK[i]) * t) for i in range(3) + ) + (255,) + draw.line([(0, y), (size - 1, y)], fill=color) + mask = Image.new("L", (size, size), 0) + ImageDraw.Draw(mask).rounded_rectangle((0, 0, size - 1, size - 1), radius=radius, fill=255) + bg = img.copy() + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + img.paste(bg, (0, 0), mask=mask) + + # Play triangle, positioned to match icon.svg's geometry. + draw = ImageDraw.Draw(img) + draw.polygon( + [ + (size * 0.345, size * 0.215), + (size * 0.345, size * 0.785), + (size * 0.755, size * 0.500), + ], + fill=_FG_PARCHMENT, ) - - # White play triangle - cx, cy = size // 2, size // 2 - r = size * 0.28 - triangle = [ - (cx - r * 0.6, cy - r), - (cx - r * 0.6, cy + r), - (cx + r * 0.9, cy), - ] - draw.polygon(triangle, fill=(255, 255, 255, 255)) - return img -def _load_icon_image() -> Image.Image: - """Load the ICO/SVG app icon, falling back to a generated image.""" +def _select_frame(image: Image.Image, target: int) -> Image.Image: + """Pick the best frame from a multi-resolution ICO. + + Pillow's ICO loader exposes the embedded sizes via ``image.info['sizes']``. + We pick the smallest frame at least as large as the target (so we never + upscale) and resize down to ``target x target`` with LANCZOS. + """ + sizes = sorted(image.info.get("sizes", []) or [], key=lambda wh: wh[0]) + chosen = next((wh for wh in sizes if wh[0] >= target), sizes[-1] if sizes else None) + if chosen is not None: + image.size = chosen + frame = image.copy().convert("RGBA") + if frame.size != (target, target): + frame = frame.resize((target, target), Image.LANCZOS) + return frame + + +def _load_icon_image(size: int = _TRAY_ICON_SIZE) -> Image.Image: + """Load the app icon for the tray. + + Order: + 1. ``icon.ico`` — the multi-resolution Windows icon ships with every + build; pick the frame closest to ``size`` and downscale if needed. + 2. ``icon.svg`` via resvg-py (preferred) or cairosvg (legacy). + 3. Procedural ``_create_icon_image`` fallback. + """ icons_dir = Path(__file__).parent / "static" / "icons" - # Try .ico first (best for Windows tray) ico_path = icons_dir / "icon.ico" if ico_path.exists(): try: - return Image.open(ico_path) + with Image.open(ico_path) as ico: + return _select_frame(ico, size) + except Exception as exc: + logger.warning("Failed to load tray icon from %s: %s", ico_path, exc) + + svg_path = icons_dir / "icon.svg" + if svg_path.exists(): + try: + import resvg_py + + png_data = resvg_py.svg_to_bytes( + svg_string=svg_path.read_text(encoding="utf-8"), + width=size, + height=size, + ) + return Image.open(io.BytesIO(bytes(png_data))).convert("RGBA") + except ImportError: + pass + except Exception as exc: + logger.warning("resvg rasterization of %s failed: %s", svg_path, exc) + + try: + import cairosvg + + png_data = cairosvg.svg2png(url=str(svg_path), output_width=size, output_height=size) + return Image.open(io.BytesIO(png_data)).convert("RGBA") except Exception: pass - # Try SVG via cairosvg - try: - import cairosvg - - svg_path = icons_dir / "icon.svg" - if svg_path.exists(): - png_data = cairosvg.svg2png(url=str(svg_path), output_width=64, output_height=64) - return Image.open(io.BytesIO(png_data)) - except Exception: - pass - - return _create_icon_image() + return _create_icon_image(size) class TrayManager: diff --git a/pyproject.toml b/pyproject.toml index 43717f8..c75e70d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,9 @@ dev = [ "pytest-asyncio>=0.21", "httpx>=0.24", "ruff>=0.4.0", + # SVG -> PNG rasterizer used by scripts/generate-icon.py to (re)build + # media_server/static/icons/icon.ico from icon.svg. Build-time only. + "resvg-py>=0.3.2", ] [project.urls] diff --git a/scripts/generate-icon.py b/scripts/generate-icon.py new file mode 100644 index 0000000..9fc111e --- /dev/null +++ b/scripts/generate-icon.py @@ -0,0 +1,113 @@ +"""Generate the Media Server application icon. + +The SVG in ``media_server/static/icons/icon.svg`` is the single source of +truth. This script rasterizes it at every Windows ICO size via ``resvg-py`` +(Rust-backed, identical math to Firefox's SVG renderer) and packs them all +into a multi-resolution ``icon.ico``. + +This replaces the original 16x16-only ICO that Windows was upscaling into +mush for the installer chrome, Start Menu, desktop shortcuts, and Alt+Tab. + +Usage: + python scripts/generate-icon.py + +Dependencies: + pip install resvg-py Pillow +""" + +from __future__ import annotations + +import io +from pathlib import Path + +import resvg_py +from PIL import Image + +# Sizes packed into the ICO. Windows picks the closest match per surface; +# more sizes = sharper rendering everywhere (taskbar, installer header, +# Alt+Tab, jump lists, Start tile, desktop, file explorer details/tiles). +ICO_SIZES: tuple[int, ...] = (16, 20, 24, 32, 40, 48, 64, 96, 128, 256) + +# The SVG source. Squircle + diagonal teal gradient + warm parchment play +# triangle with a drop shadow + ghosted echo-chevrons hinting at broadcast. +SVG_SOURCE = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +def _render_png(size: int) -> Image.Image: + """Rasterize the SVG to a PNG at ``size x size`` via resvg.""" + data = resvg_py.svg_to_bytes( + svg_string=SVG_SOURCE, + width=size, + height=size, + shape_rendering="geometric_precision", + ) + return Image.open(io.BytesIO(bytes(data))).convert("RGBA") + + +def main() -> None: + root = Path(__file__).resolve().parent.parent + out_dir = root / "media_server" / "static" / "icons" + out_dir.mkdir(parents=True, exist_ok=True) + + # SVG — canonical source, also used as the Web UI favicon. + svg_path = out_dir / "icon.svg" + svg_path.write_text(SVG_SOURCE, encoding="utf-8") + print(f"wrote {svg_path}") + + # Rasterize every ICO size via resvg. + frames = [_render_png(size) for size in ICO_SIZES] + + # Pack into a multi-resolution ICO. The "primary" image must be the + # largest; the rest go via append_images. Pillow's ICO writer then + # serializes one frame per size. + primary = frames[-1] + ico_path = out_dir / "icon.ico" + primary.save( + ico_path, + format="ICO", + sizes=[(s, s) for s in ICO_SIZES], + append_images=frames[:-1], + ) + print(f"wrote {ico_path} ({ico_path.stat().st_size:,} bytes, sizes={list(ICO_SIZES)})") + + # Largest PNG for documentation / non-Windows surfaces. + png_path = out_dir / "icon-256.png" + primary.save(png_path, format="PNG", optimize=True) + print(f"wrote {png_path}") + + +if __name__ == "__main__": + main()