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')
}
/** 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)
header.writeUInt16LE(0, 0) // reserved
header.writeUInt16LE(1, 2) // type: icon
header.writeUInt16LE(1, 4) // image count
const entry = Buffer.alloc(16)
entry.writeUInt8(0, 0) // width (0 => 256)
entry.writeUInt8(0, 1) // height (0 => 256)
entry.writeUInt8(0, 2) // palette
entry.writeUInt8(0, 3) // reserved
entry.writeUInt16LE(1, 4) // color planes
entry.writeUInt16LE(32, 6) // bits per pixel
entry.writeUInt32LE(pngBuffer.length, 8) // size of image data
entry.writeUInt32LE(6 + 16, 12) // offset to image data
return Buffer.concat([header, entry, pngBuffer])
header.writeUInt16LE(count, 4) // image count
const dir = Buffer.alloc(16 * count)
let offset = 6 + 16 * count
pngBuffers.forEach((png, i) => {
const w = png.readUInt32BE(16) // PNG IHDR width (byte 16..19)
const h = png.readUInt32BE(20) // PNG IHDR height (byte 20..23)
const e = i * 16
dir.writeUInt8(w >= 256 ? 0 : w, e + 0) // width (0 => 256)
dir.writeUInt8(h >= 256 ? 0 : h, e + 1) // height (0 => 256)
dir.writeUInt8(0, e + 2) // palette
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() {
@@ -150,9 +164,12 @@ function buildAppIcons() {
}
execFileSync('iconutil', ['-c', 'icns', iconset, '-o', path.join(BUILD_DIR, 'icon.icns')])
// .ico from a 256px render.
const ico256 = svgToPng(squareIconSvg(BRAND_BLUE), 'app-ico', ['-w', '256', '-h', '256'])
fs.writeFileSync(path.join(BUILD_DIR, 'icon.ico'), pngToIco(fs.readFileSync(ico256)))
// .ico with every size Windows asks for (taskbar/Explorer/Alt-Tab/tiles).
const icoSizes = [16, 24, 32, 48, 64, 128, 256]
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() {