新增品牌标识并接入运行时与打包流程。 - `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>
173 lines
5.9 KiB
Python
173 lines
5.9 KiB
Python
#!/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()
|