fedc198452
- 新增 tools/gen_icons 生成 7 张 vendor 占位 PNG + 256x256 应用图标 - internal/ui/vendor_icons.go 用 //go:embed 加载并按 Authenticator.Name() 查表 - entry 行圆环中心叠 16dp vendor 图标,HOTP 直接用图标占位 - 图标为通用色块 + 首字母,无第三方商标;用户可按文件名替换为真实 logo - assets/README 说明替换方式与 Windows .syso 嵌入步骤
190 lines
5.5 KiB
Go
190 lines
5.5 KiB
Go
// 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/<name>.png.
|
||
//
|
||
// Run from the repository root:
|
||
//
|
||
// go run ./tools/gen_icons
|
||
//
|
||
// Output:
|
||
// - internal/ui/assets/vendor_icons/<vendor>.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)
|
||
}
|