P0.1: iOS reference artifacts (colors, assets, screens inventory)
This commit is contained in:
147
scripts/extract_ios_assets.py
Normal file
147
scripts/extract_ios_assets.py
Normal file
@@ -0,0 +1,147 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user