feat(logos): Provider-Kette, lokaler Cache und Markenfarben
- simple-icons als kompakter Index im Repository (3.459 Marken, 2 MB gzip),
erzeugt von scripts/vendor_simple_icons.py bzw. `make vendor-icons`
- logo.dev und Brandfetch als optionale Adapter, ohne Schlüssel übersprungen
- Favicon-Fallback und generierter Buchstaben-Avatar als Garantie
- Bei eindeutigem Offline-Treffer unterbleiben Anfragen nach außen komplett
- Cache im Dateisystem nach SHA-256, Auslieferung nur über /api/logos/{id}
mit immutable-Header und ETag
- Markenfarbe aus SVG-Fills bzw. per k-Means (k=4) über 64x64 Pixel, dazu eine
aufgehellte Variante mit mindestens 4,5:1 Kontrast auf dunklem Grund
- Kandidatensuche mit Vorauswahl, Auswahl, Upload und Zurücksetzen
- Bildtyp wird nur noch am Inhalt bestimmt, nicht an der gemeldeten Kopfzeile
- 60 neue Tests, insgesamt 208 grün
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Erzeugt den lokalen simple-icons-Index aus dem npm-Paket.
|
||||
|
||||
Das npm-Paket enthält rund 3.500 einzelne SVG-Dateien (etwa 15 MB). Für den
|
||||
Betrieb genügen Slug, Titel, Markenfarbe, Pfaddaten und Aliasnamen – daraus wird
|
||||
eine einzelne gzip-komprimierte JSON-Datei von etwa 2 MB.
|
||||
|
||||
Aufruf: python scripts/vendor_simple_icons.py [--version 16] [--keep-tmp]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import gzip
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
TARGET = Path(__file__).resolve().parent.parent / "app" / "assets" / "simple_icons.json.gz"
|
||||
PATH_PATTERN = re.compile(r'<path\s+d="([^"]+)"')
|
||||
|
||||
|
||||
def download(version: str, workdir: Path) -> Path:
|
||||
"""Lädt das npm-Paket herunter und entpackt es."""
|
||||
result = subprocess.run(
|
||||
["npm", "pack", f"simple-icons@{version}"],
|
||||
cwd=workdir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
archive = workdir / result.stdout.strip().splitlines()[-1]
|
||||
with tarfile.open(archive) as tar:
|
||||
tar.extractall(workdir, filter="data")
|
||||
return workdir / "package"
|
||||
|
||||
|
||||
def collect_aliases(entry: dict) -> list[str]:
|
||||
"""Sammelt alternative Schreibweisen aus den Metadaten."""
|
||||
names: list[str] = []
|
||||
aliases = entry.get("aliases") or {}
|
||||
for key in ("aka", "alt", "old", "dup"):
|
||||
value = aliases.get(key)
|
||||
if isinstance(value, list):
|
||||
names.extend(item if isinstance(item, str) else item.get("title", "") for item in value)
|
||||
elif isinstance(value, dict):
|
||||
names.extend(str(item) for item in value.values())
|
||||
return [name for name in names if name]
|
||||
|
||||
|
||||
def build(package: Path) -> list[dict]:
|
||||
"""Baut den kompakten Index aus Metadaten und SVG-Pfaden."""
|
||||
metadata = json.loads((package / "data" / "simple-icons.json").read_text(encoding="utf-8"))
|
||||
icons: list[dict] = []
|
||||
|
||||
for entry in metadata:
|
||||
svg_file = package / "icons" / f"{entry['slug']}.svg"
|
||||
if not svg_file.exists():
|
||||
print(f" übersprungen (keine SVG-Datei): {entry['slug']}", file=sys.stderr)
|
||||
continue
|
||||
match = PATH_PATTERN.search(svg_file.read_text(encoding="utf-8"))
|
||||
if not match:
|
||||
print(f" übersprungen (kein Pfad): {entry['slug']}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
icon = {
|
||||
"s": entry["slug"],
|
||||
"t": entry["title"],
|
||||
"h": entry["hex"],
|
||||
"p": match.group(1),
|
||||
}
|
||||
aliases = collect_aliases(entry)
|
||||
if aliases:
|
||||
icon["a"] = aliases
|
||||
icons.append(icon)
|
||||
|
||||
return icons
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--version", default="16", help="npm-Version von simple-icons.")
|
||||
parser.add_argument("--keep-tmp", action="store_true", help="Arbeitsverzeichnis behalten.")
|
||||
args = parser.parse_args()
|
||||
|
||||
workdir = Path(tempfile.mkdtemp(prefix="simple-icons-"))
|
||||
try:
|
||||
print(f"Lade simple-icons@{args.version} …")
|
||||
package = download(args.version, workdir)
|
||||
version = json.loads((package / "package.json").read_text())["version"]
|
||||
|
||||
icons = build(package)
|
||||
payload = {"version": version, "icons": icons}
|
||||
raw = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
|
||||
TARGET.parent.mkdir(parents=True, exist_ok=True)
|
||||
TARGET.write_bytes(gzip.compress(raw, 9))
|
||||
|
||||
print(
|
||||
f"{len(icons)} Icons aus simple-icons {version} geschrieben nach "
|
||||
f"{TARGET.relative_to(TARGET.parent.parent.parent)} "
|
||||
f"({TARGET.stat().st_size / 1_000_000:.2f} MB)."
|
||||
)
|
||||
return 0
|
||||
finally:
|
||||
if not args.keep_tmp:
|
||||
shutil.rmtree(workdir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user