104 lines
3.0 KiB
Go
104 lines
3.0 KiB
Go
|
|
// 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"
|
|||
|
|
|
|||
|
|
"github.com/rs/zerolog/log"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// loadIcon 加载托盘图标。
|
|||
|
|
// 优先从 image.png 文件读取,回退到内嵌 iconData。
|
|||
|
|
// 解码 PNG → 缩放到 32x32 → 重新编码 → 包装为 ICO 格式返回。
|
|||
|
|
func loadIcon(iconData []byte) []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
|
|||
|
|
}
|