// Code generator for placeholder vendor icons + the app window icon. // // We embed PNG sources rather than drawing the icons in Gio at every // frame so that: // - the work happens once at build/dev time, not per redraw; // - users can swap any single PNG for a real vendor logo without // changing code, just dropping it under internal/ui/assets/ // vendor_icons/.png. // // Run from the repository root: // // go run ./tools/gen_icons // // Output: // - internal/ui/assets/vendor_icons/.png (7 files) // - internal/ui/assets/app_icon.png (256x256) // // The icons are intentionally generic (coloured rounded squares with a // single white initial) so they carry no third-party trademarks. Replace // them with real vendor logos at your discretion and at your own legal // risk. package main import ( "fmt" "image" "image/color" "image/draw" "image/png" "log" "os" "path/filepath" "golang.org/x/image/font" "golang.org/x/image/font/basicfont" "golang.org/x/image/math/fixed" ) type spec struct { name string // file name without extension; also lookup key letter string // single character to centre on the tile rgb [3]uint8 } var vendorSpecs = []spec{ {"google", "G", [3]uint8{0x42, 0x85, 0xF4}}, {"microsoft", "M", [3]uint8{0x00, 0xA4, 0xEF}}, {"okta", "O", [3]uint8{0x00, 0x7D, 0xC1}}, {"hotp", "#", [3]uint8{0x6A, 0x6A, 0x6A}}, {"battlenet", "B", [3]uint8{0x14, 0x8E, 0xFF}}, {"steam", "S", [3]uint8{0x1B, 0x2A, 0x38}}, // "qr" is used when the entry was added via QR scan and the source // vendor is unknown to us — a neutral grid-like tile keeps the row // from looking broken. {"qr", "Q", [3]uint8{0x3A, 0x3A, 0x3A}}, } const ( tileSize = 64 cornerRad = 12 appIconSz = 256 ) func main() { root := filepath.Join("internal", "ui", "assets") vendorDir := filepath.Join(root, "vendor_icons") if err := os.MkdirAll(vendorDir, 0o755); err != nil { log.Fatal(err) } for _, s := range vendorSpecs { path := filepath.Join(vendorDir, s.name+".png") if err := writePNG(path, drawTile(tileSize, cornerRad, s.rgb, s.letter)); err != nil { log.Fatal(err) } fmt.Println("wrote", path) } appPath := filepath.Join(root, "app_icon.png") if err := writePNG(appPath, drawAppIcon()); err != nil { log.Fatal(err) } fmt.Println("wrote", appPath) } // drawTile paints a rounded square of size×size pixels, filled with the // given RGB and stamped with a single white character in the middle. // We approximate rounded corners by simply skipping pixels outside the // rounded-rect mask; that is cheap and good enough for 64×64 art. func drawTile(size, radius int, rgb [3]uint8, letter string) *image.NRGBA { img := image.NewNRGBA(image.Rect(0, 0, size, size)) fill := color.NRGBA{R: rgb[0], G: rgb[1], B: rgb[2], A: 0xff} for y := 0; y < size; y++ { for x := 0; x < size; x++ { if insideRoundRect(x, y, size, size, radius) { img.Set(x, y, fill) } } } stampLetter(img, letter) return img } // drawAppIcon scales the "WinAuth" mark up to 256×256 so we have a // reusable file users can convert to .ico for window-class registration. func drawAppIcon() *image.NRGBA { rgb := [3]uint8{0x10, 0x70, 0xff} img := image.NewNRGBA(image.Rect(0, 0, appIconSz, appIconSz)) fill := color.NRGBA{R: rgb[0], G: rgb[1], B: rgb[2], A: 0xff} for y := 0; y < appIconSz; y++ { for x := 0; x < appIconSz; x++ { if insideRoundRect(x, y, appIconSz, appIconSz, 48) { img.Set(x, y, fill) } } } // Centred "W" approximation using the basicfont — small but legible. drawCenteredText(img, "W") return img } func insideRoundRect(x, y, w, h, r int) bool { if x < r && y < r { dx, dy := r-x, r-y return dx*dx+dy*dy <= r*r } if x >= w-r && y < r { dx, dy := x-(w-r-1), r-y return dx*dx+dy*dy <= r*r } if x < r && y >= h-r { dx, dy := r-x, y-(h-r-1) return dx*dx+dy*dy <= r*r } if x >= w-r && y >= h-r { dx, dy := x-(w-r-1), y-(h-r-1) return dx*dx+dy*dy <= r*r } return true } // stampLetter writes a single character centred on the tile using // basicfont.Face7x13. basicfont is part of golang.org/x/image and ships // with the project already, so this adds no new dependency. func stampLetter(img draw.Image, letter string) { face := basicfont.Face7x13 advance := font.MeasureString(face, letter).Ceil() bounds := img.Bounds() x := bounds.Min.X + (bounds.Dx()-advance)/2 y := bounds.Min.Y + bounds.Dy()/2 + face.Ascent/2 d := &font.Drawer{ Dst: img, Src: image.NewUniform(color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}), Face: face, Dot: fixed.P(x, y), } d.DrawString(letter) } // drawCenteredText is the same as stampLetter but tuned for the big // app-icon canvas: it stretches the glyph by drawing it several times // with small offsets so the result reads at 256×256. func drawCenteredText(img draw.Image, s string) { face := basicfont.Face7x13 advance := font.MeasureString(face, s).Ceil() bounds := img.Bounds() x := bounds.Min.X + (bounds.Dx()-advance*6)/2 y := bounds.Min.Y + bounds.Dy()/2 + face.Ascent*3 d := &font.Drawer{ Dst: img, Src: image.NewUniform(color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}), Face: face, } // Bloat the small glyph by stamping it in a 6×6 grid of pixel // offsets — crude but yields a readable "W" on the app icon. for dy := 0; dy < 6; dy++ { for dx := 0; dx < 6; dx++ { d.Dot = fixed.P(x+dx, y+dy) d.DrawString(s) } } } func writePNG(path string, img image.Image) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() return png.Encode(f, img) }