Files
climperor/fetch_dota2_logo.py
T
voson 1d0428b4a3 Add hash routing and Dota 2 logo to relations preview.
- New web/relations/router.js (parseHash/serializeHash/installRouter):
  top tabs, hero + detail sub-tab, item, patch version, tag filters and
  search query all sync to URL (#/heroes/axe/core, ?tags=...&q=...);
  pushState for discrete picks, replaceState for debounced search.
- export_relations_site.py copies router.js so static export deep-links.
- Header shows dota2_logo.png + title, served via /ui-icon/ whitelist.
- fetch_dota2_logo.py fetches the transparent emblem asset.
- Docs: CHANGELOG / README / DESIGN / AGENTS updated.
2026-07-27 13:09:14 +08:00

78 lines
2.7 KiB
Python

"""Fetch the official Dota 2 emblem icon (no wordmark) for the preview header.
Source (discovered, not guessed) from the dota2.com.cn site HTML:
<link rel="Shortcut Icon" href="//www.dota2.com.cn/favicon.ico"/>
That .ico is a multi-frame container whose largest frames are 256x256 PNGs of
the red Dota 2 map-marker emblem with a transparent background — i.e. the
brand icon alone, no "DOTA 2" text. We extract the first 256x256 PNG frame,
decode with OpenCV, and write it to assets/ui_icons/dota2_logo.png so it rides
the existing /ui-icon/ route + static-export glob pipeline.
"""
import struct
import sys
import cv2
import numpy as np
from common import UI_ICONS
from http_utils import http_bytes
FAVICON_URL = "https://www.dota2.com.cn/favicon.ico"
OUT = UI_ICONS / "dota2_logo.png"
TARGET_SIZE = 256
def _extract_largest_png_frame(data: bytes) -> bytes | None:
"""Return the bytes of the largest PNG-embedded frame in an ICO blob."""
if len(data) < 6 or data[:4] != b"\x00\x00\x01\x00":
return None
count = struct.unpack("<H", data[4:6])[0]
best: tuple[int, bytes] | None = None
for i in range(count):
off = 6 + 16 * i
if off + 16 > len(data):
break
w = data[off]
size = struct.unpack("<I", data[off + 8 : off + 12])[0]
img_off = struct.unpack("<I", data[off + 12 : off + 16])[0]
width = 256 if w == 0 else w
blob = data[img_off : img_off + size]
if blob[:8] != b"\x89PNG\r\n\x1a\n":
continue
if best is None or width * width > best[0]:
best = (width * width, blob)
return best[1] if best else None
def main() -> int:
if OUT.is_file() and OUT.stat().st_size >= 512:
print(f"exists: {OUT} ({OUT.stat().st_size} bytes)")
return 0
data = http_bytes(FAVICON_URL, timeout=30)
if not data or len(data) < 16:
print("empty favicon download", file=sys.stderr)
return 1
png = _extract_largest_png_frame(data)
if png is None:
print("no PNG frame in .ico", file=sys.stderr)
return 1
arr = cv2.imdecode(np.frombuffer(png, dtype=np.uint8), cv2.IMREAD_UNCHANGED)
if arr is None:
print("decode failed", file=sys.stderr)
return 1
UI_ICONS.mkdir(parents=True, exist_ok=True)
ok = cv2.imwrite(str(OUT), arr)
if not ok:
print("write failed", file=sys.stderr)
return 1
h, w = arr.shape[:2]
channels = arr.shape[2] if arr.ndim == 3 else 1
transparent = int(np.count_nonzero(arr[:, :, 3] == 0)) if channels == 4 else 0
print(f"saved {OUT} ({w}x{h}, {OUT.stat().st_size} bytes, {transparent} px transparent)")
return 0
if __name__ == "__main__":
raise SystemExit(main())