146 lines
5.3 KiB
Python
146 lines
5.3 KiB
Python
# Author: fruit.com.cn
|
|
"""使用确认的 ymb 原图生成图标、启动标识及原生平台尺寸。"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageChops, ImageDraw
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ASSETS = ROOT / "assets/icon"
|
|
IOS = ROOT / "ios/Runner/Assets.xcassets"
|
|
ANDROID = ROOT / "android/app/src/main/res"
|
|
REFERENCE = ROOT / "assets/branding/ymb-reference.png"
|
|
LAUNCH_REFERENCE = ROOT / "assets/branding/ymb-launch-reference.png"
|
|
BRAND_BACKGROUND = "#000000"
|
|
WHITE = "#FFFFFF"
|
|
CLEAR = (0, 0, 0, 0)
|
|
MASTER_SIZE = 1024
|
|
SPLASH_SIZE = 80
|
|
SPLASH_RADIUS = 18
|
|
ANDROID12_SIZE = 288
|
|
FOREGROUND_SIZE = 108
|
|
LEGACY_SIZE = 48
|
|
PLACEHOLDER_SIZE = (600, 360)
|
|
LAUNCH_SIZE = (390, 844)
|
|
WHITE_THRESHOLD = 100
|
|
WHITE_RANGE = 145
|
|
DENSITIES = {"mdpi": 1, "hdpi": 1.5, "xhdpi": 2, "xxhdpi": 3, "xxxhdpi": 4}
|
|
RESAMPLE = Image.Resampling.LANCZOS
|
|
|
|
|
|
def wordmark(reference):
|
|
"""从原图白色字标提取透明前景,不重新绘制或改变字形。"""
|
|
channels = reference.convert("RGB").split()
|
|
lightness = ImageChops.darker(ImageChops.darker(*channels[:2]), channels[2])
|
|
alpha = lightness.point(
|
|
lambda value: max(0, min(255, round(
|
|
(value - WHITE_THRESHOLD) * 255 / WHITE_RANGE
|
|
)))
|
|
)
|
|
image = Image.new("RGBA", reference.size, WHITE)
|
|
image.putalpha(alpha)
|
|
return image
|
|
|
|
|
|
def centered(canvas, mark):
|
|
"""将标识按可见边界居中,不包含字体基线额外留白。"""
|
|
origin = ((canvas.width - mark.width) // 2, (canvas.height - mark.height) // 2)
|
|
canvas.alpha_composite(mark, origin)
|
|
return canvas
|
|
|
|
|
|
def scaled_wordmark(mark, width):
|
|
"""等比缩放字标,所有输出共用同一字形。"""
|
|
height = round(mark.height * width / mark.width)
|
|
return mark.resize((width, height), RESAMPLE)
|
|
|
|
|
|
def splash_icon(master, size):
|
|
"""仅启动图烘焙圆角;桌面图标保留系统负责的裁切。"""
|
|
image = master.resize((size, size), RESAMPLE).convert("RGBA")
|
|
mask = Image.new("L", (MASTER_SIZE, MASTER_SIZE))
|
|
radius = MASTER_SIZE * SPLASH_RADIUS / SPLASH_SIZE
|
|
ImageDraw.Draw(mask).rounded_rectangle(
|
|
(0, 0, MASTER_SIZE - 1, MASTER_SIZE - 1), radius=radius, fill=255
|
|
)
|
|
image.putalpha(mask.resize((size, size), RESAMPLE))
|
|
return image
|
|
|
|
|
|
def save(image, target):
|
|
"""写入明确路径的生成资源,不清理或删除目录。"""
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
image.save(target, optimize=True)
|
|
|
|
|
|
def ios_assets(master):
|
|
"""严格按资源目录声明导出图标;开屏保留确认的完整参考图。"""
|
|
app_icons = IOS / "AppIcon.appiconset"
|
|
manifest = json.loads((app_icons / "Contents.json").read_text())
|
|
for entry in manifest["images"]:
|
|
points = float(entry["size"].split("x")[0])
|
|
scale = int(entry["scale"].removesuffix("x"))
|
|
pixels = round(points * scale)
|
|
save(master.resize((pixels, pixels), RESAMPLE), app_icons / entry["filename"])
|
|
for scale in (1, 2, 3):
|
|
suffix = "" if scale == 1 else f"@{scale}x"
|
|
size = tuple(value * scale for value in LAUNCH_SIZE)
|
|
image = Image.open(LAUNCH_REFERENCE).convert("RGB").resize(size, RESAMPLE)
|
|
save(image, IOS / f"LaunchImage.imageset/LaunchImage{suffix}.png")
|
|
|
|
|
|
def android_assets(master, foreground):
|
|
"""覆盖现有密度资源,并为 Android 12 提供独立安全区标识。"""
|
|
for density, scale in DENSITIES.items():
|
|
pixels = round(LEGACY_SIZE * scale)
|
|
save(
|
|
master.resize((pixels, pixels), RESAMPLE),
|
|
ANDROID / f"mipmap-{density}/ic_launcher.png",
|
|
)
|
|
pixels = round(FOREGROUND_SIZE * scale)
|
|
save(
|
|
foreground.resize((pixels, pixels), RESAMPLE),
|
|
ANDROID / f"drawable-{density}/ic_launcher_foreground.png",
|
|
)
|
|
save(
|
|
Image.new("RGB", (pixels, pixels), BRAND_BACKGROUND),
|
|
ANDROID / f"mipmap-{density}/ic_launcher_background.png",
|
|
)
|
|
mark = splash_icon(master, round(SPLASH_SIZE * scale))
|
|
save(mark, ANDROID / f"drawable-{density}/splash_screen.png")
|
|
size = round(ANDROID12_SIZE * scale)
|
|
image = centered(Image.new("RGBA", (size, size), CLEAR), mark)
|
|
save(image, ANDROID / f"drawable-{density}/android12splash.png")
|
|
|
|
|
|
def main():
|
|
"""直接复用确认的原始图标和开屏,输出平台所需尺寸。"""
|
|
reference = Image.open(REFERENCE).convert("RGB")
|
|
mark = wordmark(reference)
|
|
master = centered(
|
|
Image.new("RGBA", (MASTER_SIZE, MASTER_SIZE), (0, 0, 0, 255)),
|
|
mark.resize((MASTER_SIZE, MASTER_SIZE), RESAMPLE),
|
|
)
|
|
foreground = mark.resize((MASTER_SIZE, MASTER_SIZE), RESAMPLE)
|
|
save(master.convert("RGB"), ASSETS / "ymb.png")
|
|
save(foreground, ASSETS / "ymb_foreground.png")
|
|
placeholder = centered(
|
|
Image.new("RGBA", PLACEHOLDER_SIZE, CLEAR),
|
|
scaled_wordmark(mark.crop(mark.getbbox()), PLACEHOLDER_SIZE[0]),
|
|
)
|
|
save(placeholder, ASSETS / "splash_logo.png")
|
|
icon = splash_icon(master, SPLASH_SIZE * 4)
|
|
size = ANDROID12_SIZE * 4
|
|
save(
|
|
centered(Image.new("RGBA", (size, size), CLEAR), icon),
|
|
ASSETS / "ymb_android12.png",
|
|
)
|
|
ios_assets(master)
|
|
android_assets(master, foreground)
|
|
print("Generated ymb assets: master 1024, iOS catalog, Android 5 densities.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|