spark-server/internal/module/user/entity/avatar.go
2026-09-18 14:50:41 +08:00

179 lines
4.3 KiB
Go

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(
`<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">`+
`<rect width="100" height="100" fill="%s"/>`+
`<text x="50" y="50" font-size="50" fill="#fff" text-anchor="middle" dominant-baseline="central">%s</text></svg>`,
fill, letter,
)
} else {
svg = fmt.Sprintf(
`<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">`+
`<rect width="72" height="72" rx="16" fill="%s"/><circle cx="36" cy="28" r="12" fill="#fff"/>`+
`<path d="M14 58c4-12 14-18 22-18s18 6 22 18" fill="#fff"/></svg>`,
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<<uint(glyphW-1-x)) == 0 {
continue
}
r := image.Rect(ox+x*glyphPx, oy+y*glyphPx, ox+(x+1)*glyphPx, oy+(y+1)*glyphPx)
draw.Draw(img, r, &image.Uniform{C: c}, image.Point{}, draw.Src)
}
}
}
func fillPerson(img *image.RGBA, c color.Color) {
fillCircle(img, avatarSize/2, 100, 43, c)
fillShoulders(img, c)
}
func fillCircle(img *image.RGBA, cx, cy, r int, c color.Color) {
rr := r * r
b := img.Bounds()
for y := cy - r; y <= cy+r; y++ {
for x := cx - r; x <= cx+r; x++ {
if !image.Pt(x, y).In(b) {
continue
}
dx, dy := x-cx, y-cy
if dx*dx+dy*dy <= rr {
img.Set(x, y, c)
}
}
}
}
func fillShoulders(img *image.RGBA, c color.Color) {
const cx, cy, rx, ry = 128, 206, 78, 64
b := img.Bounds()
rx2, ry2 := float64(rx*rx), float64(ry*ry)
for y := cy - ry; y <= avatarSize-1; y++ {
for x := cx - rx; x <= cx+rx; x++ {
if !image.Pt(x, y).In(b) {
continue
}
dx := float64(x - cx)
dy := float64(y - cy)
if dx*dx/rx2+dy*dy/ry2 <= 1 {
img.Set(x, y, c)
}
}
}
}
func hsvRGB(h, s, v float64) color.RGBA {
i := math.Floor(h * 6)
f := h*6 - i
p := v * (1 - s)
q := v * (1 - f*s)
t := v * (1 - (1-f)*s)
var r, g, b float64
switch int(i) % 6 {
case 0:
r, g, b = v, t, p
case 1:
r, g, b = q, v, p
case 2:
r, g, b = p, v, t
case 3:
r, g, b = p, q, v
case 4:
r, g, b = t, p, v
default:
r, g, b = v, p, q
}
return color.RGBA{
R: uint8(math.Floor(r * 255)),
G: uint8(math.Floor(g * 255)),
B: uint8(math.Floor(b * 255)),
A: 255,
}
}