Skip to content

Verify_assets

FieldValue
TypeSkill Resource
Source~/.copilot/skills/generators/scripts/verify_assets.py
DescriptionNot specified

Source Content

#!/usr/bin/env python3
"""
verify_assets.py — standalone auditor for a directory of web assets produced
by generate_favicons.py / generate_og_images.py.
Unlike --validate (which only checks files as they're generated in the same
run), this script re-audits an EXISTING output directory after the fact:
right files present, correct dimensions, correct format, sane file sizes,
and naming conventions match what the skill's HTML tags expect.
Usage:
verify_assets.py <dir> [--set favicons|og|all]
Exit code is non-zero if any hard check fails. Falls back gracefully across
three inspection methods, in order:
1. Pillow (PIL) — exact dimensions + format, if installed
2. `identify` (ImageMagick) — exact dimensions + format, if on PATH
3. raw magic-byte / PNG IHDR parsing (stdlib only) — format + dimensions,
always available, used if neither of the above is present
No required check is ever skipped outright: dimension/format verification
always runs via the stdlib fallback, even with zero third-party tools.
"""
import argparse
import struct
import subprocess
import sys
from pathlib import Path
FAIL = 0
WARN_COUNT = 0
def ok(msg):
print(f"✅ {msg}")
def warn(msg):
global WARN_COUNT
print(f"⚠️ {msg}")
WARN_COUNT += 1
def error(msg):
global FAIL
print(f"❌ {msg}")
FAIL = 1
# Expected output sets, mirroring generate_favicons.py / generate_og_images.py.
FAVICON_FILES = {
"favicon-16x16.png": (16, 16, "png"),
"favicon-32x32.png": (32, 32, "png"),
"favicon-96x96.png": (96, 96, "png"),
"favicon.ico": (None, None, "ico"),
"apple-touch-icon.png": (180, 180, "png"),
"android-chrome-192x192.png": (192, 192, "png"),
"android-chrome-512x512.png": (512, 512, "png"),
}
OG_FILES = {
"og-image.png": (1200, 630, "png"),
"twitter-image.png": (1200, 675, "png"),
"og-square.png": (1200, 1200, "png"),
}
# Platform file-size ceilings (bytes) — mirrors lib/validators.py PLATFORM_REQUIREMENTS.
MAX_OG_BYTES = 8 * 1024 * 1024 # 8MB (Facebook/WhatsApp ceiling; also covers Twitter/LinkedIn's 5MB)
MAX_ICON_BYTES = 200 * 1024 # generous ceiling — an icon this large signals an unoptimized export
def try_pillow_dims(path):
try:
from PIL import Image
except ImportError:
return None
try:
with Image.open(path) as im:
return (im.width, im.height, im.format.lower() if im.format else None)
except Exception:
return None
def try_imagemagick_dims(path):
if not _which("identify"):
return None
try:
out = subprocess.run(
["identify", "-format", "%w %h %m", str(path)],
capture_output=True, text=True, timeout=10,
)
if out.returncode != 0:
return None
parts = out.stdout.strip().split()
if len(parts) < 3:
return None
w, h, fmt = int(parts[0]), int(parts[1]), parts[2].lower()
return (w, h, fmt)
except Exception:
return None
def _which(cmd):
import shutil
return shutil.which(cmd) is not None
def raw_png_dims(path):
"""Read width/height straight out of the PNG IHDR chunk. stdlib only."""
with open(path, "rb") as f:
header = f.read(24)
if len(header) < 24 or header[:8] != b"\x89PNG\r\n\x1a\n":
return None
width, height = struct.unpack(">II", header[16:24])
return (width, height, "png")
def raw_ico_valid(path):
"""Confirm the ICO magic header and at least one embedded image directory entry."""
with open(path, "rb") as f:
header = f.read(6)
if len(header) < 6:
return False
reserved, img_type, count = struct.unpack("<HHH", header)
return reserved == 0 and img_type == 1 and count >= 1
def inspect_image(path):
"""Return (width, height, format) using the best available method, or None."""
dims = try_pillow_dims(path)
if dims:
return dims, "Pillow"
dims = try_imagemagick_dims(path)
if dims:
return dims, "ImageMagick identify"
if path.suffix.lower() == ".png":
dims = raw_png_dims(path)
if dims:
return dims, "raw PNG IHDR (stdlib)"
return None, None
def check_set(out_dir, expected, label, max_bytes):
print(f"\n== {label} ==")
for filename, (want_w, want_h, want_fmt) in expected.items():
path = out_dir / filename
if not path.exists():
error(f"missing expected file: {filename}")
continue
size_bytes = path.stat().st_size
if size_bytes == 0:
error(f"{filename}: zero-byte file")
continue
if size_bytes > max_bytes:
error(f"{filename}: {size_bytes:,} bytes exceeds the {max_bytes:,}-byte ceiling")
else:
ok(f"{filename}: {size_bytes:,} bytes (within ceiling)")
if want_fmt == "ico":
if raw_ico_valid(path):
ok(f"{filename}: valid ICO container")
else:
error(f"{filename}: does not look like a valid ICO file (bad magic header)")
continue
dims, method = inspect_image(path)
if dims is None:
warn(f"{filename}: could not determine dimensions/format (no Pillow, no ImageMagick, non-PNG stdlib fallback unavailable)")
continue
w, h, fmt = dims
if want_w is not None and (w, h) != (want_w, want_h):
error(f"{filename}: is {w}x{h}, expected {want_w}x{want_h} (via {method})")
else:
ok(f"{filename}: {w}x{h} confirmed (via {method})")
if fmt and want_fmt and fmt != want_fmt:
error(f"{filename}: format is {fmt}, expected {want_fmt}")
def check_naming_conventions(out_dir):
print("\n== naming conventions ==")
bad_names = []
for p in out_dir.glob("*"):
if not p.is_file():
continue
name = p.name
if name.startswith(".") or name in ("site.webmanifest",):
continue
if p.suffix.lower() not in (".png", ".ico", ".jpg", ".jpeg", ".webp"):
continue
if name != name.lower():
bad_names.append(name)
if " " in name:
bad_names.append(name)
if bad_names:
for n in sorted(set(bad_names)):
error(f"non-conforming filename (must be lowercase, no spaces): {n}")
else:
ok("all asset filenames are lowercase with no spaces")
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("dir", help="Directory containing generated web assets (e.g. public/)")
parser.add_argument("--set", choices=["favicons", "og", "all"], default="all",
help="Which expected file set to check (default: all)")
args = parser.parse_args()
out_dir = Path(args.dir)
if not out_dir.is_dir():
print(f"Usage error: {out_dir} is not a directory", file=sys.stderr)
sys.exit(2)
print(f"== Verifying web assets in {out_dir} ==")
if not _which("identify"):
try:
import PIL # noqa: F401
except ImportError:
warn("neither Pillow nor ImageMagick 'identify' found — falling back to raw PNG IHDR parsing (stdlib); .jpg/.webp dimension checks will be skipped")
if args.set in ("favicons", "all"):
check_set(out_dir, FAVICON_FILES, "favicon / app-icon set", MAX_ICON_BYTES)
if args.set in ("og", "all"):
check_set(out_dir, OG_FILES, "Open Graph / social image set", MAX_OG_BYTES)
check_naming_conventions(out_dir)
print()
if FAIL == 0:
print(f"All checks passed ({WARN_COUNT} warning(s)).")
else:
print("One or more checks failed — see ❌ lines above.")
sys.exit(FAIL)
if __name__ == "__main__":
main()