From 6f3ed4f3b8eb45734ca77916b4c96fab937cb2e2 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 31 May 2026 02:58:27 +0800
Subject: [PATCH] Prepare 7.8.7 dock policy release
---
generate_icon.py | 273 ++++++++++++++++++++++++++++++++----------------------
1 files changed, 162 insertions(+), 111 deletions(-)
diff --git a/generate_icon.py b/generate_icon.py
index 05654bb..88393fb 100644
--- a/generate_icon.py
+++ b/generate_icon.py
@@ -1,103 +1,42 @@
#!/usr/bin/env python3
-"""Generate Apptag app icon — two overlapping tags (red + blue) on macOS squircle."""
-from PIL import Image, ImageDraw, ImageFilter, ImageChops
-import math, os, subprocess
+"""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
-OUT_DIR = "/Users/ar/Projects/Apptag/Apptag/Assets.xcassets/AppIcon.appiconset"
-os.makedirs(OUT_DIR, exist_ok=True)
+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):
- """Create a mask for a rounded rectangle."""
- 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(draw, cx, cy, width, height, color, angle_deg, hole_ratio=0.22):
- """Draw a tag shape (rounded rect with hole) rotated by angle."""
- # Create a temp image for this tag
- 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) # corner radius
-
- # Main tag body
- tag_draw.rounded_rectangle([x0, y0, x1, y1], radius=r, fill=color)
-
- # Hole at top
- 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)
- )
-
- # Rotate
- tag_img = tag_img.rotate(angle_deg, resample=Image.BICUBIC, expand=True)
-
- return tag_img, padding, hole_x, hole_y
+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))
-draw = ImageDraw.Draw(img)
-
-# macOS Big Sur style squircle background
-squircle_r = int(SIZE * 0.225) # ~225px for 1024 canvas
-bg_mask = rounded_rectangle_mask(SIZE, squircle_r)
-bg_color = Image.new("RGBA", (SIZE, SIZE), (240, 241, 245, 255)) # Light gray bg
-img = Image.composite(bg_color, img, bg_mask)
-
-# Create two tags
-red_color = (220, 45, 85, 255) # Rich red/pink
-blue_color = (45, 130, 220, 255) # Vibrant blue
-
-# Red tag - slightly tilted left, on top
-tag_w = int(SIZE * 0.42)
-tag_h = int(SIZE * 0.55)
-red_tag, red_pad, _, _ = draw_tag(None, 0, 0, tag_w, tag_h, red_color, -8)
-# Position center, slightly up
-red_x = SIZE//2 - red_tag.width//2
-red_y = SIZE//2 - red_tag.height//2 - int(SIZE * 0.04)
-
-# Blue tag - tilted right, behind/below
-tag_w2 = int(SIZE * 0.46)
-tag_h2 = int(SIZE * 0.58)
-blue_tag, _, _, _ = draw_tag(None, 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)
-
-# Composite: blue behind, red in front
-# Add drop shadows first
-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)
-
-# Red tag shadow
-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)
-
-# Apply squircle clip mask
-mask = rounded_rectangle_mask(SIZE, squircle_r)
-img.putalpha(mask)
-
-# --- Save PNG variants for .icns ---
-variants = [
+VARIANTS = [
("icon_16x16.png", 16),
("icon_16x16@2x.png", 32),
("icon_32x32.png", 32),
@@ -110,23 +49,135 @@
("icon_512x512@2x.png", 1024),
]
-for name, size in variants:
- resized = img.resize((size, size), Image.LANCZOS)
- resized.save(os.path.join(OUT_DIR, name), "PNG")
- print(f" {name} ({size}x{size})")
+# 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
-# Also save a 1024 preview
-img.save(os.path.join(OUT_DIR, "icon_1024_preview.png"), "PNG")
-print(f"\nSaved {len(variants)} variants to {OUT_DIR}")
-# --- Generate .icns using iconutil ---
-iconset_dir = OUT_DIR.rstrip('/')
-subprocess.run(["iconutil", "-c", "icns", iconset_dir, "-o",
- "/Users/ar/Projects/Apptag/build/Apptag.app/Contents/Resources/AppIcon.icns"],
- check=True)
-print("Generated AppIcon.icns")
+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.")
-# Also copy to build dir
-subprocess.run(["cp", os.path.join(OUT_DIR, "icon_1024_preview.png"),
- "/Users/ar/Projects/Apptag/icon_preview.png"], check=True)
-print("Preview: /Users/ar/Projects/Apptag/icon_preview.png")
+
+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()
--
Gitblit v1.9.3