diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index c014c856..e1280546 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -153,6 +153,29 @@ jobs: shell: bash run: bash packaging/build-runtime.sh + # ── 图标资产生成(rant 2026-08-11T18:28:09:仓库只保留 icon.svg 设计源, + # png/icns/ico 由 CI 构建时现生成,必须先于 GUI 构建执行)── + - name: Generate icon assets (icon.svg → png/icns/ico) + shell: bash + run: | + set -euo pipefail + if command -v rsvg-convert >/dev/null 2>&1; then + echo "rsvg-convert already available" + elif [ "$RUNNER_OS" = "Linux" ]; then + sudo apt-get update -qq && sudo apt-get install -y -qq librsvg2-bin + elif [ "$RUNNER_OS" = "macOS" ]; then + brew install librsvg + else + echo "no rsvg-convert; gen-assets.sh will fall back to Chrome/Edge headless" + fi + bash packaging/gen-assets.sh + # 产物必须存在(GUI 构建引用 ../packaging/assets/*.png/icns/ico) + test -s packaging/assets/icon.png + if [ "$RUNNER_OS" = "macOS" ]; then + test -s packaging/assets/icon.icns + fi + test -s packaging/assets/icon.ico + # ── GUI dist(electron-builder;linux extraResources runtime §5 R64)── - name: Build GUI (electron-builder) working-directory: emrg/gui diff --git a/.gitignore b/.gitignore index df54b9f7..98bba0a6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,10 @@ node_modules/ .pytest_cache/ /dist/ /emrg/gui/dist/ +# icon products generated at build time from packaging/assets/icon.svg (design source) +# (rant 2026-08-11T18:28:09: repo keeps only the SVG; png/icns/ico generated by gen-assets.sh) +packaging/assets/icon.png +packaging/assets/icon-512.png +packaging/assets/icon-256.png +packaging/assets/icon.icns +packaging/assets/icon.ico diff --git a/packaging/assets/icon-256.png b/packaging/assets/icon-256.png deleted file mode 100644 index 55902400..00000000 Binary files a/packaging/assets/icon-256.png and /dev/null differ diff --git a/packaging/assets/icon-512.png b/packaging/assets/icon-512.png deleted file mode 100644 index bc6179cf..00000000 Binary files a/packaging/assets/icon-512.png and /dev/null differ diff --git a/packaging/assets/icon.icns b/packaging/assets/icon.icns deleted file mode 100644 index 553a3ce3..00000000 Binary files a/packaging/assets/icon.icns and /dev/null differ diff --git a/packaging/assets/icon.ico b/packaging/assets/icon.ico deleted file mode 100644 index 96af25ae..00000000 Binary files a/packaging/assets/icon.ico and /dev/null differ diff --git a/packaging/assets/icon.png b/packaging/assets/icon.png deleted file mode 100644 index 794c9bca..00000000 Binary files a/packaging/assets/icon.png and /dev/null differ diff --git a/packaging/assets/icon.svg b/packaging/assets/icon.svg new file mode 100644 index 00000000..ee62a043 --- /dev/null +++ b/packaging/assets/icon.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/gen-assets.sh b/packaging/gen-assets.sh index 3f489548..a09d3d25 100644 --- a/packaging/gen-assets.sh +++ b/packaging/gen-assets.sh @@ -1,102 +1,89 @@ #!/usr/bin/env bash -# EMRG Phase 4 — generate packaging assets (icons). +# EMRG Phase 4 — generate packaging assets (icons) from the SVG design source. # -# Pure-stdlib approach (no PIL/ImageMagick dependency): -# 1. Python stdlib draws a 1024x1024 PNG (EMRG glyph: dark bg + green "E" + circuit node) +# Design source: packaging/assets/icon.svg ("Branch Emergence" — rant 2026-08-11T18:28:09) +# Pipeline: +# 1. Render icon.svg → 1024x1024 icon.png +# renderer priority: rsvg-convert (librsvg) → Chrome/Chromium headless → sips +# (sips 对 feGaussianBlur 支持有限 → 辉光丢失,仅作最后兜底并告警) # 2. iconutil (macOS) → .icns -# 3. Python stdlib → multi-size .ico (win) +# 3. Python stdlib (area-average box filter) → multi-size .ico (win) # 4. 512/256 PNG copies (linux) # # Output in packaging/assets/: -# icon.png (1024) icon-512.png icon-256.png icon.icns icon.ico +# icon.svg (design source, committed) icon.png (1024) icon-512.png +# icon-256.png icon.icns icon.ico (generated, gitignored) # # Run from repo root: bash packaging/gen-assets.sh set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" OUT="$ROOT/packaging/assets" +SVG="$OUT/icon.svg" mkdir -p "$OUT" -echo "==> generating 1024x1024 icon.png (pure stdlib)" +if [ ! -f "$SVG" ]; then + echo "ERROR: $SVG not found — design source missing" >&2 + exit 1 +fi + +echo "==> rendering icon.svg → 1024x1024 icon.png" +render_svg_to_png() { + # 1) rsvg-convert (librsvg) — full SVG + filter support + if command -v rsvg-convert >/dev/null 2>&1; then + echo " renderer: rsvg-convert" + rsvg-convert -w 1024 -h 1024 "$SVG" -o "$OUT/icon.png" + return 0 + fi + # 2) Chrome/Chromium headless --screenshot (full filter support) + # Windows (Git Bash): convert /c/... → file:///C:/... so Chrome resolves the URL. + SVG_URL="file://$SVG" + if command -v cygpath >/dev/null 2>&1; then + SVG_URL="file:///$(cygpath -m "$SVG")" + fi + for chrome in \ + "${CHROME_BIN:-}" \ + "google-chrome" "google-chrome-stable" "chromium" "chromium-browser" \ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + "/Applications/Chromium.app/Contents/MacOS/Chromium" \ + "/c/Program Files/Google/Chrome/Application/chrome.exe" \ + "/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"; do + if [ -n "$chrome" ] && command -v "$chrome" >/dev/null 2>&1 || [ -x "$chrome" ] 2>/dev/null; then + echo " renderer: $chrome (headless)" + "$chrome" --headless --disable-gpu --hide-scrollbars \ + --screenshot="$OUT/icon.png" --window-size=1024,1024 \ + --default-background-color=00000000 "$SVG_URL" >/dev/null 2>&1 + return 0 + fi + done + # 3) sips (macOS last resort) — 辉光(feGaussianBlur)可能丢失 + if command -v sips >/dev/null 2>&1; then + echo " ⚠️ renderer: sips (last resort) — glow may be lost (feGaussianBlur unsupported)" >&2 + sips -s format png "$SVG" --out "$OUT/icon.png" >/dev/null 2>&1 + return 0 + fi + echo "ERROR: no SVG renderer found (rsvg-convert / chrome / sips)" >&2 + return 1 +} +render_svg_to_png || exit 1 +# sanity: non-empty 1024x1024 PNG python3 - "$OUT/icon.png" <<'PYEOF' -import sys, struct, zlib, math - -path = sys.argv[1] -S = 1024 -# RGBA buffer (transparent bg) -buf = bytearray([0, 0, 0, 0]) * (S * S) - -def put(x, y, r, g, b, a=255): - if 0 <= x < S and 0 <= y < S: - i = (y * S + x) * 4 - buf[i] = r; buf[i+1] = g; buf[i+2] = b; buf[i+3] = a - -def fill_circle(cx, cy, rad, r, g, b, a=255): - # bounding box fill - x0, x1 = max(0, int(cx-rad)), min(S, int(cx+rad)+1) - y0, y1 = max(0, int(cy-rad)), min(S, int(cy+rad)+1) - r2 = rad*rad - for yy in range(y0, y1): - for xx in range(x0, x1): - dx, dy = xx-cx, yy-cy - if dx*dx + dy*dy <= r2: - put(xx, yy, r, g, b, a) - -def fill_rect(x0, y0, x1, y1, r, g, b, a=255): - for yy in range(max(0,y0), min(S,y1)): - for xx in range(max(0,x0), min(S,x1)): - put(xx, yy, r, g, b, a) - -def fill_round_rect(x0, y0, x1, y1, rad, r, g, b, a=255): - fill_rect(x0+rad, y0, x1-rad, y1, r, g, b, a) - fill_rect(x0, y0+rad, x1, y1-rad, r, g, b, a) - fill_circle(x0+rad, y0+rad, rad, r, g, b, a) - fill_circle(x1-rad, y0+rad, rad, r, g, b, a) - fill_circle(x0+rad, y1-rad, rad, r, g, b, a) - fill_circle(x1-rad, y1-rad, rad, r, g, b, a) - -# background: rounded square (EMRG brand: dark navy #0f172a) -fill_round_rect(64, 64, S-64, S-64, 180, 0x0f, 0x17, 0x2a) -# subtle outer ring -for rad in range(460, 480): - fill_circle(S//2, S//2, rad, 0x1e, 0x29, 0x3b, 255) - -# circuit: ring + node (self-evolving motif) -fill_circle(S//2, S//2, 300, 0x10, 0xb9, 0x81, 255) # green ring body -fill_circle(S//2, S//2, 250, 0x0f, 0x17, 0x2a, 255) # punch hole → ring -# 4 orbit nodes -for ang in (0, 90, 180, 270): - ox = S//2 + int(380 * math.cos(math.radians(ang))) - oy = S//2 + int(380 * math.sin(math.radians(ang))) - fill_circle(ox, oy, 40, 0x10, 0xb9, 0x81, 255) - -# "E" glyph (three bars + spine) — simple block letter -bar = 70 -spine_x0, spine_x1 = S//2 - 200, S//2 - 130 -top_y, mid_y, bot_y = S//2 - 200, S//2 - 30, S//2 + 140 -fill_rect(spine_x0, top_y, spine_x1, bot_y+bar, 0xf8, 0xfa, 0xfc) # vertical spine -fill_rect(spine_x0, top_y, S//2 + 200, top_y+bar, 0xf8, 0xfa, 0xfc) # top bar -fill_rect(spine_x0, mid_y, S//2 + 140, mid_y+bar, 0xf8, 0xfa, 0xfc) # middle bar -fill_rect(spine_x0, bot_y, S//2 + 200, bot_y+bar, 0xf8, 0xfa, 0xfc) # bottom bar - -def write_png(path, w, h, rgba): - def chunk(tag, data): - c = struct.pack(">I", len(data)) + tag + data - c += struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff) - return c - raw = b"".join(b"\x00" + bytes(rgba[y*w*4:(y+1)*w*4]) for y in range(h)) - png = b"\x89PNG\r\n\x1a\n" - png += chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)) - png += chunk(b"IDAT", zlib.compress(raw, 9)) - png += chunk(b"IEND", b"") - with open(path, "wb") as f: - f.write(png) - -write_png(path, S, S, buf) -print(" wrote", path) +import sys, struct +data = open(sys.argv[1], "rb").read() +assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a PNG" +pos = 8 +while pos < len(data): + ln = struct.unpack(">I", data[pos:pos+4])[0] + tag = data[pos+4:pos+8] + if tag == b"IHDR": + w, h = struct.unpack(">II", data[pos+8:pos+16]) + assert (w, h) == (1024, 1024), f"expected 1024x1024, got {w}x{h}" + print(f" icon.png OK: {w}x{h}") + break + pos += 12 + ln PYEOF -echo "==> resizing to 512/256 (stdlib scale-down)" +echo "==> resizing to 512/256 (stdlib area-average box filter)" python3 - "$OUT/icon.png" "$OUT" <<'PYEOF' import sys, zlib, struct @@ -124,20 +111,24 @@ def read_png(path): w, h, raw = read_png(src) stride = w * 4 -def sample(x, y): - i = y * stride + x * 4 - return raw[i], raw[i+1], raw[i+2], raw[i+3] - -def resize(nw, nh): +def resize_area(nw, nh): + """Area-average (box filter) downscale — better antialiasing than nearest.""" out = bytearray([0, 0, 0, 0]) * (nw * nh) for yy in range(nh): - sy = min(int(yy * h / nh), h-1) + y0 = yy * h // nh + y1 = max(y0 + 1, (yy + 1) * h // nh) for xx in range(nw): - sx = min(int(xx * w / nw), w-1) - i = sy * stride + sx * 4 - r, g, b, a = raw[i], raw[i+1], raw[i+2], raw[i+3] + x0 = xx * w // nw + x1 = max(x0 + 1, (xx + 1) * w // nw) + r = g = b = a = 0 + n = 0 + for sy in range(y0, y1): + for sx in range(x0, x1): + i = sy * stride + sx * 4 + r += raw[i]; g += raw[i+1]; b += raw[i+2]; a += raw[i+3] + n += 1 o = (yy * nw + xx) * 4 - out[o] = r; out[o+1] = g; out[o+2] = b; out[o+3] = a + out[o] = r // n; out[o+1] = g // n; out[o+2] = b // n; out[o+3] = a // n return bytes(out) def write_png(path, nw, nh, rgba): @@ -151,20 +142,19 @@ def write_png(path, nw, nh, rgba): for size in (512, 256): p = f"{outdir}/icon-{size}.png" - write_png(p, size, size, resize(size, size)) + write_png(p, size, size, resize_area(size, size)) print(" wrote", p) PYEOF -echo "==> iconutil → icon.icns (macOS)" -ICONSET="$OUT/icon.iconset" -mkdir -p "$ICONSET" -# iconutil wants specific sizes -for s in 16 32 128 256 512; do - d=$((s*2)) - python3 - "$OUT/icon.png" "$ICONSET/icon_${s}x${s}.png" "$s" <<'PYEOF' +echo "==> iconutil → icon.icns (macOS only; skipped on Linux/Windows)" +if command -v iconutil >/dev/null 2>&1; then + ICONSET="$OUT/icon.iconset" + mkdir -p "$ICONSET" + # iconutil wants specific sizes + for s in 16 32 128 256 512; do + python3 - "$OUT/icon.png" "$ICONSET/icon_${s}x${s}.png" "$s" <<'PYEOF' import sys, zlib, struct src, dst, size = sys.argv[1], sys.argv[2], int(sys.argv[3]) -# reuse resize from above by re-reading data = open(src, "rb").read(); pos, w, h, idat = 8, 0, 0, b"" while pos < len(data): ln = struct.unpack(">I", data[pos:pos+4])[0]; tag = data[pos+4:pos+8]; chunk = data[pos+8:pos+8+ln] @@ -175,27 +165,36 @@ raw = zlib.decompress(idat) stride0 = w*4 + 1 raw = b"".join(raw[y*stride0+1:(y+1)*stride0] for y in range(h)) stride = w*4 -out = bytearray([0,0,0,0])*(size*size) -for yy in range(size): - sy = yy*h//size - for xx in range(size): - sx = min(xx*w//size, w-1) - i = sy*stride + sx*4 - o = (yy*size+xx)*4 - out[o]=raw[i]; out[o+1]=raw[i+1]; out[o+2]=raw[i+2]; out[o+3]=raw[i+3] +def resize_area(nw, nh): + out = bytearray([0,0,0,0])*(nw*nh) + for yy in range(nh): + y0 = yy*h//nh; y1 = max(y0+1, (yy+1)*h//nh) + for xx in range(nw): + x0 = xx*w//nw; x1 = max(x0+1, (xx+1)*w//nw) + r = g = b = a = n = 0 + for sy in range(y0, y1): + for sx in range(x0, x1): + i = sy*stride + sx*4 + r += raw[i]; g += raw[i+1]; b += raw[i+2]; a += raw[i+3]; n += 1 + o = (yy*nw+xx)*4 + out[o] = r//n; out[o+1] = g//n; out[o+2] = b//n; out[o+3] = a//n + return bytes(out) def chunk(tag, data): c = struct.pack(">I", len(data))+tag+data; c += struct.pack(">I", zlib.crc32(tag+data)&0xffffffff); return c -rgba = bytes(out) +rgba = resize_area(size, size) raw2 = b"".join(b"\x00"+rgba[y*size*4:(y+1)*size*4] for y in range(size)) png = b"\x89PNG\r\n\x1a\n"+chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0))+chunk(b"IDAT", zlib.compress(raw2,9))+chunk(b"IEND", b"") open(dst, "wb").write(png) PYEOF - cp "$ICONSET/icon_${s}x${s}.png" "$ICONSET/icon_${s}x${s}@2x.png" 2>/dev/null || true -done -# icns needs exact 16/32/128/256/512 + @2x -iconutil -c icns "$ICONSET" -o "$OUT/icon.icns" -rm -rf "$ICONSET" -echo " wrote $OUT/icon.icns" + cp "$ICONSET/icon_${s}x${s}.png" "$ICONSET/icon_${s}x${s}@2x.png" 2>/dev/null || true + done + # icns needs exact 16/32/128/256/512 + @2x + iconutil -c icns "$ICONSET" -o "$OUT/icon.icns" + rm -rf "$ICONSET" + echo " wrote $OUT/icon.icns" +else + echo " skipping icon.icns (iconutil unavailable — macOS only)" +fi echo "==> icon.ico (multi-size, win)" python3 - "$OUT/icon.png" "$OUT/icon.ico" <<'PYEOF' @@ -212,15 +211,19 @@ stride0 = w*4 + 1 raw = b"".join(raw[y*stride0+1:(y+1)*stride0] for y in range(h)) stride = w*4 -def resize(nw, nh): +def resize_area(nw, nh): out = bytearray([0,0,0,0])*(nw*nh) for yy in range(nh): - sy = min(yy*h//nh, h-1) + y0 = yy*h//nh; y1 = max(y0+1, (yy+1)*h//nh) for xx in range(nw): - sx = min(xx*w//nw, w-1) - i = sy*stride + sx*4 + x0 = xx*w//nw; x1 = max(x0+1, (xx+1)*w//nw) + r = g = b = a = n = 0 + for sy in range(y0, y1): + for sx in range(x0, x1): + i = sy*stride + sx*4 + r += raw[i]; g += raw[i+1]; b += raw[i+2]; a += raw[i+3]; n += 1 o = (yy*nw+xx)*4 - out[o]=raw[i]; out[o+1]=raw[i+1]; out[o+2]=raw[i+2]; out[o+3]=raw[i+3] + out[o] = r//n; out[o+1] = g//n; out[o+2] = b//n; out[o+3] = a//n return bytes(out) def png_bytes(nw, nh, rgba): @@ -231,7 +234,7 @@ def png_bytes(nw, nh, rgba): # ICO: header + directory + PNG-embedded entries sizes = [16, 24, 32, 48, 64, 128, 256] -imgs = [(s, png_bytes(s, s, resize(s, s))) for s in sizes] +imgs = [(s, png_bytes(s, s, resize_area(s, s))) for s in sizes] header = struct.pack("