Files
honeyDueKMP/scripts/extract_ios_assets.py

148 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""Inventory iOS Asset Catalogs (imagesets + appiconsets) for parity tracking.
Scans both production asset catalogs:
- iosApp/iosApp/Assets.xcassets/
- iosApp/HoneyDue/Assets.xcassets/
Skips build/DerivedData output (PostHog examples etc.).
Output schema:
{
"image_sets": [
{"name": "outline", "path": "...", "files": ["outline.pdf"], "format": "pdf"},
...
],
"app_icons": [
{"name": "AppIcon", "path": "...", "sizes": ["1024x1024", ...]}
],
"widget_assets": [
{ ...same shape as image_sets... }
]
}
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
MAIN_XCASSETS = REPO_ROOT / "iosApp" / "iosApp" / "Assets.xcassets"
WIDGET_XCASSETS = REPO_ROOT / "iosApp" / "HoneyDue" / "Assets.xcassets"
OUT_PATH = REPO_ROOT / "docs" / "ios-parity" / "assets.json"
_IMAGE_EXTS = {".pdf", ".png", ".jpg", ".jpeg", ".svg", ".heic"}
def infer_format(files: list[str]) -> str:
exts = {Path(f).suffix.lower().lstrip(".") for f in files}
image_exts = exts & {"pdf", "png", "jpg", "jpeg", "svg", "heic"}
if not image_exts:
return "unknown"
if len(image_exts) == 1:
return next(iter(image_exts))
return "mixed(" + ",".join(sorted(image_exts)) + ")"
def list_asset_files(dir_path: Path) -> list[str]:
out: list[str] = []
for entry in sorted(dir_path.iterdir()):
if entry.is_file() and entry.suffix.lower() in _IMAGE_EXTS:
out.append(entry.name)
return out
def describe_imageset(imageset_dir: Path) -> dict[str, Any]:
name = imageset_dir.name[: -len(".imageset")]
files = list_asset_files(imageset_dir)
return {
"name": name,
"path": str(imageset_dir.relative_to(REPO_ROOT)),
"files": files,
"format": infer_format(files),
}
def describe_appicon(appicon_dir: Path) -> dict[str, Any]:
name = appicon_dir.name[: -len(".appiconset")]
contents = appicon_dir / "Contents.json"
sizes: list[str] = []
files = list_asset_files(appicon_dir)
if contents.is_file():
try:
data = json.loads(contents.read_text(encoding="utf-8"))
except json.JSONDecodeError:
data = {}
for image in data.get("images", []):
size = image.get("size")
scale = image.get("scale")
idiom = image.get("idiom")
if size:
label = size
if scale:
label = f"{label}@{scale}"
if idiom:
label = f"{label} ({idiom})"
sizes.append(label)
return {
"name": name,
"path": str(appicon_dir.relative_to(REPO_ROOT)),
"sizes": sizes,
"files": files,
}
def walk_catalog(root: Path) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
imagesets: list[dict[str, Any]] = []
appicons: list[dict[str, Any]] = []
if not root.is_dir():
return imagesets, appicons
for dirpath, dirnames, _ in root.walk() if hasattr(root, "walk") else _walk(root):
p = Path(dirpath)
if p.name.endswith(".imageset"):
imagesets.append(describe_imageset(p))
dirnames[:] = [] # don't recurse inside
elif p.name.endswith(".appiconset"):
appicons.append(describe_appicon(p))
dirnames[:] = []
imagesets.sort(key=lambda x: x["name"])
appicons.sort(key=lambda x: x["name"])
return imagesets, appicons
def _walk(root: Path):
"""Fallback walker for Python < 3.12 where Path.walk is unavailable."""
import os
for dirpath, dirnames, filenames in os.walk(root):
yield dirpath, dirnames, filenames
def main() -> int:
main_images, main_icons = walk_catalog(MAIN_XCASSETS)
widget_images, widget_icons = walk_catalog(WIDGET_XCASSETS)
output = {
"image_sets": main_images,
"app_icons": main_icons + widget_icons,
"widget_assets": widget_images,
}
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
with OUT_PATH.open("w", encoding="utf-8") as f:
json.dump(output, f, indent=2)
f.write("\n")
print(f"[extract_ios_assets] wrote {OUT_PATH}")
print(
f" image_sets={len(output['image_sets'])} "
f"app_icons={len(output['app_icons'])} "
f"widget_assets={len(output['widget_assets'])}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())