feat(brand): add application logo for exe and window icon
新增品牌标识并接入运行时与打包流程。 - `scripts/gen_logo.py` 从单一几何定义同时产出 SVG 母版、多尺寸 ICO 和 PNG 预览,避免母版与位图漂移;改样式改脚本后重新生成即可。 - 标记为暖橙圆角徽章 + 白色上升箭头(提升并发布)+ 四角星芒(AI 生成), 刻意避开蝦皮官方购物袋标识,避免暗示官方关联。 - ICO 含 16/20/24/32/40/48/64/128/256 九档,每档从矢量几何独立渲染而非 由大图缩放,保证任务栏与资源管理器小尺寸清晰。 - `app/assets` 作为可导入包提供无 Qt 依赖的路径解析,兼容源码树、 PyInstaller onedir 的 _MEIPASS 与 exe 同级目录三种布局。 - `widgets.app_icon()` 在 Qt 缺失或资源未生成时返回 None,调用方容错; 在 QApplication 上设置图标以便任务栏、对话框和消息框一并继承, MainWindow 另行设置以覆盖直接构造窗口的测试路径。 - 两个 spec 均加 `icon=`,主 spec 收集资源到 datas 并在资源缺失时直接 报错提示先跑生成脚本。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6a0cd1c763
commit
049d4b6bbc
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the cmshopee application logo.
|
||||
|
||||
Geometry lives here once and is emitted to both the editable SVG master and
|
||||
the rasterised Windows icon, so the two can never drift apart.
|
||||
|
||||
Usage:
|
||||
py -3.10 scripts/gen_logo.py
|
||||
|
||||
Outputs (all under app/assets/):
|
||||
cmshopee-logo.svg editable master
|
||||
cmshopee.ico multi-size Windows icon (exe + window icon)
|
||||
cmshopee-256.png preview / non-Windows fallback
|
||||
|
||||
The mark is an upward arrow rising from a base bar: the product listing being
|
||||
lifted and published. It deliberately avoids Shopee's own bag glyph so the
|
||||
tool is not mistaken for an official Shopee application.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw
|
||||
except ImportError: # pragma: no cover - developer tooling only
|
||||
sys.exit("需要 Pillow 才能生成图标:py -3.10 -m pip install Pillow")
|
||||
|
||||
|
||||
CANVAS = 256
|
||||
SUPERSAMPLE = 8
|
||||
ICO_SIZES = (16, 20, 24, 32, 40, 48, 64, 128, 256)
|
||||
|
||||
# Badge
|
||||
BADGE = (10, 10, 246, 246) # x0, y0, x1, y1
|
||||
BADGE_RADIUS = 54
|
||||
GRADIENT_FROM = (245, 121, 63) # #F5793F
|
||||
GRADIENT_TO = (214, 55, 28) # #D6371C
|
||||
|
||||
# Mark (white): an arrow lifting a listing off its base, plus an AI sparkle.
|
||||
ARROW_HEAD = ((112, 58), (56, 116), (168, 116))
|
||||
ARROW_STEM = (90, 110, 134, 162) # x0, y0, x1, y1
|
||||
ARROW_STEM_RADIUS = 10
|
||||
BASE_BAR = (56, 178, 168, 204)
|
||||
BASE_BAR_RADIUS = 13
|
||||
MARK_COLOR = (255, 255, 255)
|
||||
|
||||
# Four-point sparkle: the AI generation step. Degrades to a small accent dot
|
||||
# at 16px rather than turning to mush.
|
||||
SPARKLE_CENTER = (194, 74)
|
||||
SPARKLE_OUTER = 34
|
||||
SPARKLE_INNER = 11
|
||||
|
||||
ASSETS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "app", "assets")
|
||||
|
||||
|
||||
def _hex(rgb):
|
||||
return "#%02X%02X%02X" % rgb
|
||||
|
||||
|
||||
def _gradient(size):
|
||||
"""Diagonal linear gradient, built small and scaled up (it is smooth)."""
|
||||
|
||||
base = Image.new("RGB", (CANVAS, CANVAS))
|
||||
pixels = base.load()
|
||||
span = 2 * (CANVAS - 1)
|
||||
for y in range(CANVAS):
|
||||
for x in range(CANVAS):
|
||||
t = (x + y) / span
|
||||
pixels[x, y] = tuple(
|
||||
round(GRADIENT_FROM[i] + (GRADIENT_TO[i] - GRADIENT_FROM[i]) * t)
|
||||
for i in range(3)
|
||||
)
|
||||
return base.resize((size, size), Image.LANCZOS)
|
||||
|
||||
|
||||
def _scaled(values, factor):
|
||||
return [value * factor for value in values]
|
||||
|
||||
|
||||
def _sparkle_points():
|
||||
"""Eight alternating outer/inner points forming a four-point star."""
|
||||
|
||||
cx, cy = SPARKLE_CENTER
|
||||
o, i = SPARKLE_OUTER, SPARKLE_INNER
|
||||
diagonal = i * (2 ** 0.5) / 2
|
||||
return [
|
||||
(cx, cy - o),
|
||||
(cx + diagonal, cy - diagonal),
|
||||
(cx + o, cy),
|
||||
(cx + diagonal, cy + diagonal),
|
||||
(cx, cy + o),
|
||||
(cx - diagonal, cy + diagonal),
|
||||
(cx - o, cy),
|
||||
(cx - diagonal, cy - diagonal),
|
||||
]
|
||||
|
||||
|
||||
def render(size):
|
||||
"""Render one square icon at the given pixel size."""
|
||||
|
||||
work = size * SUPERSAMPLE
|
||||
factor = work / CANVAS
|
||||
|
||||
badge_mask = Image.new("L", (work, work), 0)
|
||||
draw = ImageDraw.Draw(badge_mask)
|
||||
draw.rounded_rectangle(_scaled(BADGE, factor), radius=BADGE_RADIUS * factor, fill=255)
|
||||
|
||||
mark_mask = Image.new("L", (work, work), 0)
|
||||
draw = ImageDraw.Draw(mark_mask)
|
||||
draw.polygon([(x * factor, y * factor) for x, y in ARROW_HEAD], fill=255)
|
||||
draw.rounded_rectangle(_scaled(ARROW_STEM, factor), radius=ARROW_STEM_RADIUS * factor, fill=255)
|
||||
draw.rounded_rectangle(_scaled(BASE_BAR, factor), radius=BASE_BAR_RADIUS * factor, fill=255)
|
||||
draw.polygon([(x * factor, y * factor) for x, y in _sparkle_points()], fill=255)
|
||||
|
||||
icon = Image.new("RGBA", (work, work), (0, 0, 0, 0))
|
||||
icon.paste(_gradient(work), (0, 0), badge_mask)
|
||||
icon.paste(Image.new("RGBA", (work, work), MARK_COLOR + (255,)), (0, 0), mark_mask)
|
||||
return icon.resize((size, size), Image.LANCZOS)
|
||||
|
||||
|
||||
def write_svg(path):
|
||||
head = "M {} {} L {} {} L {} {} Z".format(*[c for point in ARROW_HEAD for c in point])
|
||||
sparkle = " ".join(f"{x:.1f},{y:.1f}" for x, y in _sparkle_points())
|
||||
svg = f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {CANVAS} {CANVAS}" width="{CANVAS}" height="{CANVAS}" role="img" aria-label="蝦皮圈優化助手">
|
||||
<title>蝦皮圈優化助手</title>
|
||||
<defs>
|
||||
<linearGradient id="badge" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="{_hex(GRADIENT_FROM)}"/>
|
||||
<stop offset="1" stop-color="{_hex(GRADIENT_TO)}"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="{BADGE[0]}" y="{BADGE[1]}" width="{BADGE[2] - BADGE[0]}" height="{BADGE[3] - BADGE[1]}" rx="{BADGE_RADIUS}" fill="url(#badge)"/>
|
||||
<path d="{head}" fill="{_hex(MARK_COLOR)}"/>
|
||||
<rect x="{ARROW_STEM[0]}" y="{ARROW_STEM[1]}" width="{ARROW_STEM[2] - ARROW_STEM[0]}" height="{ARROW_STEM[3] - ARROW_STEM[1]}" rx="{ARROW_STEM_RADIUS}" fill="{_hex(MARK_COLOR)}"/>
|
||||
<rect x="{BASE_BAR[0]}" y="{BASE_BAR[1]}" width="{BASE_BAR[2] - BASE_BAR[0]}" height="{BASE_BAR[3] - BASE_BAR[1]}" rx="{BASE_BAR_RADIUS}" fill="{_hex(MARK_COLOR)}"/>
|
||||
<polygon points="{sparkle}" fill="{_hex(MARK_COLOR)}"/>
|
||||
</svg>
|
||||
"""
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(svg)
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(ASSETS_DIR, exist_ok=True)
|
||||
|
||||
# Each size is rendered from the vector geometry rather than downscaled from
|
||||
# one bitmap, so 16px and 20px stay crisp in Explorer and the taskbar.
|
||||
images = [render(size) for size in ICO_SIZES]
|
||||
largest = images[-1]
|
||||
|
||||
ico_path = os.path.join(ASSETS_DIR, "cmshopee.ico")
|
||||
largest.save(
|
||||
ico_path,
|
||||
format="ICO",
|
||||
sizes=[(size, size) for size in ICO_SIZES],
|
||||
append_images=images[:-1],
|
||||
)
|
||||
|
||||
png_path = os.path.join(ASSETS_DIR, "cmshopee-256.png")
|
||||
largest.save(png_path, format="PNG")
|
||||
|
||||
svg_path = os.path.join(ASSETS_DIR, "cmshopee-logo.svg")
|
||||
write_svg(svg_path)
|
||||
|
||||
for path in (svg_path, ico_path, png_path):
|
||||
print(f"已生成 {os.path.relpath(path, os.path.dirname(ASSETS_DIR))} {os.path.getsize(path)} bytes")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user