// gen_ico produces a multi-resolution .ico file from a single PNG // source. The output embeds each size as a PNG payload (Vista+ .ico // format) so the result stays small and crisp. // // Usage: // // go run ./tools/gen_ico [src.png] [dst.ico] // // Defaults: // // src = internal/ui/assets/app_icon.png // dst = internal/ui/assets/app_icon.ico // // After generating the .ico, embed it into the Windows binary via: // // go run github.com/akavel/rsrc@latest \ // -ico internal/ui/assets/app_icon.ico \ // -o internal/ui/assets/rsrc_windows.syso // // rsrc_windows.syso is auto-linked by `go build` on Windows. package main import ( "bytes" "encoding/binary" "fmt" "image" "image/png" "io" "os" "golang.org/x/image/draw" ) // icoSizes are the embedded resolutions. Windows picks the largest size // that fits the target use (title bar = 16, Alt-Tab = 32, Explorer = up // to 256) and scales down. Skipping sizes Windows can't read anyway // keeps the file under 200 KB. var icoSizes = []int{16, 24, 32, 48, 64, 128, 256} // icoEntry holds one image's dimensions and its encoded PNG bytes. type icoEntry struct { w, h int png []byte } func main() { srcPath := "internal/ui/assets/app_icon.png" dstPath := "internal/ui/assets/app_icon.ico" if len(os.Args) > 1 { srcPath = os.Args[1] } if len(os.Args) > 2 { dstPath = os.Args[2] } if err := run(srcPath, dstPath); err != nil { fmt.Fprintln(os.Stderr, "gen_ico:", err) os.Exit(1) } } func run(srcPath, dstPath string) error { srcF, err := os.Open(srcPath) if err != nil { return fmt.Errorf("open src: %w", err) } defer srcF.Close() src, _, err := image.Decode(srcF) if err != nil { return fmt.Errorf("decode src: %w", err) } var entries []icoEntry for _, size := range icoSizes { dst := image.NewNRGBA(image.Rect(0, 0, size, size)) // CatmullRom gives sharper results at small sizes than the // default nearest-neighbour. draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil) var buf bytes.Buffer if err := png.Encode(&buf, dst); err != nil { return fmt.Errorf("encode %dpx: %w", size, err) } entries = append(entries, icoEntry{w: size, h: size, png: buf.Bytes()}) } out, err := os.Create(dstPath) if err != nil { return fmt.Errorf("create dst: %w", err) } defer out.Close() return writeICO(out, entries) } // writeICO serialises a Vista+ PNG-based .ico file: ICONDIR + one // ICONDIRENTRY per image + the PNG bytes concatenated. func writeICO(w io.Writer, entries []icoEntry) error { if _, err := w.Write([]byte{ 0, 0, // reserved 1, 0, // type = 1 (icon) byte(len(entries)), 0, // count }); err != nil { return err } const dirHdrLen = 6 const dirEntryLen = 16 headerLen := dirHdrLen + dirEntryLen*len(entries) offset := headerLen for _, e := range entries { wByte := byte(e.w) if e.w >= 256 { // 0 means 256 in the .ico directory. wByte = 0 } hByte := byte(e.h) if e.h >= 256 { hByte = 0 } size := uint32(len(e.png)) entry := make([]byte, dirEntryLen) entry[0] = wByte entry[1] = hByte entry[2] = 0 // color palette entry[3] = 0 // reserved binary.LittleEndian.PutUint16(entry[4:], 1) // planes binary.LittleEndian.PutUint16(entry[6:], 32) // bit count binary.LittleEndian.PutUint32(entry[8:], size) binary.LittleEndian.PutUint32(entry[12:], uint32(offset)) if _, err := w.Write(entry); err != nil { return err } offset += int(size) } for _, e := range entries { if _, err := w.Write(e.png); err != nil { return err } } return nil }