Files
TFSImport/tools/make_icon.py
T
chenshoulin d055b415fa feat: 增强便携版支持与配置管理
- 更新 .gitignore 文件,添加便携版分发目录和 Fody 生成的配置文件。
- 修改 README.md,详细说明便携版的构建与分发流程,强调单文件分发的优势。
- 在 Config.cs 中实现便携配置管理,优先使用 exe 同目录的 config.json,提升用户体验。
- 更新 ExcelService.cs,优化默认模板的导出逻辑,支持现场替换模板文件。
- 在 MainForm.cs 中加载应用图标,提升界面一致性。
- 调整 Program.cs,确保在单文件分发时正确加载 native DLL。
2026-11-01 20:17:56 +08:00

306 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""生成 TfsExcelImporter 的应用图标(app.ico / 预览图)。
纯标准库实现(本机没有 Pillow / numpy):用有向距离场(SDF)解析求覆盖率做
抗锯齿,再手写 PNG / ICO 容器,不依赖任何第三方包。
设计:蓝色渐变圆角方块 + 白色线性「上升箭头」字形(导入语义,
方向与界面「开始导入」按钮的 ArrowUp 一致)。按尺寸分档:
- 大尺寸:加顶部高光与 1px 内描边(立体感)
- 小尺寸(<=24):关掉高光/内描边、加粗描边并略微放大字形,避免糊成一团
用法:
python tools/make_icon.py # 生成 app.ico + tools/icon_preview.png
python tools/make_icon.py --only-preview # 只出预览图
"""
import argparse
import math
import os
import struct
import zlib
# ----------------------------------------------------------------- 设计参数
SIZES = [16, 20, 24, 32, 40, 48, 64, 128, 256]
COLOR_TOP = (0x44, 0x8E, 0xFA) # 顶部亮蓝
COLOR_BOT = (0x18, 0x42, 0xC4) # 底部深蓝
WHITE = (255, 255, 255)
TILE_MARGIN = 0.045
TILE_RADIUS = 0.235
def style(size):
"""按尺寸分档的视觉参数。"""
if size <= 20:
return dict(stroke=0.150, gloss=0.00, rim=0.00, ring=True, grow=1.08)
if size <= 24:
return dict(stroke=0.138, gloss=0.00, rim=0.00, ring=True, grow=1.06)
if size <= 40:
return dict(stroke=0.118, gloss=0.09, rim=0.14, ring=False, grow=1.02)
return dict(stroke=0.100, gloss=0.10, rim=0.17, ring=False, grow=1.00)
# 字形骨架(归一化坐标,单位方格)。全部是圆头胶囊,端点已按描边半径内缩过,
# 这样描边加粗时外形不会溢出画布。
# 返回 (箭头胶囊组, 副元素胶囊组, 副元素描边系数);副元素为空表示只画箭头。
def glyph_capsules(stroke, variant="arrow"):
if variant == "arrow":
# 只有箭头:上下留白对称(0.14~0.85),比例比带盒子时更大更饱满
arrow = [
(0.500, 0.270, 0.500, 0.790),
(0.262, 0.418, 0.500, 0.196),
(0.500, 0.196, 0.738, 0.418),
]
return arrow, [], 1.0
shaft_end = 0.700 if variant == "tray" else 0.650
arrow = [
# 箭杆 + 箭头两撇(V 形,圆头在顶点自然并成箭头)
(0.500, 0.235, 0.500, shaft_end),
(0.330, 0.375, 0.500, 0.205),
(0.500, 0.205, 0.670, 0.375),
]
if variant == "slot":
# 单根加粗横条("槽/落点"),无竖壁,小尺寸更干净
return arrow, [(0.250, 0.780, 0.750, 0.780)], 1.30
# 托盘:底边 + 两侧竖壁;底边端点落在竖壁中心线上,角部由此自然圆接
return arrow, [
(0.300, 0.800, 0.700, 0.800),
(0.300, 0.640, 0.300, 0.800),
(0.700, 0.640, 0.700, 0.800),
], 1.0
# ----------------------------------------------------------------- SDF 基础
def sd_round_rect(x, y, cx, cy, hw, hh, r):
dx = abs(x - cx) - (hw - r)
dy = abs(y - cy) - (hh - r)
ox = dx if dx > 0.0 else 0.0
oy = dy if dy > 0.0 else 0.0
inner = dx if dx > dy else dy
if inner > 0.0:
inner = 0.0
return math.hypot(ox, oy) + inner - r
def sd_seg(x, y, ax, ay, bx, by):
pax, pay = x - ax, y - ay
bax, bay = bx - ax, by - ay
den = bax * bax + bay * bay
t = 0.0 if den == 0.0 else max(0.0, min(1.0, (pax * bax + pay * bay) / den))
return math.hypot(pax - bax * t, pay - bay * t)
def clamp01(v):
return 0.0 if v < 0.0 else (1.0 if v > 1.0 else v)
def cov(d, size):
"""距离场 -> 覆盖率(1px 过渡带)。"""
return clamp01(0.5 - d * size)
# ----------------------------------------------------------------- 单尺寸渲染
def render(size, variant="tray"):
st = style(size)
# 只有箭头时没有「托盘」当配重,线条要更粗一点才站得住
stroke = st["stroke"] + (0.016 if variant == "arrow" else 0.0)
grow = st["grow"]
arrow, tray, tray_w = glyph_capsules(stroke, variant)
gr = stroke / 2.0
# 大尺寸下托盘描边略细于箭头:用「粗细」而不是「透明度」做主次(透明度压在深蓝上会发灰)
tray_k = tray_w * (1.0 if size <= 32 else 0.94)
tray_tone = 1.0
hw = 0.5 - TILE_MARGIN
rad = TILE_RADIUS
inner_shrink = 1.0 / size # 内描边宽度 = 1px
buf = bytearray(size * size * 4)
span = 1.0 - 2.0 * TILE_MARGIN
for py in range(size):
v = (py + 0.5) / size
# 字形整体缩放:把坐标拉回以 0.5 为心
gv = 0.5 + (v - 0.5) / grow
row_off = py * size * 4
for px in range(size):
u = (px + 0.5) / size
a = cov(sd_round_rect(u, v, 0.5, 0.5, hw, hw, rad), size)
if a <= 0.0:
continue
# 渐变(略偏对角线,避免纯竖条的死板)
t = clamp01(((v - TILE_MARGIN) * 0.82 + (u - TILE_MARGIN) * 0.18) / span)
cr = COLOR_TOP[0] + (COLOR_BOT[0] - COLOR_TOP[0]) * t
cg = COLOR_TOP[1] + (COLOR_BOT[1] - COLOR_TOP[1]) * t
cb = COLOR_TOP[2] + (COLOR_BOT[2] - COLOR_TOP[2]) * t
# 顶部高光
if st["gloss"] > 0.0:
g = st["gloss"] * clamp01(1.0 - (v - TILE_MARGIN) / 0.38)
cr += (255.0 - cr) * g
cg += (255.0 - cg) * g
cb += (255.0 - cb) * g
# 1px 内描边(沿边缘的细亮线,让方块边界更挺)
if st["rim"] > 0.0:
d_in = sd_round_rect(u, v, 0.5, 0.5, hw - inner_shrink,
hw - inner_shrink, rad - inner_shrink)
band = a - cov(d_in, size)
if band > 0.0:
k = st["rim"] * band
cr += (255.0 - cr) * k
cg += (255.0 - cg) * k
cb += (255.0 - cb) * k
# 字形:托盘(略细)+ 箭头(主调)
gu = 0.5 + (u - 0.5) / grow
for group, tone, k in ((tray, tray_tone, tray_k), (arrow, 1.0, 1.0)):
d = 1e9
rg = gr * k
for (ax, ay, bx, by) in group:
dd = sd_seg(gu, gv, ax, ay, bx, by) - rg
if dd < d:
d = dd
ag = cov(d, size) * a * tone
if ag > 0.0:
cr += (WHITE[0] - cr) * ag
cg += (WHITE[1] - cg) * ag
cb += (WHITE[2] - cb) * ag
i = row_off + px * 4
buf[i] = int(cr + 0.5)
buf[i + 1] = int(cg + 0.5)
buf[i + 2] = int(cb + 0.5)
buf[i + 3] = int(a * 255.0 + 0.5)
return buf
# ----------------------------------------------------------------- PNG / ICO
def png_bytes(w, h, rgba):
raw = bytearray()
for y in range(h):
raw.append(0)
raw += rgba[y * w * 4:(y + 1) * w * 4]
def chunk(tag, data):
return (struct.pack(">I", len(data)) + tag + data
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr)
+ chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + chunk(b"IEND", b""))
def dib_bytes(w, h, rgba):
"""32bpp 自下而上的 BMP(ICO 里的 DIB,含 AND 掩码)。"""
xor = bytearray()
for y in range(h - 1, -1, -1):
for x in range(w):
i = (y * w + x) * 4
xor += bytes((rgba[i + 2], rgba[i + 1], rgba[i], rgba[i + 3]))
and_stride = ((w + 31) // 32) * 4
and_mask = bytes(and_stride * h)
hdr = struct.pack("<IiiHHIIiiII", 40, w, h * 2, 1, 32, 0,
len(xor) + len(and_mask), 0, 0, 0, 0)
return hdr + bytes(xor) + and_mask
def ico_bytes(frames):
n = len(frames)
out = [struct.pack("<HHH", 0, 1, n)]
offset = 6 + 16 * n
body = b""
for size, payload in frames:
b = 0 if size >= 256 else size
out.append(struct.pack("<BBBBHHII", b, b, 0, 0, 1, 32, len(payload),
offset + len(body)))
body += payload
return b"".join(out) + body
# ----------------------------------------------------------------- 预览拼版
def blit(dst, dw, dh, src, sw, sh, ox, oy, scale=1):
for y in range(sh * scale):
sy = y // scale
dy = oy + y
if dy >= dh:
break
for x in range(sw * scale):
sx = x // scale
dx = ox + x
if dx >= dw:
break
si = (sy * sw + sx) * 4
di = (dy * dw + dx) * 4
dst[di:di + 4] = src[si:si + 4]
def preview(renders, path):
pad = 10
big = renders[256]
strip = SIZES[:-1] # 16..128
zoom = 4
strip_w = sum(s * zoom + pad for s in strip)
w = max(256 + 2 * pad, strip_w + pad)
h = 256 + strip_w * 0 + 64 + 128 * zoom + 3 * pad
h = 256 + pad * 3 + 128 * zoom
canvas = bytearray(w * h * 4)
for i in range(w * h):
canvas[i * 4 + 0] = 0xF3
canvas[i * 4 + 1] = 0xF4
canvas[i * 4 + 2] = 0xF6
canvas[i * 4 + 3] = 0xFF
blit(canvas, w, h, big, 256, 256, pad, pad, 1)
x = pad
y = 256 + pad * 2
for s in strip:
blit(canvas, w, h, renders[s], s, s, x, y + (128 - s) * zoom, zoom)
x += s * zoom + pad
with open(path, "wb") as f:
f.write(png_bytes(w, h, canvas))
return w, h
# ----------------------------------------------------------------- main
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="TfsExcelImporter/app.ico")
ap.add_argument("--preview", default="tools/icon_preview.png")
ap.add_argument("--only-preview", action="store_true")
ap.add_argument("--variant", default="arrow", choices=["arrow", "tray", "slot"])
args = ap.parse_args()
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
out_ico = args.out if os.path.isabs(args.out) else os.path.join(root, args.out)
out_png = (args.preview if os.path.isabs(args.preview)
else os.path.join(root, args.preview))
if args.variant != "arrow":
base, ext = os.path.splitext(out_png)
out_png = "%s_%s%s" % (base, args.variant, ext)
renders = {}
for s in SIZES:
renders[s] = render(s, args.variant)
print("rendered %dx%d" % (s, s))
frames = [(s, png_bytes(s, s, renders[s]) if s >= 256
else dib_bytes(s, s, renders[s])) for s in SIZES]
if not args.only_preview:
with open(out_ico, "wb") as f:
f.write(ico_bytes(frames))
print("wrote %s (%d bytes)" % (out_ico, os.path.getsize(out_ico)))
with open(os.path.splitext(out_png)[0] + "_256.png", "wb") as f:
f.write(png_bytes(256, 256, renders[256]))
w, h = preview(renders, out_png)
print("wrote %s (%dx%d)" % (out_png, w, h))
if __name__ == "__main__":
main()