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,57 @@
|
|||||||
|
"""Bundled static assets (application icon).
|
||||||
|
|
||||||
|
The icon ships inside the package so the same file backs the window icon at
|
||||||
|
runtime and the executable icon at build time. Keep this module free of Qt
|
||||||
|
imports: the packaging spec imports it while collecting data files.
|
||||||
|
|
||||||
|
Regenerate the files with ``py -3.10 scripts/gen_logo.py``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ICON_NAME = "cmshopee.ico"
|
||||||
|
ICON_PNG_NAME = "cmshopee-256.png"
|
||||||
|
|
||||||
|
_PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_paths(name):
|
||||||
|
"""Locations to probe, covering source tree and PyInstaller onedir."""
|
||||||
|
|
||||||
|
yield os.path.join(_PACKAGE_DIR, name)
|
||||||
|
|
||||||
|
bundle_dir = getattr(sys, "_MEIPASS", None)
|
||||||
|
if bundle_dir:
|
||||||
|
yield os.path.join(bundle_dir, "app", "assets", name)
|
||||||
|
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
yield os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(sys.executable)),
|
||||||
|
"app",
|
||||||
|
"assets",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def asset_path(name):
|
||||||
|
"""Return an existing asset path, or None when it is not bundled."""
|
||||||
|
|
||||||
|
for candidate in _candidate_paths(name):
|
||||||
|
if candidate and os.path.isfile(candidate):
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def icon_path():
|
||||||
|
"""Path to the multi-size Windows icon, or None when unavailable."""
|
||||||
|
|
||||||
|
return asset_path(ICON_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def icon_png_path():
|
||||||
|
"""Path to the 256px PNG fallback, or None when unavailable."""
|
||||||
|
|
||||||
|
return asset_path(ICON_PNG_NAME)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="蝦皮圈優化助手">
|
||||||
|
<title>蝦皮圈優化助手</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="badge" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#F5793F"/>
|
||||||
|
<stop offset="1" stop-color="#D6371C"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="10" y="10" width="236" height="236" rx="54" fill="url(#badge)"/>
|
||||||
|
<path d="M 112 58 L 56 116 L 168 116 Z" fill="#FFFFFF"/>
|
||||||
|
<rect x="90" y="110" width="44" height="52" rx="10" fill="#FFFFFF"/>
|
||||||
|
<rect x="56" y="178" width="112" height="26" rx="13" fill="#FFFFFF"/>
|
||||||
|
<polygon points="194.0,40.0 201.8,66.2 228.0,74.0 201.8,81.8 194.0,108.0 186.2,81.8 160.0,74.0 186.2,66.2" fill="#FFFFFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 777 B |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -122,6 +122,11 @@ def main() -> int:
|
|||||||
return 1
|
return 1
|
||||||
_ensure_offscreen_for_headless_tests()
|
_ensure_offscreen_for_headless_tests()
|
||||||
app = QApplication.instance() or QApplication(sys.argv)
|
app = QApplication.instance() or QApplication(sys.argv)
|
||||||
|
# Set on the application so the taskbar button, dialogs and message boxes
|
||||||
|
# all inherit it, not just the main window.
|
||||||
|
startup_icon = app_icon()
|
||||||
|
if startup_icon is not None:
|
||||||
|
app.setWindowIcon(startup_icon)
|
||||||
health_context = update_health.context_from_argv(
|
health_context = update_health.context_from_argv(
|
||||||
sys.argv,
|
sys.argv,
|
||||||
appconfig.app_base_dir(),
|
appconfig.app_base_dir(),
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ class MainWindow(QMainWindow):
|
|||||||
or appconfig.ai_models_config_path(self.config)
|
or appconfig.ai_models_config_path(self.config)
|
||||||
)
|
)
|
||||||
self.setWindowTitle(display_name())
|
self.setWindowTitle(display_name())
|
||||||
|
window_icon = app_icon()
|
||||||
|
if window_icon is not None:
|
||||||
|
self.setWindowIcon(window_icon)
|
||||||
_fit_and_center_window(self)
|
_fit_and_center_window(self)
|
||||||
self.setStyleSheet(BUTTON_BASE_STYLE)
|
self.setStyleSheet(BUTTON_BASE_STYLE)
|
||||||
self._settings_tab_index = TAB_TITLES.index("设置")
|
self._settings_tab_index = TAB_TITLES.index("设置")
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
from .. import assets
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QTimer
|
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QTimer
|
||||||
from PySide6.QtGui import QColor, QIcon, QImage, QPainter, QPixmap
|
from PySide6.QtGui import QColor, QIcon, QImage, QPainter, QPixmap
|
||||||
@@ -136,6 +138,22 @@ QPushButton:disabled {{
|
|||||||
_RawQMessageBox = QMessageBox if QT_IMPORT_ERROR is None else None
|
_RawQMessageBox = QMessageBox if QT_IMPORT_ERROR is None else None
|
||||||
|
|
||||||
|
|
||||||
|
def app_icon():
|
||||||
|
"""Application icon, or None when Qt is unavailable or the file is missing.
|
||||||
|
|
||||||
|
Callers must tolerate None: the icon is a packaged asset, and a source
|
||||||
|
checkout that has not run scripts/gen_logo.py still has to start.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if QT_IMPORT_ERROR is not None:
|
||||||
|
return None
|
||||||
|
path = assets.icon_path() or assets.icon_png_path()
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
icon = QIcon(path)
|
||||||
|
return None if icon.isNull() else icon
|
||||||
|
|
||||||
|
|
||||||
class _GuiAttributeProxy:
|
class _GuiAttributeProxy:
|
||||||
def __init__(self, name, fallback):
|
def __init__(self, name, fallback):
|
||||||
self._name = name
|
self._name = name
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Same mark as the main executable: the updater window belongs to this product.
|
||||||
|
APP_ICON = os.path.join(SPECPATH, "app", "assets", "cmshopee.ico")
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
["app/updater_entry.py"],
|
["app/updater_entry.py"],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
@@ -25,4 +30,5 @@ exe = EXE(
|
|||||||
strip=False,
|
strip=False,
|
||||||
upx=True,
|
upx=True,
|
||||||
console=False,
|
console=False,
|
||||||
|
icon=APP_ICON,
|
||||||
)
|
)
|
||||||
|
|||||||
+14
-1
@@ -7,6 +7,8 @@ include config files, SQLite databases, Chrome profiles, generated images,
|
|||||||
logs, prompts, or other operator data.
|
logs, prompts, or other operator data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from PyInstaller.utils.hooks import collect_data_files
|
from PyInstaller.utils.hooks import collect_data_files
|
||||||
|
|
||||||
block_cipher = None
|
block_cipher = None
|
||||||
@@ -14,12 +16,22 @@ default_prompt_datas = collect_data_files(
|
|||||||
"app.default_prompts",
|
"app.default_prompts",
|
||||||
includes=["**/*.txt"],
|
includes=["**/*.txt"],
|
||||||
)
|
)
|
||||||
|
# Window icon at runtime; the same file is the executable icon below.
|
||||||
|
asset_datas = collect_data_files(
|
||||||
|
"app.assets",
|
||||||
|
includes=["*.ico", "*.png", "**/*.ico", "**/*.png"],
|
||||||
|
)
|
||||||
|
if not asset_datas:
|
||||||
|
raise SystemExit(
|
||||||
|
"app/assets 下没有找到图标资源,请先运行:py -3.10 scripts/gen_logo.py"
|
||||||
|
)
|
||||||
|
APP_ICON = os.path.join(SPECPATH, "app", "assets", "cmshopee.ico")
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
["main.py"],
|
["main.py"],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=default_prompt_datas,
|
datas=default_prompt_datas + asset_datas,
|
||||||
hiddenimports=[
|
hiddenimports=[
|
||||||
"PySide6.QtCore",
|
"PySide6.QtCore",
|
||||||
"PySide6.QtGui",
|
"PySide6.QtGui",
|
||||||
@@ -48,6 +60,7 @@ exe = EXE(
|
|||||||
upx=True,
|
upx=True,
|
||||||
console=False,
|
console=False,
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
|
icon=APP_ICON,
|
||||||
)
|
)
|
||||||
coll = COLLECT(
|
coll = COLLECT(
|
||||||
exe,
|
exe,
|
||||||
|
|||||||
@@ -19,4 +19,6 @@
|
|||||||
| [tab6-suite-prompt-dialog-T637.svg](tab6-suite-prompt-dialog-T637.svg) | 商品套图(T-637 提案):标题行「AI帮写/取消」紧随 label、最右新增「提示词设置」;提示词设置弹窗——左编辑基础模板(行序对齐调研笔记组装顺序,套图名称+补充描述、插入变量菜单、占位符校验、只读规则占位符内联)、右只读实时最终预览(预览分类下拉)、保存/恢复默认共用原子写盘;第一版不做同类多张序号/差异化 |
|
| [tab6-suite-prompt-dialog-T637.svg](tab6-suite-prompt-dialog-T637.svg) | 商品套图(T-637 提案):标题行「AI帮写/取消」紧随 label、最右新增「提示词设置」;提示词设置弹窗——左编辑基础模板(行序对齐调研笔记组装顺序,套图名称+补充描述、插入变量菜单、占位符校验、只读规则占位符内联)、右只读实时最终预览(预览分类下拉)、保存/恢复默认共用原子写盘;第一版不做同类多张序号/差异化 |
|
||||||
| [workbench-redesign-v1.html](workbench-redesign-v1.html) | 工作台视觉提案(HTML 可交互原型):以当前 6 Tab 信息架构为基础,统一「数据准备区→表格命令区→表格→状态反馈区→完成任务区」页面骨架;命令按作用域分区(导入/筛选/选择集/工作流完成),表格补齐当前行、选择集、三态全选与排序契约;范围确认、部分成功、取消中间态和破坏性确认按干扰层级分配到对话框、信息条与 Toast;覆盖浅色/深色/高对比度近似、键盘(Tab/方向键/F6/Ctrl+F/F5)与 Windows 断点。商品状态相关交互已对齐 T-662b/c/d、T-663~T-665、T-667 定稿(无状态列与状态下拉筛选,③无强制更新入口)。颜色 Token 取自 `app/gui/widgets.py`。使用模拟数据,不接生产接口;页脚「原型说明与验收边界」列出假设与不可仅靠 HTML 验收的项。 |
|
| [workbench-redesign-v1.html](workbench-redesign-v1.html) | 工作台视觉提案(HTML 可交互原型):以当前 6 Tab 信息架构为基础,统一「数据准备区→表格命令区→表格→状态反馈区→完成任务区」页面骨架;命令按作用域分区(导入/筛选/选择集/工作流完成),表格补齐当前行、选择集、三态全选与排序契约;范围确认、部分成功、取消中间态和破坏性确认按干扰层级分配到对话框、信息条与 Toast;覆盖浅色/深色/高对比度近似、键盘(Tab/方向键/F6/Ctrl+F/F5)与 Windows 断点。商品状态相关交互已对齐 T-662b/c/d、T-663~T-665、T-667 定稿(无状态列与状态下拉筛选,③无强制更新入口)。颜色 Token 取自 `app/gui/widgets.py`。使用模拟数据,不接生产接口;页脚「原型说明与验收边界」列出假设与不可仅靠 HTML 验收的项。 |
|
||||||
|
|
||||||
|
应用图标(非效果图,是随程序发布的资源):`app/assets/cmshopee-logo.svg` 是可编辑母版,`cmshopee.ico`(16–256 共 9 档)用于 exe 图标和 GUI 窗口左上角,`cmshopee-256.png` 为预览与非 Windows 回退。三者由 `py -3.10 scripts/gen_logo.py` 从同一份几何定义生成,改样式请改脚本后重新生成,不要单独手改某一个文件。
|
||||||
|
|
||||||
> 仅为效果图或交互原型,最终样式以实现为准。带 `-T584`/`-v2` 等后缀的为**改版提案**,未定稿。HTML 原型不能验证原生 Qt 标题栏、系统主题、DPI、无障碍树、线程和真实任务生命周期;界面职责与流程见 [../routes.md](../routes.md)。
|
> 仅为效果图或交互原型,最终样式以实现为准。带 `-T584`/`-v2` 等后缀的为**改版提案**,未定稿。HTML 原型不能验证原生 Qt 标题栏、系统主题、DPI、无障碍树、线程和真实任务生命周期;界面职责与流程见 [../routes.md](../routes.md)。
|
||||||
|
|||||||
@@ -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