diff --git a/apps/desktop-client/build/icon.ico b/apps/desktop-client/build/icon.ico index daa925f..36c8eb8 100644 Binary files a/apps/desktop-client/build/icon.ico and b/apps/desktop-client/build/icon.ico differ diff --git a/apps/desktop-client/scripts/generate-brand-icons.cjs b/apps/desktop-client/scripts/generate-brand-icons.cjs index af95b35..6ff52f7 100644 --- a/apps/desktop-client/scripts/generate-brand-icons.cjs +++ b/apps/desktop-client/scripts/generate-brand-icons.cjs @@ -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() {