// Command icongen 根据一张图片快速生成当前项目的托盘图标与打包图标, // 已存在的图标文件会被直接覆盖。 // // 用法(在项目根目录执行): // // go run ./pkg/icongen -src /path/to/image.png // // 可选参数: // // go run ./pkg/icongen -src logo.jpg -root . -appicon 1024 -tray 256 package main import ( "bytes" "encoding/binary" "flag" "fmt" "image" imagedraw "image/draw" "image/png" "log" "os" "path/filepath" "golang.org/x/image/draw" _ "golang.org/x/image/webp" _ "image/gif" _ "image/jpeg" ) // ico 文件中包含的尺寸 var icoSizes = []int{256, 128, 64, 48, 32, 16} func main() { src := flag.String("src", "", "源图片路径(png/jpg/jpeg/gif/bmp/webp)") root := flag.String("root", ".", "项目根目录") appicon := flag.Int("appicon", 1024, "build/appicon.png 尺寸") icon := flag.Int("icon", 1024, "build/icon.png 尺寸") tray := flag.Int("tray", 256, "trayicons/assets 图标尺寸") flag.Parse() if *src == "" { log.Fatal("[-src] 源图片路径不能为空") } f, err := os.Open(*src) if err != nil { log.Fatalf("打开源图片失败: %v", err) } defer f.Close() srcImg, _, err := image.Decode(f) if err != nil { log.Fatalf("解码源图片失败(支持 png/jpg/gif/bmp/webp): %v", err) } log.Printf("源图片: %s (%dx%d)", *src, srcImg.Bounds().Dx(), srcImg.Bounds().Dy()) cropped := centerCrop(srcImg) // PNG 目标:源图居中裁剪后缩放 writePNG(filepath.Join(*root, "build", "appicon.png"), cropped, *appicon) writePNG(filepath.Join(*root, "build", "icon.png"), cropped, *icon) writePNG(filepath.Join(*root, "assets", "icon.png"), cropped, *tray) writePNG(filepath.Join(*root, "trayicons", "icon.png"), cropped, *tray) // ICO 目标:Windows exe / 托盘图标(多尺寸) writeICO(filepath.Join(*root, "build", "windows", "icon.ico"), cropped, icoSizes) log.Println("完成,所有图标已按需覆盖。") } // centerCrop 从图像中心裁剪出正方形区域。 func centerCrop(srcImg image.Image) image.Image { b := srcImg.Bounds() w, h := b.Dx(), b.Dy() side := w if h < side { side = h } x0 := b.Min.X + (w-side)/2 y0 := b.Min.Y + (h-side)/2 rect := image.Rect(0, 0, side, side) out := image.NewRGBA(rect) imagedraw.Draw(out, rect, srcImg, image.Pt(x0, y0), imagedraw.Src) return out } // writePNG 按 size 缩放后写出 PNG 文件。 func writePNG(path string, cropped image.Image, size int) { dst := image.NewRGBA(image.Rect(0, 0, size, size)) draw.CatmullRom.Scale(dst, dst.Bounds(), cropped, cropped.Bounds(), draw.Over, nil) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { log.Fatalf("创建目录失败 %s: %v", filepath.Dir(path), err) } out, err := os.Create(path) if err != nil { log.Fatalf("创建文件失败 %s: %v", path, err) } defer out.Close() if err := png.Encode(out, dst); err != nil { log.Fatalf("编码 PNG 失败 %s: %v", path, err) } fmt.Printf(" [ok] %s (%dx%d)\n", path, size, size) } // writeICO 写出多尺寸 ICO(Windows 图标,PNG 条目压缩,Vista+ 支持)。 func writeICO(path string, cropped image.Image, sizes []int) { type entry struct { w, h byte data []byte } entries := make([]entry, 0, len(sizes)) for _, s := range sizes { dst := image.NewRGBA(image.Rect(0, 0, s, s)) draw.CatmullRom.Scale(dst, dst.Bounds(), cropped, cropped.Bounds(), draw.Over, nil) var pngBuf bytes.Buffer if err := png.Encode(&pngBuf, dst); err != nil { log.Fatalf("编码 ICO 条目失败 (%dpx): %v", s, err) } w, h := byte(s), byte(s) if s == 256 { w, h = 0, 0 // 0 表示 256 } entries = append(entries, entry{w: w, h: h, data: pngBuf.Bytes()}) } var buf bytes.Buffer // ICONDIR(6 字节头) binary.Write(&buf, binary.LittleEndian, uint16(0)) // Reserved binary.Write(&buf, binary.LittleEndian, uint16(1)) // Type: icon binary.Write(&buf, binary.LittleEndian, uint16(len(entries))) // ICONDIRENTRY(16 字节 x N) cur := 6 + 16*len(entries) for _, e := range entries { binary.Write(&buf, binary.LittleEndian, e.w) binary.Write(&buf, binary.LittleEndian, e.h) 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(e.data))) binary.Write(&buf, binary.LittleEndian, uint32(cur)) cur += len(e.data) } // PNG 图像数据 for _, e := range entries { buf.Write(e.data) } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { log.Fatalf("创建目录失败 %s: %v", filepath.Dir(path), err) } if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil { log.Fatalf("写入 ICO 失败 %s: %v", path, err) } fmt.Printf(" [ok] %s (ico %v)\n", path, sizes) }