Apptag/Assets.xcassets/AppIcon.appiconset/Contents.json
New file @@ -0,0 +1,68 @@ { "images" : [ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } Apptag/Assets.xcassets/AppIcon.appiconset/icon_1024_preview.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_128x128.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_16x16.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_256x256.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_32x32.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_512x512.pngApptag/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.pngRelease/AppStore-7.6.0-20260527.0124/APP_STORE_ASSET_INVENTORY.md
@@ -1,17 +1,19 @@ # App Store Asset Inventory This release pack is frozen for TagLauncher `7.6.0` build `20260527.0124`. This release pack was originally frozen for TagLauncher `7.6.0` build `20260527.0124`. 2026-05-29 icon hotfix: the local QA DMG and App icon assets were regenerated from the current icon-resource hotfix branch to remove opaque/removed icon resources from the project tree. Use the refreshed hashes below for the local QA package. Use this file as the submission asset checklist. Anything marked `Needs user input` must be completed before submitting in App Store Connect. ## Frozen Build ## Local QA Build - Version: `7.6.0` - Local QA build: `20260527.0124` - Source ref: `appstore-7.6.0-20260527.0124` - Local QA build: `20260529.0153` - Source ref: `codex/post-freeze-new-requirement` icon-resource hotfix - Local QA DMG: `Archive/TagLauncher-7.6.0-20260527.0124-local-QA.dmg` - DMG SHA-256: `1952295e0169853c46572e6226c72adc43059cb20dd7ee9b161e28e42887f5a2` - App binary SHA-256: `b31270ee22d8c4c74e85b00ea96d0c378e5762f41432b057b653896566ba2f62` - DMG SHA-256: `a887c26943136c629bf36a577ded512f85a0aac8132622e2c418c4c6d2b870f7` - App binary SHA-256: `8e0fd85ee62b6791940ce32c220ca802a02f5e6f9d43a366f6d1f6b7bb53eb9a` ## Metadata Release/AppStore-7.6.0-20260527.0124/Archive/TagLauncher-7.6.0-20260527.0124-local-QA.dmgBinary files differ
Release/AppStore-7.6.0-20260527.0124/Assets/TagLauncher-AppIcon-1024.pngRelease/AppStore-7.6.0-20260527.0124/BUILD_SIGN_UPLOAD.md
@@ -4,10 +4,10 @@ The local QA build is suitable for verification and direct local testing, but it is not an App Store upload artifact. - Frozen local QA build: `20260527.0124` - Frozen source ref: `appstore-7.6.0-20260527.0124` - Frozen DMG SHA-256: `1952295e0169853c46572e6226c72adc43059cb20dd7ee9b161e28e42887f5a2` - Frozen app binary SHA-256: `b31270ee22d8c4c74e85b00ea96d0c378e5762f41432b057b653896566ba2f62` - Local QA build: `20260529.0153` - Source ref: `codex/post-freeze-new-requirement` icon-resource hotfix - DMG SHA-256: `a887c26943136c629bf36a577ded512f85a0aac8132622e2c418c4c6d2b870f7` - App binary SHA-256: `8e0fd85ee62b6791940ce32c220ca802a02f5e6f9d43a366f6d1f6b7bb53eb9a` - Local build command: `bash build.sh` - Local DMG command: `bash make_dmg.sh` - App path: `build/TagLauncher.app` Release/AppStore-7.6.0-20260527.0124/QA_RELEASE_EVIDENCE.md
@@ -3,18 +3,20 @@ ## Local Build - Version: `7.6.0` - Build: `20260527.0124` - Frozen source ref: `appstore-7.6.0-20260527.0124` - Build: `20260529.0153` - Source ref: `codex/post-freeze-new-requirement` icon-resource hotfix - App path: `/Users/ar/Projects/Taglauncher/build/TagLauncher.app` - DMG path: `/Users/ar/Projects/Taglauncher/build/TagLauncher.dmg` - App executable size: `1.4M` - DMG size: `5.7M` 2026-05-29 icon hotfix: the local QA DMG and App icon assets were regenerated to remove opaque/removed icon resources from the project tree. ## Hashes ```text 1952295e0169853c46572e6226c72adc43059cb20dd7ee9b161e28e42887f5a2 build/TagLauncher.dmg b31270ee22d8c4c74e85b00ea96d0c378e5762f41432b057b653896566ba2f62 build/TagLauncher.app/Contents/MacOS/TagLauncher a887c26943136c629bf36a577ded512f85a0aac8132622e2c418c4c6d2b870f7 build/TagLauncher.dmg 8e0fd85ee62b6791940ce32c220ca802a02f5e6f9d43a366f6d1f6b7bb53eb9a build/TagLauncher.app/Contents/MacOS/TagLauncher ``` ## Commands Run @@ -24,6 +26,7 @@ bash make_dmg.sh codesign --verify --deep --strict --verbose=2 build/TagLauncher.app hdiutil verify build/TagLauncher.dmg python3 generate_icon.py ``` ## Window Logic QA Result Release/AppStore-7.6.0-20260527.0124/README.md
@@ -8,16 +8,16 @@ - Platform: macOS / Mac App Store - Bundle ID: `com.taglauncher.app` - Version: `7.6.0` - Local QA build: `20260527.0124` - Frozen source ref: `appstore-7.6.0-20260527.0124` - Frozen source branch: `main` - Local QA build: `20260529.0153` - Source ref: `codex/post-freeze-new-requirement` icon-resource hotfix - Source branch: `codex/post-freeze-new-requirement` - Release folder: `Release/AppStore-7.6.0-20260527.0124` - DMG SHA-256: `1952295e0169853c46572e6226c72adc43059cb20dd7ee9b161e28e42887f5a2` - App binary SHA-256: `b31270ee22d8c4c74e85b00ea96d0c378e5762f41432b057b653896566ba2f62` - DMG SHA-256: `a887c26943136c629bf36a577ded512f85a0aac8132622e2c418c4c6d2b870f7` - App binary SHA-256: `8e0fd85ee62b6791940ce32c220ca802a02f5e6f9d43a366f6d1f6b7bb53eb9a` ## Freeze Status This release pack is frozen on the locally verified build `20260527.0124`. Use this folder, its hashes, and its metadata as the source of truth for the first Mac App Store submission prep. This release pack was originally frozen on the locally verified build `20260527.0124`. On 2026-05-29 the local QA DMG and App icon assets were refreshed to remove opaque/removed icon resources from the project tree. Use the hashes above for the refreshed local QA package. Do not use earlier prep folders or builds such as `20260524.1652` or `20260524.1849`; they predate the final fullscreen/Split View secondary-window close and same-app Split View fixes. Release/_archive/AppStore-7.6.0-20260525.0016/APP_STORE_ASSET_INVENTORY.md
@@ -1,17 +1,19 @@ # App Store Asset Inventory This release pack is frozen for TagLauncher `7.6.0` build `20260525.0016`. This release pack was originally frozen for TagLauncher `7.6.0` build `20260525.0016`. 2026-05-29 icon cleanup: the local QA DMG and App icon asset in this archived folder were refreshed from the current icon-resource hotfix build so the project tree no longer contains stale icon packages. This archive is reference-only and should not be used for submission. Use this file as the submission asset checklist. Anything marked `Needs user input` must be completed before submitting in App Store Connect. ## Frozen Build ## Archived Build - Version: `7.6.0` - Local QA build: `20260525.0016` - Source commit: `0258b3e` - Local QA build: `20260529.0153` icon cleanup copy - Source commit: `codex/post-freeze-new-requirement` icon-resource hotfix - Local QA DMG: `Archive/TagLauncher-7.6.0-20260525.0016-local-QA.dmg` - DMG SHA-256: `4689856f4a976c394ef4737db5bb5eebcc5b5bb95eea81fe72f5b6c3e3ed4b23` - App binary SHA-256: `01ace57df10061fd7d684fa8a5bd795f87f55ee2c713cbb1ab2d01f5f63ed404` - DMG SHA-256: `a887c26943136c629bf36a577ded512f85a0aac8132622e2c418c4c6d2b870f7` - App binary SHA-256: `8e0fd85ee62b6791940ce32c220ca802a02f5e6f9d43a366f6d1f6b7bb53eb9a` ## Metadata Release/_archive/AppStore-7.6.0-20260525.0016/Archive/TagLauncher-7.6.0-20260525.0016-local-QA.dmgBinary files differ
Release/_archive/AppStore-7.6.0-20260525.0016/Assets/TagLauncher-AppIcon-1024.pngRelease/_archive/AppStore-7.6.0-20260525.0016/BUILD_SIGN_UPLOAD.md
@@ -4,10 +4,10 @@ The local QA build is suitable for verification and direct local testing, but it is not an App Store upload artifact. - Frozen local QA build: `20260525.0016` - Frozen source commit: `0258b3e` - Frozen DMG SHA-256: `4689856f4a976c394ef4737db5bb5eebcc5b5bb95eea81fe72f5b6c3e3ed4b23` - Frozen app binary SHA-256: `01ace57df10061fd7d684fa8a5bd795f87f55ee2c713cbb1ab2d01f5f63ed404` - Local QA build: `20260529.0153` icon cleanup copy - Source ref: `codex/post-freeze-new-requirement` icon-resource hotfix - DMG SHA-256: `a887c26943136c629bf36a577ded512f85a0aac8132622e2c418c4c6d2b870f7` - App binary SHA-256: `8e0fd85ee62b6791940ce32c220ca802a02f5e6f9d43a366f6d1f6b7bb53eb9a` - Local build command: `bash build.sh` - Local DMG command: `bash make_dmg.sh` - App path: `build/TagLauncher.app` Release/_archive/AppStore-7.6.0-20260525.0016/QA_RELEASE_EVIDENCE.md
@@ -3,18 +3,20 @@ ## Local Build - Version: `7.6.0` - Build: `20260525.0016` - Frozen source commit: `0258b3e` - Build: `20260529.0153` icon cleanup copy - Source ref: `codex/post-freeze-new-requirement` icon-resource hotfix - App path: `/Users/ar/Projects/Taglauncher/build/TagLauncher.app` - DMG path: `/Users/ar/Projects/Taglauncher/build/TagLauncher.dmg` - App executable size: `1.4M` - DMG size: `5.7M` 2026-05-29 icon cleanup: the local QA DMG and App icon asset in this archived folder were refreshed so the project tree no longer contains stale icon packages. This archive is reference-only and should not be used for submission. ## Hashes ```text 4689856f4a976c394ef4737db5bb5eebcc5b5bb95eea81fe72f5b6c3e3ed4b23 build/TagLauncher.dmg 01ace57df10061fd7d684fa8a5bd795f87f55ee2c713cbb1ab2d01f5f63ed404 build/TagLauncher.app/Contents/MacOS/TagLauncher a887c26943136c629bf36a577ded512f85a0aac8132622e2c418c4c6d2b870f7 build/TagLauncher.dmg 8e0fd85ee62b6791940ce32c220ca802a02f5e6f9d43a366f6d1f6b7bb53eb9a build/TagLauncher.app/Contents/MacOS/TagLauncher ``` ## Commands Run Release/_archive/AppStore-7.6.0-20260525.0016/README.md
@@ -8,16 +8,16 @@ - Platform: macOS / Mac App Store - Bundle ID: `com.taglauncher.app` - Version: `7.6.0` - Local QA build: `20260525.0016` - Frozen source commit: `0258b3e` - Frozen source branch: `main` - Local QA build: `20260529.0153` icon cleanup copy - Source ref: `codex/post-freeze-new-requirement` icon-resource hotfix - Source branch: `codex/post-freeze-new-requirement` - Release folder: `Release/AppStore-7.6.0-20260525.0016` - DMG SHA-256: `4689856f4a976c394ef4737db5bb5eebcc5b5bb95eea81fe72f5b6c3e3ed4b23` - App binary SHA-256: `01ace57df10061fd7d684fa8a5bd795f87f55ee2c713cbb1ab2d01f5f63ed404` - DMG SHA-256: `a887c26943136c629bf36a577ded512f85a0aac8132622e2c418c4c6d2b870f7` - App binary SHA-256: `8e0fd85ee62b6791940ce32c220ca802a02f5e6f9d43a366f6d1f6b7bb53eb9a` ## Freeze Status This release pack is frozen on the locally QA-passed build `20260525.0016`. Use this folder, its hashes, and its metadata as the source of truth for the first Mac App Store submission prep. This archive was originally frozen on the locally QA-passed build `20260525.0016`. On 2026-05-29 its local QA DMG and App icon asset were refreshed to remove stale icon packages from the project tree. This archive is reference-only and should not be used for submission. Do not use earlier prep folders or builds such as `20260524.1652` or `20260524.1849`; they predate the final fullscreen/Split View secondary-window close and same-app Split View fixes. TODO.md
@@ -38,6 +38,12 @@ ## Done - [2026-05-29] 修复 App 图标白边并清除旧图标残留。 - 提交: 本次提交 - 结果: 当前 grid/rainbow AppIcon 的圆角外区域已改为透明;`generate_icon.py` 删除旧“两张标签”生成逻辑,并增加 grid/rainbow 图标族校验,防止旧图标被重新生成。 - 范围: 更新 `.icns`、`AppIcon.appiconset`、Release 1024 图标、Local QA DMG;`build.sh` 改为优先从 `AppIcon.appiconset` 生成 bundle `AppIcon.icns`。 - 验证: `python3 generate_icon.py`、`bash build.sh`、`codesign --verify --deep --strict`、DMG 挂载检查、全项目图片/DMG 旧图标近似匹配扫描均通过;根 `.icns`、appiconset、Release 1024 图标、bundle/DMG 内 AppIcon 四角 alpha 均为 0。 - [2026-05-29] 清理或改名 `AppGridItem.swift` 残留。 - 提交: 本次提交 - 结果: `AppGridItem.swift` 已改名为 `AppGridSupport.swift`,文件标题改为 App Grid shared support,避免误解为旧 SwiftUI 浏览态 grid。 build.sh
@@ -71,10 +71,39 @@ /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $APP_BUILD" "$APP_INFO_PLIST" echo "==> Copying AppIcon.icns..." if [ -f "$PROJECT_DIR/icon-icns.icns" ]; then APPICON_SOURCE_DIR="$SWIFT_DIR/Assets.xcassets/AppIcon.appiconset" if [ -d "$APPICON_SOURCE_DIR" ]; then TMP_ICONSET_PARENT="$(mktemp -d -t taglauncher-appicon.XXXXXX)" TMP_ICONSET="$TMP_ICONSET_PARENT/AppIcon.iconset" mkdir -p "$TMP_ICONSET" ICON_NAMES=( icon_16x16.png icon_16x16@2x.png icon_32x32.png icon_32x32@2x.png icon_128x128.png icon_128x128@2x.png icon_256x256.png icon_256x256@2x.png icon_512x512.png icon_512x512@2x.png ) for icon_name in "${ICON_NAMES[@]}"; do if [ ! -f "$APPICON_SOURCE_DIR/$icon_name" ]; then echo "❌ Missing AppIcon asset: $APPICON_SOURCE_DIR/$icon_name" exit 1 fi cp "$APPICON_SOURCE_DIR/$icon_name" "$TMP_ICONSET/$icon_name" done iconutil -c icns "$TMP_ICONSET" -o "$RESOURCES_DIR/AppIcon.icns" rm -rf "$TMP_ICONSET_PARENT" elif [ -f "$PROJECT_DIR/icon-icns.icns" ]; then cp "$PROJECT_DIR/icon-icns.icns" "$RESOURCES_DIR/AppIcon.icns" elif [ -f "$SWIFT_DIR/AppIcon.iconset/icon_512x512@2x.png" ]; then iconutil -c icns "$SWIFT_DIR/AppIcon.iconset" -o "$RESOURCES_DIR/AppIcon.icns" 2>/dev/null else echo "❌ Missing AppIcon source" exit 1 fi echo "==> Copying menu bar icon..." generate_icon.py
@@ -1,84 +1,42 @@ #!/usr/bin/env python3 """Generate TagLauncher app icon — two overlapping tags (red + blue) on macOS squircle.""" from PIL import Image, ImageDraw, ImageFilter import math, os, subprocess, tempfile """Regenerate the current TagLauncher grid/rainbow app icon assets.""" from __future__ import annotations import colorsys import subprocess import tempfile from pathlib import Path from PIL import Image, ImageChops, ImageDraw SIZE = 1024 PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = Path(__file__).resolve().parent APPICON_DIR = PROJECT_DIR / "Apptag" / "Assets.xcassets" / "AppIcon.appiconset" ROOT_ICNS = PROJECT_DIR / "icon-icns.icns" def rounded_rectangle_mask(size, radius): mask = Image.new("L", (size, size), 0) draw = ImageDraw.Draw(mask) draw.rounded_rectangle([(0, 0), (size-1, size-1)], radius=radius, fill=255) return mask RELEASE_ICON_TARGETS = [ PROJECT_DIR / "Release" / "AppStore-7.6.0-20260527.0124" / "Assets" / "TagLauncher-AppIcon-1024.png", PROJECT_DIR / "Release" / "_archive" / "AppStore-7.6.0-20260525.0016" / "Assets" / "TagLauncher-AppIcon-1024.png", ] def draw_tag(cx, cy, width, height, color, angle_deg, hole_ratio=0.22): padding = int(width * 1.5) tag_img = Image.new("RGBA", (padding*2, padding*2), (0,0,0,0)) tag_draw = ImageDraw.Draw(tag_img) x0 = padding - width//2 y0 = padding - height//2 x1 = padding + width//2 y1 = padding + height//2 r = int(min(width, height) * 0.18) tag_draw.rounded_rectangle([x0, y0, x1, y1], radius=r, fill=color) hole_r = int(width * hole_ratio) hole_x = padding + width//2 - int(width * 0.22) hole_y = padding - height//2 + int(height * 0.2) tag_draw.ellipse( [hole_x - hole_r, hole_y - hole_r, hole_x + hole_r, hole_y + hole_r], fill=(0,0,0,0) ) tag_img = tag_img.rotate(angle_deg, resample=Image.BICUBIC, expand=True) return tag_img SOURCE_CANDIDATES = [ APPICON_DIR / "icon_512x512@2x.png", RELEASE_ICON_TARGETS[0], ROOT_ICNS, ] # --- Build the icon --- img = Image.new("RGBA", (SIZE, SIZE), (0,0,0,0)) squircle_r = int(SIZE * 0.225) bg_mask = rounded_rectangle_mask(SIZE, squircle_r) bg_color = Image.new("RGBA", (SIZE, SIZE), (240, 241, 245, 255)) img = Image.composite(bg_color, img, bg_mask) red_color = (220, 45, 85, 255) blue_color = (45, 130, 220, 255) tag_w = int(SIZE * 0.42) tag_h = int(SIZE * 0.55) red_tag = draw_tag(0, 0, tag_w, tag_h, red_color, -8) red_x = SIZE//2 - red_tag.width//2 red_y = SIZE//2 - red_tag.height//2 - int(SIZE * 0.04) tag_w2 = int(SIZE * 0.46) tag_h2 = int(SIZE * 0.58) blue_tag = draw_tag(0, 0, tag_w2, tag_h2, blue_color, 12) blue_x = SIZE//2 - blue_tag.width//2 + int(SIZE * 0.02) blue_y = SIZE//2 - blue_tag.height//2 + int(SIZE * 0.03) # Drop shadows shadow = blue_tag.copy() shadow_data = shadow.getdata() shadow.putdata([(0,0,0, min(a//4, 80)) if a > 0 else (0,0,0,0) for (r,g,b,a) in shadow_data]) shadow = shadow.filter(ImageFilter.GaussianBlur(radius=SIZE*0.025)) img.paste(shadow, (blue_x+int(SIZE*0.015), blue_y+int(SIZE*0.02)), shadow) img.paste(blue_tag, (blue_x, blue_y), blue_tag) shadow2 = red_tag.copy() shadow_data2 = shadow2.getdata() shadow2.putdata([(0,0,0, min(a//3, 80)) if a > 0 else (0,0,0,0) for (r,g,b,a) in shadow_data2]) shadow2 = shadow2.filter(ImageFilter.GaussianBlur(radius=SIZE*0.02)) img.paste(shadow2, (red_x+int(SIZE*0.01), red_y+int(SIZE*0.015)), shadow2) img.paste(red_tag, (red_x, red_y), red_tag) mask = rounded_rectangle_mask(SIZE, squircle_r) img.putalpha(mask) # --- Save PNG variants to temp .iconset, then generate .icns --- with tempfile.TemporaryDirectory() as tmpdir: iconset = os.path.join(tmpdir, "AppIcon.iconset") os.makedirs(iconset) variants = [ VARIANTS = [ ("icon_16x16.png", 16), ("icon_16x16@2x.png", 32), ("icon_32x32.png", 32), @@ -91,19 +49,135 @@ ("icon_512x512@2x.png", 1024), ] for name, size in variants: resized = img.resize((size, size), Image.LANCZOS) path = os.path.join(iconset, name) # The approved grid/rainbow icon artwork sits inside this rounded rect. # Keeping this mask explicit prevents the old opaque-white square corners from # reappearing when assets are regenerated from an older flattened source. MASK_BOUNDS = (77, 57, 943, 934) MASK_RADIUS = 232 def load_source() -> tuple[Path, Image.Image]: for path in SOURCE_CANDIDATES: if not path.exists(): continue with Image.open(path) as image: image.load() source = image.convert("RGBA").resize((SIZE, SIZE), Image.LANCZOS) return path, source raise FileNotFoundError("No app icon source found.") def assert_current_grid_rainbow_icon(image: Image.Image) -> None: """Reject the removed two-tag icon if it is accidentally used as source.""" bands = {"yellow": 0, "green": 0, "cyan": 0, "purple": 0} pixels = image.load() for y in range(120, 900, 4): for x in range(520, 900, 4): red, green, blue, alpha = pixels[x, y] if alpha < 128: continue hue, saturation, value = colorsys.rgb_to_hsv( red / 255.0, green / 255.0, blue / 255.0 ) if saturation < 0.30 or value < 0.35: continue if 0.10 <= hue <= 0.19: bands["yellow"] += 1 elif 0.20 <= hue <= 0.42: bands["green"] += 1 elif 0.43 <= hue <= 0.58: bands["cyan"] += 1 elif 0.70 <= hue <= 0.86: bands["purple"] += 1 present_bands = [name for name, count in bands.items() if count >= 100] if len(present_bands) < 3: raise ValueError( "Source image does not match the current grid/rainbow icon family; " f"detected color bands: {bands}" ) def icon_mask() -> Image.Image: x0, y0, x1, y1 = MASK_BOUNDS mask = Image.new("L", (SIZE, SIZE), 0) draw = ImageDraw.Draw(mask) draw.rounded_rectangle( [x0, y0, x1 - 1, y1 - 1], radius=MASK_RADIUS, fill=255, ) return mask def normalize_icon(image: Image.Image) -> Image.Image: image = image.resize((SIZE, SIZE), Image.LANCZOS).convert("RGBA") alpha = ImageChops.multiply(image.getchannel("A"), icon_mask()) normalized = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) normalized.paste(image, (0, 0), alpha) normalized.putalpha(alpha) return normalized def save_png(path: Path, image: Image.Image, size: int) -> None: path.parent.mkdir(parents=True, exist_ok=True) resized = image.resize((size, size), Image.LANCZOS) resized.save(path, "PNG", optimize=True) print(f" {name} ({size}x{size}): {os.path.getsize(path):,}B") print(f" {path.relative_to(PROJECT_DIR)} ({size}x{size})") # Generate .icns to project root out_icns = os.path.join(PROJECT_DIR, "icon-icns.icns") subprocess.run(["iconutil", "-c", "icns", iconset, "-o", out_icns], check=True) icns_size = os.path.getsize(out_icns) print(f"\nGenerated: {out_icns} ({icns_size:,}B, {icns_size/1024:.0f}KB)") # Also save 1024x1024 preview preview_path = os.path.join(PROJECT_DIR, "icon_preview.png") img.save(preview_path, "PNG", optimize=True) print(f"Preview: {preview_path}") def generate_icns(image: Image.Image) -> None: with tempfile.TemporaryDirectory() as temp_dir: iconset = Path(temp_dir) / "AppIcon.iconset" iconset.mkdir() for name, size in VARIANTS: image.resize((size, size), Image.LANCZOS).save(iconset / name, "PNG") subprocess.run( ["iconutil", "-c", "icns", str(iconset), "-o", str(ROOT_ICNS)], check=True, ) print(f" {ROOT_ICNS.relative_to(PROJECT_DIR)}") def print_corner_alpha(path: Path) -> None: with Image.open(path) as image: rgba = image.convert("RGBA") width, height = rgba.size corners = [ rgba.getpixel((0, 0))[3], rgba.getpixel((width - 1, 0))[3], rgba.getpixel((0, height - 1))[3], rgba.getpixel((width - 1, height - 1))[3], ] print(f" alpha corners {path.relative_to(PROJECT_DIR)}: {corners}") def main() -> None: source_path, source = load_source() assert_current_grid_rainbow_icon(source) icon = normalize_icon(source) print(f"Source: {source_path.relative_to(PROJECT_DIR)}") print("Writing AppIcon.appiconset:") save_png(APPICON_DIR / "icon_1024_preview.png", icon, 1024) for name, size in VARIANTS: save_png(APPICON_DIR / name, icon, size) print("Writing release 1024 assets:") for target in RELEASE_ICON_TARGETS: save_png(target, icon, 1024) print("Writing root .icns:") generate_icns(icon) print("Validation:") print_corner_alpha(ROOT_ICNS) print_corner_alpha(APPICON_DIR / "icon_512x512@2x.png") for target in RELEASE_ICON_TARGETS: print_corner_alpha(target) if __name__ == "__main__": main() icon-icns.icnsBinary files differ