v0.6.9: stop serving match-template crops as hero portraits.

Export and OSS upload require wide Heroes-page cards only; bump cache stamp to wide2 so clients drop polluted squares.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-30 23:24:42 +08:00
co-authored by Cursor
parent ee180f3519
commit a257f96d94
7 changed files with 85 additions and 19 deletions
+46
View File
@@ -84,6 +84,51 @@ def _build_staging() -> tuple[Path, dict[str, int]]:
return staging, counts
def _png_size(path: Path) -> tuple[int, int] | None:
"""Return (width, height) for a PNG, or None if unreadable."""
try:
raw = path.read_bytes()
except OSError:
return None
if len(raw) < 24 or raw[:8] != b"\x89PNG\r\n\x1a\n":
return None
# IHDR: length(4) + type(4) + width(4) + height(4)
if raw[12:16] != b"IHDR":
return None
w = int.from_bytes(raw[16:20], "big")
h = int.from_bytes(raw[20:24], "big")
return w, h
def _assert_wide_portraits(staging: Path) -> None:
"""Refuse top-bar match crops (square ~96×96) before any OSS put.
Official cards are usually 256×144; Steam occasionally ships half-res
128×72 landscape (still fine for the grid). Match templates are square.
"""
portrait_dir = staging / "portrait"
if not portrait_dir.is_dir():
raise SystemExit("staging missing portrait/ — run fetch_hero_portraits.py")
bad: list[str] = []
for path in sorted(portrait_dir.glob("*.png")):
size = _png_size(path)
if size is None:
bad.append(f"{path.name}: unreadable")
continue
w, h = size
# Landscape Heroes cards: aspect ≈ 16:9. Match CDN faces are square.
if h <= 0 or w / h < 1.4:
bad.append(f"{path.name}: {w}x{h}")
if bad:
sample = "; ".join(bad[:6])
more = f" (+{len(bad) - 6} more)" if len(bad) > 6 else ""
raise SystemExit(
f"refusing to upload {len(bad)} non-wide portrait(s) "
f"(need landscape aspect ≥1.4, not match-template squares): "
f"{sample}{more}. Run: python web/fetch_hero_portraits.py"
)
def _iter_files(root: Path) -> list[Path]:
files: list[Path] = []
for sub in ASSET_DIRS:
@@ -97,6 +142,7 @@ def _iter_files(root: Path) -> list[Path]:
def upload(bucket_name: str, *, force: bool = False) -> None:
staging, counts = _build_staging()
try:
_assert_wide_portraits(staging)
files = _iter_files(staging)
total_bytes = sum(f.stat().st_size for f in files)
print(f"staging {staging} ({len(files)} files, {total_bytes / 1e6:.1f} MB)")