// Copyright (c) 2026 S.A. (SNOWARE). Written for SCU Team. // Licensed under MulanPubL-2.0. // You may obtain a copy at: http://license.coscl.org.cn/MulanPubL-2.0 // THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND. // trayicon_windows 模块:托盘图标加载,将 PNG 解码、缩放并包装为 Windows ICO 格式。 // getlantern/systray 在 Windows 上存在 GDI+ → CreateIconIndirect 兼容性 bug, // 解法是将 PNG 包装成 ICO 格式(现代 Windows 原生支持 ICO 内嵌 PNG)。 package main import ( "bytes" "encoding/binary" "image" "image/png" "os" "path/filepath" ) // loadIcon 加载托盘图标。 // 优先从 image.png 文件读取,回退到内嵌 iconData。 // 解码 PNG → 缩放到 32x32 → 重新编码 → 包装为 ICO 格式返回。 func loadIcon() []byte { raw := iconData exePath, err := os.Executable() if err == nil { iconFile := filepath.Join(filepath.Dir(exePath), "image.png") if data, err := os.ReadFile(iconFile); err == nil { raw = data log.Debug().Str("file", iconFile).Msg("从文件读取图标") } } img, err := png.Decode(bytes.NewReader(raw)) if err != nil { log.Warn().Err(err).Msg("PNG 解码失败") return nil } icon := resizeIcon(img, 32, 32) var pngBuf bytes.Buffer if err := png.Encode(&pngBuf, icon); err != nil { log.Warn().Err(err).Msg("PNG 重新编码失败") return nil } ico := pngToICO(pngBuf.Bytes(), 32, 32) log.Debug().Int("size", len(ico)).Msg("托盘图标已生成 (ICO 32x32)") return ico } // pngToICO 将 PNG 字节数据包装为 Windows ICO 文件格式。 // ICO 结构:6 字节文件头 + 16 字节目录项 + PNG 图像数据。 func pngToICO(pngData []byte, w, h int) []byte { var buf bytes.Buffer // ICO 文件头 binary.Write(&buf, binary.LittleEndian, uint16(0)) // 保留 binary.Write(&buf, binary.LittleEndian, uint16(1)) // 类型: ICO binary.Write(&buf, binary.LittleEndian, uint16(1)) // 图标数量 // 处理 256 边界的宽度/高度 width := uint8(w) if w >= 256 { width = 0 } height := uint8(h) if h >= 256 { height = 0 } // ICO 目录项 binary.Write(&buf, binary.LittleEndian, width) binary.Write(&buf, binary.LittleEndian, height) binary.Write(&buf, binary.LittleEndian, uint8(0)) binary.Write(&buf, binary.LittleEndian, uint8(0)) binary.Write(&buf, binary.LittleEndian, uint16(1)) binary.Write(&buf, binary.LittleEndian, uint16(32)) binary.Write(&buf, binary.LittleEndian, uint32(len(pngData))) binary.Write(&buf, binary.LittleEndian, uint32(22)) buf.Write(pngData) return buf.Bytes() } // resizeIcon 使用最近邻插值将图像缩放到指定尺寸。 // 返回 *image.RGBA 避免与原图共享底层数据。 func resizeIcon(src image.Image, w, h int) *image.RGBA { dst := image.NewRGBA(image.Rect(0, 0, w, h)) sb := src.Bounds() for y := range h { for x := range w { sx := sb.Min.X + x*sb.Dx()/w sy := sb.Min.Y + y*sb.Dy()/h dst.Set(x, y, src.At(sx, sy)) } } return dst }