chore: 重新生成应用图标
- 使用新的绿色M logo(白底+内容图案作为整体) - 图标内容占画布80.5%,四周留透明边距(参考腾讯视频) - 恢复脚本为正确的圆角裁剪逻辑,去掉错误的trim和overscale - 移除Android/iOS图标生成(桌面端项目不需要)
@@ -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 Logo(Windows)
|
||||
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全部完成!")
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 743 B After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 997 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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")
|
||||
|
||||
# 生成 ICO(Windows)
|
||||
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()
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |