chore: 重新生成应用图标

- 使用新的绿色M logo(白底+内容图案作为整体)
- 图标内容占画布80.5%,四周留透明边距(参考腾讯视频)
- 恢复脚本为正确的圆角裁剪逻辑,去掉错误的trim和overscale
- 移除Android/iOS图标生成(桌面端项目不需要)
This commit is contained in:
小鱼开发
2026-05-19 23:58:16 +08:00
parent 4cbbb8d2b3
commit 331e9ccc23
19 changed files with 35 additions and 316 deletions
+35 -182
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
"""生成 macOS Big Sur 风格圆角矩形图标"""
"""生成圆角图标:原图(白底+内容)作为整体,缩放居中,圆角外透明"""
import os
from PIL import Image, ImageDraw, ImageFilter
from PIL import Image, ImageDraw
ICONS_DIR = "/Users/0fun/work/meijiaka-zy/tauri-app/src-tauri/icons"
SOURCE_PNG = "/tmp/original-source-icon.png"
@@ -10,7 +10,9 @@ SOURCE_PNG = "/tmp/original-source-icon.png"
# macOS Big Sur 圆角比例 ≈ 22.6%
CORNER_RATIO = 0.226
# 输出尺寸列表 (文件名, 尺寸)
# 内容占画布比例(参考腾讯视频 ≈ 80.5%)
CONTENT_RATIO = 0.805
PNG_SIZES = [
("icon.png", 512),
("128x128@2x.png", 256),
@@ -19,7 +21,6 @@ PNG_SIZES = [
("64x64.png", 64),
]
# Windows Store/Square 尺寸
SQUARE_SIZES = [
("Square310x310Logo.png", 310),
("Square284x284Logo.png", 284),
@@ -35,113 +36,44 @@ SQUARE_SIZES = [
def create_rounded_rect_mask(size: int, radius: int) -> Image.Image:
"""创建圆角矩形蒙版"""
"""创建圆角矩形蒙版(硬边缘)"""
mask = Image.new("L", (size, size), 0)
draw = ImageDraw.Draw(mask)
draw.rounded_rectangle((0, 0, size, size), radius=radius, fill=255)
return mask
def create_big_sur_background(size: int) -> Image.Image:
"""创建 macOS Big Sur 风格圆角矩形底板(微妙渐变)"""
radius = int(size * CORNER_RATIO)
def compose_icon(size: int, source: Image.Image) -> Image.Image:
"""原图作为整体,缩放至画布 CONTENT_RATIO,居中,裁成圆角矩形"""
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
# 顶部稍亮、底部稍暗的微妙渐变
bg = Image.new("RGBA", (size, size), (255, 255, 255, 0))
draw = ImageDraw.Draw(bg)
for y in range(size):
val = int(250 - (y / size) * 10)
draw.line([(0, y), (size, y)], fill=(val, val, val, 255))
# 圆角裁剪
mask = create_rounded_rect_mask(size, radius)
masked = Image.new("RGBA", (size, size), (255, 255, 255, 0))
masked.paste(bg, mask=mask)
return masked
def create_shadow_layer(size: int, radius: int) -> Image.Image:
"""创建底板外部阴影(用于大尺寸)"""
pad = int(size * 0.06)
canvas_size = size + pad * 2
shadow = Image.new("RGBA", (canvas_size, canvas_size), (0, 0, 0, 0))
draw = ImageDraw.Draw(shadow)
draw.rounded_rectangle(
(pad, pad, pad + size, pad + size),
radius=radius,
fill=(0, 0, 0, 40),
)
shadow = shadow.filter(ImageFilter.GaussianBlur(radius=pad // 2))
return shadow, pad
def clean_source_icon(source: Image.Image) -> Image.Image:
"""清理原始图标中的幽灵半透明像素(如内层圆角矩形轮廓)"""
rgba = source.convert("RGBA")
pixels = rgba.load()
width, height = rgba.size
for y in range(height):
for x in range(width):
r, g, b, a = pixels[x, y]
if a < 250:
pixels[x, y] = (0, 0, 0, 0)
else:
pixels[x, y] = (r, g, b, 255)
return rgba
def _prepare_m_icon(source: Image.Image, plate_size: int) -> Image.Image:
"""裁剪 M 的透明边距,宽度充满底板,保持完整形状(不裁剪)"""
bbox = source.getbbox()
if bbox:
m_cropped = source.crop(bbox)
else:
m_cropped = source
# 保持宽高比,宽度充满底板,高度自然留白
m_cropped.thumbnail((plate_size, plate_size), Image.LANCZOS)
return m_cropped
def compose_icon(size: int, source: Image.Image, add_shadow: bool = False) -> Image.Image:
"""将 M 图标合成到圆角矩形底板上(底板占画布80%,四周留透明边距)"""
# macOS 图标底板占画布约 80%,四周留透明呼吸边距
plate_size = int(size * 0.80)
plate_size = int(size * CONTENT_RATIO)
plate_offset = (size - plate_size) // 2
radius = int(plate_size * CORNER_RATIO)
if add_shadow and size >= 128:
shadow, pad = create_shadow_layer(plate_size, int(plate_size * CORNER_RATIO))
canvas = Image.new("RGBA", (size + pad * 2, size + pad * 2), (0, 0, 0, 0))
canvas.paste(shadow, (0, 0), shadow)
# 原图等比缩放,短边充满 plate_size
src_w, src_h = source.size
ratio = max(plate_size / src_w, plate_size / src_h)
new_w = int(src_w * ratio)
new_h = int(src_h * ratio)
resized = source.resize((new_w, new_h), Image.LANCZOS)
bg = create_big_sur_background(plate_size)
canvas.paste(bg, (pad + plate_offset, pad + plate_offset), bg)
# 居中裁剪到 plate_size
left = (new_w - plate_size) // 2
top = (new_h - plate_size) // 2
img = resized.crop((left, top, left + plate_size, top + plate_size))
m_img = _prepare_m_icon(source, plate_size)
offset_x = (plate_size - m_img.width) // 2
offset_y = (plate_size - m_img.height) // 2
canvas.paste(m_img, (pad + plate_offset + offset_x, pad + plate_offset + offset_y), m_img)
return canvas
else:
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
bg = create_big_sur_background(plate_size)
canvas.paste(bg, (plate_offset, plate_offset), bg)
m_img = _prepare_m_icon(source, plate_size)
offset_x = (plate_size - m_img.width) // 2
offset_y = (plate_size - m_img.height) // 2
canvas.paste(m_img, (plate_offset + offset_x, plate_offset + offset_y), m_img)
return canvas
# 圆角蒙版裁剪
mask = create_rounded_rect_mask(plate_size, radius)
canvas.paste(img, (plate_offset, plate_offset), mask)
return canvas
def generate_icns(source: Image.Image, output_path: str):
"""生成 macOS .icns 文件"""
import tempfile
import subprocess
import shutil
sizes = [16, 32, 64, 128, 256, 512, 1024]
iconset_dir = tempfile.mkdtemp(suffix=".iconset")
@@ -157,14 +89,11 @@ def generate_icns(source: Image.Image, output_path: str):
["iconutil", "-c", "icns", iconset_dir, "-o", output_path],
check=True,
)
# 清理
import shutil
shutil.rmtree(iconset_dir)
def generate_ico(source: Image.Image, output_path: str):
"""生成 Windows .ico 文件(手动组装,支持多分辨率 PNG 嵌入)"""
"""生成 Windows .ico 文件"""
import struct
import io
@@ -180,30 +109,18 @@ def generate_ico(source: Image.Image, output_path: str):
png_datas.append(data)
entries.append((sz, len(data)))
# ICO 文件头: Reserved(2) + Type(2) + Count(2)
ico = struct.pack("<HHH", 0, 1, len(sizes))
# 计算数据偏移量: 文件头(6) + 目录项(16 * count)
data_offset = 6 + 16 * len(sizes)
# ICONDIRENTRY
for sz, size_bytes in entries:
width = sz if sz < 256 else 0
height = sz if sz < 256 else 0
ico += struct.pack(
"<BBBBHHII",
width, # Width
height, # Height
0, # Colors (0 for >256)
0, # Reserved
1, # Color planes
32, # Bits per pixel
size_bytes, # Size in bytes
data_offset, # Offset
width, height, 0, 0, 1, 32, size_bytes, data_offset,
)
data_offset += size_bytes
# 追加图像数据
for data in png_datas:
ico += data
@@ -211,89 +128,25 @@ def generate_ico(source: Image.Image, output_path: str):
f.write(ico)
def generate_android(source: Image.Image, android_dir: str):
"""生成 Android 图标"""
android_sizes = {
"mipmap-hdpi": 72,
"mipmap-mdpi": 48,
"mipmap-xhdpi": 96,
"mipmap-xxhdpi": 144,
"mipmap-xxxhdpi": 192,
}
for folder, sz in android_sizes.items():
folder_path = os.path.join(android_dir, folder)
os.makedirs(folder_path, exist_ok=True)
img = compose_icon(sz, source)
img.save(os.path.join(folder_path, "ic_launcher.png"))
# foreground / background / round / monochrome 等可能不需要更新
def generate_ios(source: Image.Image, ios_dir: str):
"""生成 iOS 图标"""
ios_sizes = [
("AppIcon-20x20@1x.png", 20),
("AppIcon-20x20@2x.png", 40),
("AppIcon-20x20@3x.png", 60),
("AppIcon-29x29@1x.png", 29),
("AppIcon-29x29@2x.png", 58),
("AppIcon-29x29@3x.png", 87),
("AppIcon-40x40@1x.png", 40),
("AppIcon-40x40@2x.png", 80),
("AppIcon-40x40@3x.png", 120),
("AppIcon-512@2x~ipad.png", 1024),
("AppIcon-512x512@1x.png", 512),
("AppIcon-512x512@2x.png", 1024),
("AppIcon-60x60@2x.png", 120),
("AppIcon-60x60@3x.png", 180),
("AppIcon-76x76@1x.png", 76),
("AppIcon-76x76@2x.png", 152),
("AppIcon-83.5x83.5@2x.png", 167),
("AppIcon-Notification@3x.png", 60),
]
for filename, sz in ios_sizes:
img = compose_icon(sz, source)
img.save(os.path.join(ios_dir, filename))
def main():
source_raw = Image.open(SOURCE_PNG).convert("RGBA")
source = clean_source_icon(source_raw)
source = Image.open(SOURCE_PNG).convert("RGBA")
print(f"源图标尺寸: {source.size}")
# 生成基础 PNG
for filename, size in PNG_SIZES:
path = os.path.join(ICONS_DIR, filename)
img = compose_icon(size, source)
img.save(path)
compose_icon(size, source).save(path)
print(f"已生成: {filename} ({size}x{size})")
# 生成 Square LogoWindows
for filename, size in SQUARE_SIZES:
path = os.path.join(ICONS_DIR, filename)
img = compose_icon(size, source)
img.save(path)
compose_icon(size, source).save(path)
print(f"已生成: {filename} ({size}x{size})")
# 生成 .icns
icns_path = os.path.join(ICONS_DIR, "icon.icns")
generate_icns(source, icns_path)
print(f"已生成: icon.icns")
generate_icns(source, os.path.join(ICONS_DIR, "icon.icns"))
print("已生成: icon.icns")
# 生成 .ico
ico_path = os.path.join(ICONS_DIR, "icon.ico")
generate_ico(source, ico_path)
print(f"已生成: icon.ico")
# 生成 Android
android_dir = os.path.join(ICONS_DIR, "android")
generate_android(source, android_dir)
print(f"已生成: Android 图标")
# 生成 iOS
ios_dir = os.path.join(ICONS_DIR, "ios")
generate_ios(source, ios_dir)
print(f"已生成: iOS 图标")
generate_ico(source, os.path.join(ICONS_DIR, "icon.ico"))
print("已生成: icon.ico")
print("\n全部完成!")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

-134
View File
@@ -1,134 +0,0 @@
#!/usr/bin/env python3
"""生成 macOS / Windows 圆角图标"""
from PIL import Image, ImageDraw
import subprocess
import os
import struct
import io
# 配置
LOGO_PATH = "../../public/assets/logo.png"
OUTPUT_DIR = "."
BG_COLOR = (255, 255, 255, 255)
# macOS Big Sur 风格圆角:约 22% 半径
CORNER_RADIUS_RATIO = 0.22
# 白色圆角背景占整个图标的 70%
BG_SCALE = 0.70
# Logo 占白色背景的 60%
LOGO_SCALE = 0.60
def generate_icon(size):
"""生成指定尺寸的圆角图标"""
# 创建透明画布
canvas = Image.new("RGBA", size, (0, 0, 0, 0))
# 白色圆角背景尺寸(占画布的 BG_SCALE)
bg_size = int(min(size) * BG_SCALE)
bg_radius = int(bg_size * CORNER_RADIUS_RATIO)
bg_x = (size[0] - bg_size) // 2
bg_y = (size[1] - bg_size) // 2
# 绘制白色圆角背景
draw = ImageDraw.Draw(canvas)
draw.rounded_rectangle(
(bg_x, bg_y, bg_x + bg_size, bg_y + bg_size),
radius=bg_radius,
fill=BG_COLOR,
)
# 加载并缩放 logo
logo = Image.open(LOGO_PATH).convert("RGBA")
logo_size = int(bg_size * LOGO_SCALE)
logo = logo.resize((logo_size, logo_size), Image.LANCZOS)
# 居中绘制 logo(相对于整个画布)
x = (size[0] - logo.width) // 2
y = (size[1] - logo.height) // 2
canvas.paste(logo, (x, y), logo)
return canvas
def save_ico(images, sizes, filepath):
"""手动构建多帧 ICO 文件"""
num_images = len(images)
header = struct.pack("<HHH", 0, 1, num_images)
offset = 6 + 16 * num_images
entries = []
data = b""
for img, size in zip(images, sizes):
buf = io.BytesIO()
img.save(buf, format="PNG")
png_bytes = buf.getvalue()
w = size if size < 256 else 0
h = size if size < 256 else 0
entry = struct.pack("<BBBBHHII", w, h, 0, 0, 1, 32, len(png_bytes), offset)
entries.append(entry)
data += png_bytes
offset += len(png_bytes)
with open(filepath, "wb") as f:
f.write(header)
for e in entries:
f.write(e)
f.write(data)
def main():
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# 生成各尺寸 PNG(用于 ICNS)
sizes = {
"icon_16x16.png": (16, 16),
"icon_32x32.png": (32, 32),
"icon_128x128.png": (128, 128),
"icon_256x256.png": (256, 256),
"icon_512x512.png": (512, 512),
"icon_512x512@2x.png": (1024, 1024),
}
# 用于 ICNS 的临时目录
icns_dir = "/tmp/icon.iconset"
os.makedirs(icns_dir, exist_ok=True)
for filename, size in sizes.items():
img = generate_icon(size)
img.save(f"{icns_dir}/{filename}", "PNG")
print(f"Generated {filename} ({size[0]}x{size[1]})")
# 复制到输出目录
for src, dst in [
("icon_32x32.png", "32x32.png"),
("icon_128x128.png", "128x128.png"),
("icon_256x256.png", "128x128@2x.png"),
("icon_512x512.png", "icon.png"),
]:
subprocess.run(["cp", f"{icns_dir}/{src}", f"{OUTPUT_DIR}/{dst}"])
print(f"Copied to {dst}")
# 生成 ICNS
subprocess.run(
["iconutil", "-c", "icns", "-o", f"{OUTPUT_DIR}/icon.icns", icns_dir],
check=True,
)
print("Generated icon.icns")
# 生成 ICOWindows
ico_sizes = [16, 32, 48, 64, 128, 256]
ico_images = [generate_icon((s, s)) for s in ico_sizes]
save_ico(ico_images, ico_sizes, f"{OUTPUT_DIR}/icon.ico")
print("Generated icon.ico")
# 清理临时目录
subprocess.run(["rm", "-rf", icns_dir])
print("\n✅ 所有图标已生成")
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB