Files
cmshoppe/scripts/gen_logo.py
T

173 lines
5.9 KiB
Python
Raw Normal View History

#!/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()