Files
meijiaka-zy/scripts/generate-rounded-icon.py
T
小鱼开发 f20de12fa2 feat: macOS Big Sur 风格图标 + Docker 日志轮转 + 后台运维 SQL
图标:
- 添加白色圆角矩形底板,占画布 80%(四周留透明呼吸边距)
- M 内容占底板 65%,裁剪透明边距后居中
- 底板微妙渐变(#FAFAFA → #F0F0F0)
- 清理原始图标幽灵半透明像素
- 全平台图标重新生成(PNG / ICNS / ICO / Android / iOS)

运维:
- docker-compose.prod.yml & test.yml 添加 json-file 日志轮转
  max-size: 100m, max-file: 5
- scripts/admin-ops.sql: 新增用户、积分赠送、积分补偿、批量补偿
- scripts/generate-rounded-icon.py: 可复用的图标生成脚本

其他:
- prompts 文件重命名为语义化文件名
- .gitignore 移除 binaries/ 忽略(FFmpeg sidecar 需提交)
2026-05-15 11:33:51 +08:00

304 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""生成 macOS Big Sur 风格圆角矩形图标"""
import os
from PIL import Image, ImageDraw, ImageFilter
ICONS_DIR = "/Users/0fun/work/meijiaka-zy/tauri-app/src-tauri/icons"
SOURCE_PNG = "/tmp/original-source-icon.png"
# macOS Big Sur 圆角比例 ≈ 22.6%
CORNER_RATIO = 0.226
# 输出尺寸列表 (文件名, 尺寸)
PNG_SIZES = [
("icon.png", 512),
("128x128@2x.png", 256),
("128x128.png", 128),
("32x32.png", 32),
("64x64.png", 64),
]
# Windows Store/Square 尺寸
SQUARE_SIZES = [
("Square310x310Logo.png", 310),
("Square284x284Logo.png", 284),
("Square150x150Logo.png", 150),
("Square142x142Logo.png", 142),
("Square107x107Logo.png", 107),
("Square89x89Logo.png", 89),
("Square71x71Logo.png", 71),
("Square44x44Logo.png", 44),
("Square30x30Logo.png", 30),
("StoreLogo.png", 50),
]
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)
# 顶部稍亮、底部稍暗的微妙渐变
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
# 参考 macOS 有底板图标(Xcode、系统设置等),内容占底板约 65%
target_size = int(plate_size * 0.65)
m_cropped.thumbnail((target_size, target_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_offset = (size - plate_size) // 2
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)
bg = create_big_sur_background(plate_size)
canvas.paste(bg, (pad + plate_offset, pad + 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, (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
def generate_icns(source: Image.Image, output_path: str):
"""生成 macOS .icns 文件"""
import tempfile
import subprocess
sizes = [16, 32, 64, 128, 256, 512, 1024]
iconset_dir = tempfile.mkdtemp(suffix=".iconset")
for sz in sizes:
img = compose_icon(sz, source)
img.save(os.path.join(iconset_dir, f"icon_{sz}x{sz}.png"))
if sz <= 512:
img2x = compose_icon(sz * 2, source)
img2x.save(os.path.join(iconset_dir, f"icon_{sz}x{sz}@2x.png"))
subprocess.run(
["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 嵌入)"""
import struct
import io
sizes = [16, 24, 32, 48, 64, 128, 256]
png_datas = []
entries = []
for sz in sizes:
img = compose_icon(sz, source)
buf = io.BytesIO()
img.save(buf, format="PNG")
data = buf.getvalue()
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
)
data_offset += size_bytes
# 追加图像数据
for data in png_datas:
ico += data
with open(output_path, "wb") as f:
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)
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)
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)
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")
# 生成 .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 图标")
print("\n全部完成!")
if __name__ == "__main__":
main()