148 lines
4.4 KiB
Python
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())
|