Ariver
2026-05-29 844e2f2459bb31d6903f1010f694089edabfd8ec
Fix app icon transparency
27 files modified
1 files added
444 ■■■■ changed files
Apptag/Assets.xcassets/AppIcon.appiconset/Contents.json 68 ●●●●● patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_1024_preview.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_128x128.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_16x16.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_256x256.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_32x32.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_512x512.png patch | view | raw | blame | history
Apptag/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png patch | view | raw | blame | history
Release/AppStore-7.6.0-20260527.0124/APP_STORE_ASSET_INVENTORY.md 14 ●●●●● patch | view | raw | blame | history
Release/AppStore-7.6.0-20260527.0124/Archive/TagLauncher-7.6.0-20260527.0124-local-QA.dmg patch | view | raw | blame | history
Release/AppStore-7.6.0-20260527.0124/Assets/TagLauncher-AppIcon-1024.png patch | view | raw | blame | history
Release/AppStore-7.6.0-20260527.0124/BUILD_SIGN_UPLOAD.md 8 ●●●● patch | view | raw | blame | history
Release/AppStore-7.6.0-20260527.0124/QA_RELEASE_EVIDENCE.md 11 ●●●●● patch | view | raw | blame | history
Release/AppStore-7.6.0-20260527.0124/README.md 12 ●●●● patch | view | raw | blame | history
Release/_archive/AppStore-7.6.0-20260525.0016/APP_STORE_ASSET_INVENTORY.md 14 ●●●●● patch | view | raw | blame | history
Release/_archive/AppStore-7.6.0-20260525.0016/Archive/TagLauncher-7.6.0-20260525.0016-local-QA.dmg patch | view | raw | blame | history
Release/_archive/AppStore-7.6.0-20260525.0016/Assets/TagLauncher-AppIcon-1024.png patch | view | raw | blame | history
Release/_archive/AppStore-7.6.0-20260525.0016/BUILD_SIGN_UPLOAD.md 8 ●●●● patch | view | raw | blame | history
Release/_archive/AppStore-7.6.0-20260525.0016/QA_RELEASE_EVIDENCE.md 10 ●●●●● patch | view | raw | blame | history
Release/_archive/AppStore-7.6.0-20260525.0016/README.md 12 ●●●● patch | view | raw | blame | history
TODO.md 6 ●●●●● patch | view | raw | blame | history
build.sh 31 ●●●●● patch | view | raw | blame | history
generate_icon.py 250 ●●●●● patch | view | raw | blame | history
icon-icns.icns patch | view | raw | blame | history
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.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_128x128.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_16x16.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_256x256.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_32x32.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_512x512.png

Apptag/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png

Release/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.dmg
Binary files differ
Release/AppStore-7.6.0-20260527.0124/Assets/TagLauncher-AppIcon-1024.png

Release/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.dmg
Binary files differ
Release/_archive/AppStore-7.6.0-20260525.0016/Assets/TagLauncher-AppIcon-1024.png

Release/_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.icns
Binary files differ