feat: portfolio-led redesign with Shootings umbrella, original logo and motion layer

This commit is contained in:
desfoto automation
2026-09-19 17:11:25 +02:00
parent 2ccb39a69b
commit 447b909d05
177 changed files with 1853 additions and 773 deletions

View File

@@ -7,6 +7,10 @@ Sources
We pull the pre-derived `web` variant (long edge 2560 px) instead of the
20-60 MB camera originals. Only assets we are allowed to publish as the
rights holder are listed in src/images.json.
* Own portfolio files of the existing site (www.dennyschulz.de/legacy/<file>).
These carry the concert, band and free-work photographs that are missing from
the pool; they are fetched as-is and downscaled by the build. Also used for
the operator's original logo (www.dennyschulz.de/logo.png).
* Google Fonts (open licensed, OFL) - downloaded once, then self-hosted.
Licences are written to docs/.
* YouTube poster frame for the own music video "Thjodroerir - Skogamor".
@@ -26,10 +30,13 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "assets-src"
IMG_OUT = SRC / "images"
BRAND_OUT = SRC / "brand"
FONT_OUT = SRC / "fonts"
TTF_OUT = SRC / "fonts-ttf"
DOCS = ROOT / "docs"
LOGO_URL = "https://www.dennyschulz.de/logo.png"
UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36"
# Old Safari advertises truetype-only support, so Google serves real .ttf
# files (the MSIE 6 UA returns EOT, which PIL/FreeType cannot read).
@@ -81,22 +88,26 @@ def try_fetch(url: str, *, ua: str = UA) -> bytes | None:
def fetch_images() -> None:
manifest = json.loads((ROOT / "src" / "images.json").read_text(encoding="utf-8"))
base = manifest["pool_base"].rstrip("/")
legacy_base = manifest["legacy_base"].rstrip("/")
suffixes = (".webp", ".src", ".jpg", ".jpeg", ".png")
ok = skipped = 0
for entry in manifest["images"]:
slug, raw = entry["slug"], entry["raw"]
rel = raw.replace("\\", "/")
target = IMG_OUT / f"{slug}.webp"
source = IMG_OUT / f"{slug}.src"
if target.exists() or source.exists():
if any((IMG_OUT / f"{slug}{suffix}").exists() for suffix in suffixes):
skipped += 1
continue
data = try_fetch(f"{base}/_derived/web/{rel}.webp")
if data is None:
print(f" [!] web variant missing, falling back to original: {rel}")
data = fetch(f"{base}/{rel}")
source.write_bytes(data)
if entry.get("source") == "legacy":
data = fetch(f"{legacy_base}/{rel}")
(IMG_OUT / f"{slug}.jpg").write_bytes(data)
else:
target.write_bytes(data)
data = try_fetch(f"{base}/_derived/web/{rel}.webp")
if data is None:
print(f" [!] web variant missing, falling back to original: {rel}")
data = fetch(f"{base}/{rel}")
(IMG_OUT / f"{slug}.src").write_bytes(data)
else:
(IMG_OUT / f"{slug}.webp").write_bytes(data)
ok += 1
print(f" [ok] {slug} ({len(data) / 1024:.0f} KiB)")
print(f"images: {ok} fetched, {skipped} already present")
@@ -110,6 +121,18 @@ def fetch_images() -> None:
print(f" [ok] video poster ({len(data) / 1024:.0f} KiB)")
def fetch_brand() -> None:
"""The operator's original logo (used for the brand mark and all icons)."""
BRAND_OUT.mkdir(parents=True, exist_ok=True)
target = BRAND_OUT / "logo.png"
if target.exists():
print(f"brand: logo already present ({target.stat().st_size / 1024:.0f} KiB)")
return
data = fetch(LOGO_URL)
target.write_bytes(data)
print(f" [ok] logo ({len(data) / 1024:.0f} KiB)")
def fetch_fonts() -> None:
FONT_OUT.mkdir(parents=True, exist_ok=True)
for family, spec in FAMILIES.items():
@@ -160,8 +183,10 @@ def fetch_fonts() -> None:
def main() -> int:
for path in (IMG_OUT, FONT_OUT, DOCS):
for path in (IMG_OUT, BRAND_OUT, FONT_OUT, DOCS):
path.mkdir(parents=True, exist_ok=True)
print("== brand ==")
fetch_brand()
print("== images ==")
fetch_images()
print("== fonts ==")