Ariver
2026-05-30 b02c7cd28f1c48c5380bb5e3f839bdfb7615f25f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#!/usr/bin/env python3
"""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 = Path(__file__).resolve().parent
APPICON_DIR = PROJECT_DIR / "Apptag" / "Assets.xcassets" / "AppIcon.appiconset"
ROOT_ICNS = PROJECT_DIR / "icon-icns.icns"
 
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",
]
 
SOURCE_CANDIDATES = [
    APPICON_DIR / "icon_512x512@2x.png",
    RELEASE_ICON_TARGETS[0],
    ROOT_ICNS,
]
 
VARIANTS = [
    ("icon_16x16.png", 16),
    ("icon_16x16@2x.png", 32),
    ("icon_32x32.png", 32),
    ("icon_32x32@2x.png", 64),
    ("icon_128x128.png", 128),
    ("icon_128x128@2x.png", 256),
    ("icon_256x256.png", 256),
    ("icon_256x256@2x.png", 512),
    ("icon_512x512.png", 512),
    ("icon_512x512@2x.png", 1024),
]
 
# 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"  {path.relative_to(PROJECT_DIR)} ({size}x{size})")
 
 
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()