//go:build ignore // gen renders the talk icon from assets/icon/talk.svg into everything the // build embeds, so no resource compiler is needed: // // internal/icon/talk.png 256 px, embedded in the binary (toast // and notification icon) // assets/icon/talk.ico multi-size Windows icon // cmd/talk/rsrc_windows_.syso COFF resource object with the icon; // go build links it into talk.exe // // Run from the repository root (needs inkscape in PATH): // // go run assets/icon/gen.go package main import ( "bytes" "encoding/binary" "fmt" "image/color" "image/png" "os" "os/exec" "path/filepath" ) // sizes are the icon sizes Windows asks for (Explorer, taskbar, Alt-Tab, // toast header); 256 is stored as PNG, the rest as plain bitmaps. var sizes = []int{16, 20, 24, 32, 40, 48, 64, 128, 256} const svgPath = "assets/icon/talk.svg" // iconImage is one size of the icon in the form ICO files and RT_ICON // resources store it. type iconImage struct { size int data []byte // PNG (256) or BITMAPINFOHEADER + pixels + mask } func main() { if _, err := os.Stat("go.mod"); err != nil { fail("run from the repository root: %v", err) } tmp, err := os.MkdirTemp("", "talk-icon-") if err != nil { fail("%v", err) } defer os.RemoveAll(tmp) var images []iconImage for _, s := range sizes { out := filepath.Join(tmp, fmt.Sprintf("%d.png", s)) cmd := exec.Command("inkscape", svgPath, "-w", fmt.Sprint(s), "-h", fmt.Sprint(s), "-o", out) if b, err := cmd.CombinedOutput(); err != nil { fail("inkscape %d px: %v: %s", s, err, b) } b, err := os.ReadFile(out) if err != nil { fail("%v", err) } if s == 256 { write("internal/icon/talk.png", b) images = append(images, iconImage{s, b}) continue } dib, err := toDIB(b) if err != nil { fail("%d px: %v", s, err) } images = append(images, iconImage{s, dib}) } write("assets/icon/talk.ico", buildICO(images)) for arch, machine := range map[string]uint16{"amd64": 0x8664, "arm64": 0xAA64} { write("cmd/talk/rsrc_windows_"+arch+".syso", buildSyso(machine, images)) } } func write(path string, b []byte) { if err := os.WriteFile(path, b, 0o644); err != nil { fail("%v", err) } fmt.Printf("wrote %s (%d bytes)\n", path, len(b)) } func fail(format string, a ...any) { fmt.Fprintf(os.Stderr, "gen: "+format+"\n", a...) os.Exit(1) } // toDIB converts a PNG into the classic icon bitmap: BITMAPINFOHEADER with // doubled height, 32-bit BGRA rows bottom-up, then the 1-bit AND mask. func toDIB(pngData []byte) ([]byte, error) { img, err := png.Decode(bytes.NewReader(pngData)) if err != nil { return nil, err } b := img.Bounds() w, h := b.Dx(), b.Dy() maskRow := ((w + 31) / 32) * 4 var buf bytes.Buffer hdr := []uint32{40, uint32(w), uint32(2 * h)} binary.Write(&buf, binary.LittleEndian, hdr) binary.Write(&buf, binary.LittleEndian, []uint16{1, 32}) // planes, bit count binary.Write(&buf, binary.LittleEndian, []uint32{0, uint32(w*h*4 + maskRow*h), 0, 0, 0, 0}) for y := h - 1; y >= 0; y-- { for x := 0; x < w; x++ { c := color.NRGBAModel.Convert(img.At(b.Min.X+x, b.Min.Y+y)).(color.NRGBA) buf.Write([]byte{c.B, c.G, c.R, c.A}) } } for y := h - 1; y >= 0; y-- { row := make([]byte, maskRow) for x := 0; x < w; x++ { if _, _, _, a := img.At(b.Min.X+x, b.Min.Y+y).RGBA(); a == 0 { row[x/8] |= 0x80 >> (x % 8) } } buf.Write(row) } return buf.Bytes(), nil } // dirEntry writes the 12 bytes ICONDIRENTRY and GRPICONDIRENTRY share. func dirEntry(buf *bytes.Buffer, im iconImage) { dim := byte(im.size) if im.size >= 256 { dim = 0 } buf.Write([]byte{dim, dim, 0, 0}) binary.Write(buf, binary.LittleEndian, []uint16{1, 32}) binary.Write(buf, binary.LittleEndian, uint32(len(im.data))) } // buildICO lays out an .ico file: ICONDIR, one ICONDIRENTRY per image with // the file offset of its data, then the images. func buildICO(images []iconImage) []byte { var buf bytes.Buffer binary.Write(&buf, binary.LittleEndian, []uint16{0, 1, uint16(len(images))}) offset := 6 + 16*len(images) for _, im := range images { dirEntry(&buf, im) binary.Write(&buf, binary.LittleEndian, uint32(offset)) offset += len(im.data) } for _, im := range images { buf.Write(im.data) } return buf.Bytes() } // buildGroupIcon is the RT_GROUP_ICON payload: like ICONDIR, but each entry // ends with the RT_ICON resource id instead of a file offset. func buildGroupIcon(images []iconImage) []byte { var buf bytes.Buffer binary.Write(&buf, binary.LittleEndian, []uint16{0, 1, uint16(len(images))}) for i, im := range images { dirEntry(&buf, im) binary.Write(&buf, binary.LittleEndian, uint16(i+1)) } return buf.Bytes() } const ( rtIcon = 3 rtGroupIcon = 14 langNeutral = 0x0409 ) // resource is one leaf of the .rsrc tree: type/id/language and its data. type resource struct { typ, id uint32 data []byte } // buildSyso writes a COFF object with a single .rsrc section the Go linker // merges into the PE resource table: the directory tree (type → id → // language → data entry), the data, one ADDR32NB relocation per data entry // (the linker turns the section offsets into image RVAs), and the section // symbol the relocations refer to. func buildSyso(machine uint16, images []iconImage) []byte { var res []resource for i, im := range images { res = append(res, resource{rtIcon, uint32(i + 1), im.data}) } res = append(res, resource{rtGroupIcon, 1, buildGroupIcon(images)}) // Layout: root dir, the two type dirs, one language dir per resource, // one data entry per resource, then the data. const dirHdr, dirEnt, dataEnt = 16, 8, 16 types := []uint32{rtIcon, rtGroupIcon} byType := map[uint32][]resource{} for _, r := range res { byType[r.typ] = append(byType[r.typ], r) } rootOff := uint32(0) typeDirOff := map[uint32]uint32{} off := uint32(dirHdr + dirEnt*len(types)) for _, t := range types { typeDirOff[t] = off off += uint32(dirHdr + dirEnt*len(byType[t])) } langDirOff := make([]uint32, len(res)) for i := range res { langDirOff[i] = off off += dirHdr + dirEnt } dataEntOff := make([]uint32, len(res)) for i := range res { dataEntOff[i] = off off += dataEnt } dataOff := make([]uint32, len(res)) for i, r := range res { off = (off + 7) &^ 7 dataOff[i] = off off += uint32(len(r.data)) } sectionSize := off var sec bytes.Buffer le := binary.LittleEndian dir := func(entries int) { binary.Write(&sec, le, []uint32{0, 0}) binary.Write(&sec, le, []uint16{0, 0, 0, uint16(entries)}) } sub := func(id, target uint32) { binary.Write(&sec, le, []uint32{id, target | 0x80000000}) } leaf := func(id, target uint32) { binary.Write(&sec, le, []uint32{id, target}) } index := func(r resource) int { for i := range res { if res[i].typ == r.typ && res[i].id == r.id { return i } } panic("unreachable") } _ = rootOff dir(len(types)) for _, t := range types { sub(t, typeDirOff[t]) } for _, t := range types { dir(len(byType[t])) for _, r := range byType[t] { sub(r.id, langDirOff[index(r)]) } } for i := range res { dir(1) leaf(langNeutral, dataEntOff[i]) } var relocs []uint32 for i, r := range res { relocs = append(relocs, uint32(sec.Len())) binary.Write(&sec, le, []uint32{dataOff[i], uint32(len(r.data)), 0, 0}) } for i, r := range res { sec.Write(make([]byte, int(dataOff[i])-sec.Len())) sec.Write(r.data) } if uint32(sec.Len()) != sectionSize { fail("section layout mismatch: %d != %d", sec.Len(), sectionSize) } relocType := uint16(0x0003) // IMAGE_REL_AMD64_ADDR32NB if machine == 0xAA64 { relocType = 0x0002 // IMAGE_REL_ARM64_ADDR32NB } const fileHdr, secHdr, relocSize, symSize = 20, 40, 10, 18 rawOff := uint32(fileHdr + secHdr) relocOff := rawOff + sectionSize symOff := relocOff + uint32(relocSize*len(relocs)) var out bytes.Buffer binary.Write(&out, le, machine) binary.Write(&out, le, uint16(1)) // sections binary.Write(&out, le, uint32(0)) // timestamp binary.Write(&out, le, symOff) // symbol table binary.Write(&out, le, uint32(1)) // symbols binary.Write(&out, le, uint16(0)) // optional header binary.Write(&out, le, uint16(0x0104)) // 32-bit machine, line numbers stripped name := [8]byte{'.', 'r', 's', 'r', 'c'} out.Write(name[:]) binary.Write(&out, le, []uint32{0, 0, sectionSize, rawOff, relocOff, 0}) binary.Write(&out, le, []uint16{uint16(len(relocs)), 0}) binary.Write(&out, le, uint32(0x40000040)) // initialized data, readable out.Write(sec.Bytes()) for _, at := range relocs { binary.Write(&out, le, []uint32{at, 0}) binary.Write(&out, le, relocType) } out.Write(name[:]) binary.Write(&out, le, uint32(0)) // value binary.Write(&out, le, int16(1)) // section number binary.Write(&out, le, uint16(0)) // type out.Write([]byte{3, 0}) // static, no aux symbols binary.Write(&out, le, uint32(4)) // empty string table return out.Bytes() }