package entity
import (
"bytes"
"encoding/base64"
"fmt"
"hash/adler32"
"image"
"image/color"
"image/draw"
"image/png"
"math"
"strings"
"unicode"
"unicode/utf8"
)
const (
avatarSize = 256
avatarSat = 0.3
avatarVal = 0.9
glyphW = 5
glyphH = 7
glyphPx = 20
)
// LetterAvatarPNG draws a first-letter avatar. Hue follows FastAdmin letter_avatar (adler32 % 360).
func LetterAvatarPNG(text string) ([]byte, error) {
img := image.NewRGBA(image.Rect(0, 0, avatarSize, avatarSize))
bg := AvatarColor(text)
draw.Draw(img, img.Bounds(), &image.Uniform{C: bg}, image.Point{}, draw.Src)
if letter := AvatarLetter(text); letter != "" {
if rows, ok := avatarGlyphs[letter[0]]; ok {
blitGlyph(img, rows, color.White)
} else {
fillPerson(img, color.White)
}
} else {
fillPerson(img, color.White)
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
const avatarDataURIPrefix = "data:image/svg+xml;base64,"
// LetterAvatarDataURI is a compact SVG fallback (FastAdmin letter_avatar). Always fits users.avatar VARCHAR(512).
func LetterAvatarDataURI(text string) string {
c := AvatarColor(text)
fill := fmt.Sprintf("rgb(%d,%d,%d)", c.R, c.G, c.B)
var svg string
if letter := AvatarLetter(text); letter != "" {
svg = fmt.Sprintf(``, fill, letter)
} else {
svg = fmt.Sprintf(``, fill)
}
return avatarDataURIPrefix + base64.StdEncoding.EncodeToString([]byte(svg))
}
// AvatarLetter is the uppercase first rune. Non-Latin glyphs use the silhouette fallback.
func AvatarLetter(text string) string {
text = strings.TrimSpace(text)
if text == "" {
return "?"
}
r, _ := utf8.DecodeRuneInString(text)
if r == utf8.RuneError {
return "?"
}
r = unicode.ToUpper(r)
if r > unicode.MaxASCII || !unicode.IsLetter(r) && !unicode.IsDigit(r) {
return ""
}
return string(r)
}
// AvatarColor is the pastel background from adler32(text) % 360, sat 0.3 / val 0.9.
func AvatarColor(text string) color.RGBA {
hue := float64(adler32.Checksum([]byte(text))%360) / 360
return hsvRGB(hue, avatarSat, avatarVal)
}
func blitGlyph(img *image.RGBA, rows [glyphH]byte, c color.Color) {
gw, gh := glyphW*glyphPx, glyphH*glyphPx
ox := (avatarSize - gw) / 2
oy := (avatarSize - gh) / 2
for y := 0; y < glyphH; y++ {
row := rows[y]
for x := 0; x < glyphW; x++ {
if row&(1<