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()
|