fetch-browser-icons.sh raw
1 #!/usr/bin/env bash
2 # Fetch and convert browser launcher icons into assets/browser-icons/<id>.png
3 # at 96x96. Uses Wikimedia's Special:FilePath redirector for canonical lookup.
4 # Anything we can't fetch is left unwritten — the app falls back to the
5 # in-app colored swatch for that browser.
6 set -uo pipefail
7
8 cd "$(dirname "$0")/.."
9 out=assets/browser-icons
10 mkdir -p "$out"
11
12 UA="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
13
14 # id|filename-on-commons-or-en-wikipedia|origin-host
15 entries=(
16 "firefox|Firefox_logo,_2019.svg|commons"
17 "firefox-nightly|Firefox_Nightly_logo,_2017.svg|commons"
18 "chrome|Google_Chrome_icon_(February_2022).svg|commons"
19 "brave|Brave_lion_icon.svg|commons"
20 "tor|Tor-logo-2011-flat.svg|commons"
21 "ddg|The_DuckDuckGo_Duck.png|en"
22 )
23
24 # Direct-URL sources for niche browsers Wikimedia doesn't host.
25 direct=(
26 "ironfox|https://ironfoxoss.org/ironfox.png"
27 "vanadium|https://grapheneos.org/apple-touch-icon.png"
28 )
29
30 for entry in "${entries[@]}"; do
31 IFS='|' read -r id file host <<<"$entry"
32 case $host in
33 commons) url="https://commons.wikimedia.org/wiki/Special:FilePath/$file" ;;
34 en) url="https://en.wikipedia.org/wiki/Special:FilePath/$file" ;;
35 esac
36 tmp=$(mktemp -t bf-icon).${file##*.}
37 if curl -fsSL -A "$UA" -o "$tmp" "$url"; then
38 ok=0
39 if [[ ${file##*.} == svg ]] && command -v rsvg-convert >/dev/null; then
40 rsvg-convert -w 96 -h 96 "$tmp" -o "$out/$id.png" 2>/dev/null && ok=1
41 fi
42 if [ $ok -eq 0 ]; then
43 magick "$tmp" -resize 96x96 -background none -gravity center -extent 96x96 "$out/$id.png" 2>/dev/null && ok=1
44 fi
45 [ $ok -eq 1 ] && echo "ok($id)" || echo "convert-fail($id)"
46 else
47 echo "fetch-fail($id)"
48 fi
49 rm -f "$tmp"
50 sleep 1
51 done
52
53
54 # ─── Direct-URL sources for niche browsers ──────────────────────────────
55 for entry in "${direct[@]}"; do
56 IFS='|' read -r id url <<<"$entry"
57 if [ -f "$out/$id.png" ]; then echo "skip($id) — already have"; continue; fi
58 tmp=$(mktemp -t bf-icon).png
59 if curl -fsSL -A "$UA" -o "$tmp" "$url"; then
60 magick "$tmp" -resize 96x96 -background none -gravity center -extent 96x96 "$out/$id.png" 2>/dev/null \
61 && echo "direct($id) ← $url" || echo "convert-fail($id)"
62 else
63 echo "fetch-fail($id) ← $url"
64 fi
65 rm -f "$tmp"
66 sleep 1
67 done
68
69 # Firefox Beta has no canonical public icon distinct from Firefox release.
70 # Copy the Firefox icon so the row still gets a brand-correct logo.
71 if [ -f "$out/firefox.png" ] && [ ! -f "$out/firefox-beta.png" ]; then
72 cp "$out/firefox.png" "$out/firefox-beta.png" && echo "alias(firefox-beta ← firefox)"
73 fi
74
75 # ─── Fallback: scrape privacytests.org/android ───────────────────────────
76 # Their browser-logo-image <img> tags carry base64 PNG data URIs that map
77 # cleanly to several of our ids — and crucially include Firefox Focus, which
78 # Wikimedia doesn't host. We only fill in ids we don't already have.
79 echo
80 echo "Filling gaps from privacytests.org/android…"
81 pt=$(mktemp -t pt-android).html
82 if curl -fsSL -A "$UA" -o "$pt" "https://privacytests.org/android"; then
83 python3 - "$pt" "$out" <<'PY'
84 import re, base64, pathlib, sys
85 html_path, out_dir = sys.argv[1], pathlib.Path(sys.argv[2])
86 html = pathlib.Path(html_path).read_text()
87 # privacytests name -> our browser id
88 m = {'brave': 'brave', 'chrome': 'chrome', 'duckduckgo': 'ddg',
89 'firefox': 'firefox', 'focus': 'focus', 'tor': 'tor'}
90 hits = re.findall(
91 r'<img class="browser-logo-image" alt="(\w+) logo" '
92 r'src="data:image/png;base64,([^"]+)"', html)
93 for name, b64 in hits:
94 if name not in m: continue
95 our = m[name]
96 dst = out_dir / f"{our}.png"
97 if dst.exists():
98 print(f"skip({our}) — already have")
99 continue
100 dst.write_bytes(base64.b64decode(b64))
101 print(f"pt({our}) ← privacytests.org")
102 PY
103 fi
104 rm -f "$pt"
105
106 echo
107 echo "Wrote $(ls "$out" 2>/dev/null | wc -l | tr -d ' ') icons to $out/"
108