fix(packaging): 修复 Windows 安装与固件打包并完善客户端文案
- fix(installer): 修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签 - fix(firmware): 从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序 - fix(installer): 修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo - style(client): 应用图标增加圆角,统一“裁判系统客户端”命名 - fix(config): 安装提示改用可选本地热点配置,排除 Wi-Fi 凭据 - docs: 润色项目说明并统一裁判系统术语 - test(packaging): 补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成 验证:Windows NSIS 构建及原生页面预览;通用与本地安装配置编译;固件 release 构建、重复打包和客户端离线校验;6 项固件打包回归与 Windows 权限回归;git diff --check。未执行实机烧录。
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""Render the existing SVG brand sources with Tauri, then export PNG/ICO/ICNS/BMP.
|
||||
|
||||
Requires the installed desktop Node dependencies and Python's Pillow package.
|
||||
Text is rasterized with Pillow so exports do not depend on the SVG renderer's
|
||||
font discovery. No dependency is added to the application or its build hooks.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
DESKTOP = Path(__file__).resolve().parents[1]
|
||||
ICONS = DESKTOP / "src-tauri/icons"
|
||||
INSTALLER = DESKTOP / "src-tauri/installer"
|
||||
SVG = "{http://www.w3.org/2000/svg}"
|
||||
ET.register_namespace("", SVG[1:-1])
|
||||
|
||||
|
||||
def system_font(monospace=False, bold=False):
|
||||
fonts = Path(os.environ.get("WINDIR", "C:/Windows")) / "Fonts"
|
||||
candidates = (
|
||||
[fonts / ("consolab.ttf" if bold else "consola.ttf"),
|
||||
Path("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf")]
|
||||
if monospace else
|
||||
[fonts / ("msyhbd.ttc" if bold else "msyh.ttc"),
|
||||
Path("/System/Library/Fonts/PingFang.ttc"),
|
||||
Path("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc")]
|
||||
)
|
||||
for path in candidates:
|
||||
if path.exists():
|
||||
return path
|
||||
raise RuntimeError("Set --sans-font and --mono-font to installed fonts supporting the artwork's text.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--sans-font", type=Path)
|
||||
parser.add_argument("--mono-font", type=Path)
|
||||
args = parser.parse_args()
|
||||
node = shutil.which("node")
|
||||
cli = DESKTOP / "node_modules/@tauri-apps/cli/tauri.js"
|
||||
if not node or not cli.exists():
|
||||
raise RuntimeError("Install Node and run pnpm install --frozen-lockfile in desktop/ first.")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="rmic-brand-") as temporary:
|
||||
scratch = Path(temporary)
|
||||
|
||||
def render(root, size):
|
||||
source = scratch / "source.svg"
|
||||
ET.ElementTree(root).write(source, encoding="utf-8", xml_declaration=True)
|
||||
subprocess.run([node, str(cli), "icon", str(source), "--output", str(scratch),
|
||||
"--png", str(size)], cwd=DESKTOP, check=True, capture_output=True)
|
||||
with Image.open(scratch / f"{size}x{size}.png") as image:
|
||||
return image.convert("RGBA")
|
||||
|
||||
original = ET.parse(ICONS / "icon.svg").getroot()
|
||||
images = {}
|
||||
for size in (16, 20, 24, 32, 48, 64, 128, 256, 512, 1024):
|
||||
root = copy.deepcopy(original)
|
||||
scale = 0.97 if size <= 48 else 0.88 if size <= 128 else 0.82
|
||||
group = root.find(f"{SVG}g")
|
||||
group.set("transform", group.get("transform").replace("scale(0.82)", f"scale({scale})"))
|
||||
images[size] = render(root, size)
|
||||
for filename, size in (("32x32.png", 32), ("128x128.png", 128),
|
||||
("128x128@2x.png", 256), ("icon.png", 512)):
|
||||
images[size].save(ICONS / filename)
|
||||
|
||||
def png(size):
|
||||
stream = io.BytesIO()
|
||||
images[size].save(stream, format="PNG")
|
||||
return stream.getvalue()
|
||||
|
||||
# Supply every ICO size separately, preserving optical scaling at 16–48px.
|
||||
entries, payloads = [], []
|
||||
sizes = (16, 20, 24, 32, 48, 64, 128, 256)
|
||||
offset = 6 + 16 * len(sizes)
|
||||
for size in sizes:
|
||||
data = png(size)
|
||||
entries.append(struct.pack("<BBBBHHII", size % 256, size % 256, 0, 0, 1, 32, len(data), offset))
|
||||
payloads.append(data)
|
||||
offset += len(data)
|
||||
(ICONS / "icon.ico").write_bytes(struct.pack("<HHH", 0, 1, len(sizes)) + b"".join(entries + payloads))
|
||||
chunks = []
|
||||
for kind, size in ((b"icp4", 16), (b"icp5", 32), (b"icp6", 64), (b"ic07", 128),
|
||||
(b"ic08", 256), (b"ic09", 512), (b"ic10", 1024),
|
||||
(b"ic11", 32), (b"ic12", 64), (b"ic13", 256), (b"ic14", 512)):
|
||||
data = png(size)
|
||||
chunks.append(kind + struct.pack(">I", 8 + len(data)) + data)
|
||||
body = b"".join(chunks)
|
||||
(ICONS / "icon.icns").write_bytes(b"icns" + struct.pack(">I", 8 + len(body)) + body)
|
||||
|
||||
for name in ("header", "sidebar"):
|
||||
root = ET.parse(INSTALLER / f"{name}.svg").getroot()
|
||||
width, height = int(root.get("width")), int(root.get("height"))
|
||||
text = []
|
||||
for element in list(root):
|
||||
if element.tag == f"{SVG}text":
|
||||
text.append(element)
|
||||
root.remove(element)
|
||||
elif element.tag == f"{SVG}image":
|
||||
# Installer art may reference the unframed original mark.
|
||||
# Preserve its own viewBox instead of replacing every image
|
||||
# with the square application icon.
|
||||
source = (INSTALLER / element.get("href")).resolve()
|
||||
logo = ET.parse(source).getroot()
|
||||
for attr in ("x", "y", "width", "height"):
|
||||
logo.set(attr, element.get(attr))
|
||||
logo.set("preserveAspectRatio", "xMidYMid meet")
|
||||
index = list(root).index(element)
|
||||
root.remove(element)
|
||||
root.insert(index, logo)
|
||||
edge = max(width, height)
|
||||
root.set("width", str(edge))
|
||||
root.set("height", str(edge))
|
||||
root.set("viewBox", f"0 0 {edge} {edge}")
|
||||
factor = 4
|
||||
image = render(root, edge * factor)
|
||||
draw = ImageDraw.Draw(image)
|
||||
for element in text:
|
||||
mono = element.get("font-family") == "monospace"
|
||||
bold = element.get("font-weight") == "700"
|
||||
font_path = (args.mono_font if mono else args.sans_font) or system_font(mono, bold)
|
||||
font = ImageFont.truetype(str(font_path), round(float(element.get("font-size")) * factor))
|
||||
draw.text((float(element.get("x")) * factor, float(element.get("y")) * factor),
|
||||
element.text, font=font, fill=element.get("fill"), anchor="ls")
|
||||
image = image.crop((0, 0, width * factor, height * factor)).convert("RGB")
|
||||
image.resize((width, height), Image.Resampling.LANCZOS).save(INSTALLER / f"{name}.png")
|
||||
# MUI scales these to the dialog's font/DPI. Supply 2x bitmaps so
|
||||
# Windows does not have to enlarge the raster text on modern screens.
|
||||
image.resize((width * 2, height * 2), Image.Resampling.LANCZOS).save(INSTALLER / f"{name}.bmp")
|
||||
print("Generated rounded PNG/ICO/ICNS icons and 24-bit NSIS header/sidebar artwork.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user