feat(icons): enhance .ico generation to support multiple sizes for better clarity
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 51s
Desktop Client Build / Resolve Build Metadata (push) Successful in 20s
Desktop Client Build / Build Desktop Client (push) Successful in 23m20s

This commit is contained in:
Xu Liang
2026-05-31 14:01:46 +08:00
parent bdad050a8b
commit 2ea806e635
2 changed files with 33 additions and 16 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 22 KiB

@@ -103,22 +103,36 @@ function b64(pngPath) {
return fs.readFileSync(pngPath).toString('base64') return fs.readFileSync(pngPath).toString('base64')
} }
/** Wrap a PNG buffer in a minimal (PNG-payload) .ico container. */ /**
function pngToIco(pngBuffer) { * Wrap N square PNG buffers into a multi-image (PNG-payload) .ico container.
* Windows picks the closest size when rendering at 16/24/32/48/256 px, so a
* single 256px entry forces a blurry downscale in the taskbar/Explorer — and
* the old rcedit electron-builder ships with mis-stamps some single-entry ICOs.
* Embedding every common size keeps the app icon crisp at all of them.
*/
function pngsToIco(pngBuffers) {
const count = pngBuffers.length
const header = Buffer.alloc(6) const header = Buffer.alloc(6)
header.writeUInt16LE(0, 0) // reserved header.writeUInt16LE(0, 0) // reserved
header.writeUInt16LE(1, 2) // type: icon header.writeUInt16LE(1, 2) // type: icon
header.writeUInt16LE(1, 4) // image count header.writeUInt16LE(count, 4) // image count
const entry = Buffer.alloc(16) const dir = Buffer.alloc(16 * count)
entry.writeUInt8(0, 0) // width (0 => 256) let offset = 6 + 16 * count
entry.writeUInt8(0, 1) // height (0 => 256) pngBuffers.forEach((png, i) => {
entry.writeUInt8(0, 2) // palette const w = png.readUInt32BE(16) // PNG IHDR width (byte 16..19)
entry.writeUInt8(0, 3) // reserved const h = png.readUInt32BE(20) // PNG IHDR height (byte 20..23)
entry.writeUInt16LE(1, 4) // color planes const e = i * 16
entry.writeUInt16LE(32, 6) // bits per pixel dir.writeUInt8(w >= 256 ? 0 : w, e + 0) // width (0 => 256)
entry.writeUInt32LE(pngBuffer.length, 8) // size of image data dir.writeUInt8(h >= 256 ? 0 : h, e + 1) // height (0 => 256)
entry.writeUInt32LE(6 + 16, 12) // offset to image data dir.writeUInt8(0, e + 2) // palette
return Buffer.concat([header, entry, pngBuffer]) dir.writeUInt8(0, e + 3) // reserved
dir.writeUInt16LE(1, e + 4) // color planes
dir.writeUInt16LE(32, e + 6) // bits per pixel
dir.writeUInt32LE(png.length, e + 8) // size of image data
dir.writeUInt32LE(offset, e + 12) // offset to image data
offset += png.length
})
return Buffer.concat([header, dir, ...pngBuffers])
} }
function buildAppIcons() { function buildAppIcons() {
@@ -150,9 +164,12 @@ function buildAppIcons() {
} }
execFileSync('iconutil', ['-c', 'icns', iconset, '-o', path.join(BUILD_DIR, 'icon.icns')]) execFileSync('iconutil', ['-c', 'icns', iconset, '-o', path.join(BUILD_DIR, 'icon.icns')])
// .ico from a 256px render. // .ico with every size Windows asks for (taskbar/Explorer/Alt-Tab/tiles).
const ico256 = svgToPng(squareIconSvg(BRAND_BLUE), 'app-ico', ['-w', '256', '-h', '256']) const icoSizes = [16, 24, 32, 48, 64, 128, 256]
fs.writeFileSync(path.join(BUILD_DIR, 'icon.ico'), pngToIco(fs.readFileSync(ico256))) const icoPngs = icoSizes.map((s) =>
fs.readFileSync(svgToPng(squareIconSvg(BRAND_BLUE), `app-ico-${s}`, ['-w', String(s), '-h', String(s)])),
)
fs.writeFileSync(path.join(BUILD_DIR, 'icon.ico'), pngsToIco(icoPngs))
} }
function buildMainProcessIcons() { function buildMainProcessIcons() {