avatars/main.go
Nisan Coşkun d2aadec31d
All checks were successful
Publish Docker Image / build_push (push) Successful in 2m43s
feat: new face parts
2025-11-26 14:23:02 +03:00

222 lines
5.3 KiB
Go

package main
import (
"bytes"
"embed"
"hash/fnv"
"image"
"image/color"
"image/jpeg"
_ "image/png"
"io/fs"
"log"
"net/http"
"sync"
"strconv"
"strings"
"time"
"golang.org/x/image/draw"
)
type cacheItem struct {
value []byte
lastAccess int64
}
type AvatarCache struct {
cache map[string]*cacheItem
mu *sync.RWMutex
}
func (ac *AvatarCache) Clear() {
ac.mu.Lock()
for k, v := range ac.cache {
if time.Now().Unix()-v.lastAccess > int64(100) {
delete(ac.cache, k)
}
}
ac.mu.Unlock()
}
// insert to cache
func (ac *AvatarCache) Insert(id string, size int, value []byte) {
ac.mu.Lock()
defer ac.mu.Unlock()
ac.cache[id+strconv.Itoa(size)] = &cacheItem{value: value, lastAccess: time.Now().Unix()}
// remove oldest item if cache is full
if len(ac.cache) > 1000 {
for k := range ac.cache {
delete(ac.cache, k)
break
}
}
}
// get from cache
func (ac *AvatarCache) Get(id string, size int) []byte {
ac.mu.Lock()
defer ac.mu.Unlock()
item, ok := ac.cache[id+strconv.Itoa(size)]
if !ok {
return nil
}
item.lastAccess = time.Now().Unix()
return item.value
}
// create new cache
var avatarCache = AvatarCache{cache: make(map[string]*cacheItem), mu: &sync.RWMutex{}}
//go:embed assets/*
var files embed.FS
var BaseRects [11]draw.Image
var EYES = make([]image.Image, 0)
var NOSE = make([]image.Image, 0)
var MOUTH = make([]image.Image, 0)
var emptyRect = image.Rect(0, 0, 400, 400)
func getPartsById(id string) (draw.Image, image.Image, image.Image, image.Image) {
idBytes := []byte(id)
colorHash := fnv.New32a()
colorHash.Write(idBytes)
eyesHash := fnv.New32a()
eyesHash.Write(idBytes)
noseHash := fnv.New32a()
noseHash.Write(idBytes)
mouthHash := fnv.New32a()
mouthHash.Write(idBytes)
return BaseRects[colorHash.Sum32()%11],
EYES[eyesHash.Sum32()%10],
NOSE[noseHash.Sum32()%9],
MOUTH[mouthHash.Sum32()%10]
}
func serveAvatar(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
// trollin' attackers
w.Header().Set("x-powered-by", "Undertow/1")
w.Header().Set("server", "JBoss-EAP/7")
// get the id and size from the request
id := r.PathValue("id")
sizeStr := r.PathValue("size")
if sizeStr == "" {
sizeStr = r.URL.Query().Get("s")
}
size, err := strconv.Atoi(sizeStr)
if err != nil {
size = 400
}
// min size is 32, max size is 2048
if size < 32 {
size = 32
} else if size > 2048 {
size = 2048
}
if avatarCache.Get(id, size) != nil {
// get from cache
w.Header().Set("content-type", "image/jpeg")
w.WriteHeader(http.StatusFound)
byteSize, err := w.Write(avatarCache.Get(id, size))
if err != nil {
log.Println(err.Error())
}
log.Printf("[id:%s][size:%d] [%s] [%s] [%d bytes] [%v] [cache hit]\n",
id, size, r.RemoteAddr, r.Proto, byteSize, time.Since(startTime))
return
}
img := image.NewRGBA(emptyRect)
// draw parts over base(color) image
base, eyes, nose, mouth := getPartsById(id)
draw.Draw(img, emptyRect, base, emptyRect.Min, draw.Src)
draw.Draw(img, emptyRect, eyes, emptyRect.Min, draw.Over)
draw.Draw(img, emptyRect, nose, emptyRect.Min, draw.Over)
draw.Draw(img, emptyRect, mouth, emptyRect.Min, draw.Over)
if size != 400 {
// resize the image
dst := image.NewRGBA(image.Rect(0, 0, size, size))
draw.CatmullRom.Scale(dst, dst.Bounds(), img, emptyRect, draw.Over, nil)
img = dst
}
// write the image to the response
w.Header().Set("content-type", "image/jpeg")
w.WriteHeader(http.StatusCreated)
buf := new(bytes.Buffer)
err = jpeg.Encode(buf, img, nil)
if err != nil {
log.Println(err.Error())
}
byteSize, err := w.Write(buf.Bytes())
if err != nil {
log.Println(err.Error())
}
avatarCache.Insert(id, size, buf.Bytes())
log.Printf("[id:%s][size:%d] [%s] [%s] [%d bytes] [%v]\n",
id, size, r.RemoteAddr, r.Proto, byteSize, time.Since(startTime))
}
func main() {
go func() {
for range time.Tick(time.Minute) {
avatarCache.Clear()
}
}()
mux := http.NewServeMux()
mux.HandleFunc("GET /{id}", serveAvatar)
mux.HandleFunc("GET /{size}/{id}", serveAvatar)
http.ListenAndServe("0.0.0.0:3382", mux)
log.Println("Server started at 0.0.0.0:3382")
}
func init() {
_ = fs.WalkDir(files, "assets", func(path string, d fs.DirEntry, _ error) error {
if !d.IsDir() {
file, _ := files.ReadFile(path)
img, _, _ := image.Decode(bytes.NewReader(file))
if strings.Contains(path, "/eyes/eyes") {
EYES = append(EYES, img)
} else if strings.Contains(path, "/nose/nose") {
NOSE = append(NOSE, img)
} else if strings.Contains(path, "/mouth/mouth") {
MOUTH = append(MOUTH, img)
}
}
return nil
})
colors := [11]color.RGBA{
{129, 190, 241, 255},
{173, 139, 242, 255},
{191, 242, 136, 255},
{222, 120, 120, 255},
{165, 170, 197, 255},
{111, 242, 197, 255},
{240, 218, 94, 255},
{235, 89, 114, 255},
{246, 190, 93, 255},
{0, 185, 189, 255},
{251, 105, 0, 255},
}
//init Base Rectangles
for i := 0; i < 11; i++ {
BaseRects[i] = image.NewRGBA(image.Rect(0, 0, 400, 400))
for t := 0; t < 200; t++ {
// draw horizontal lines
for x := 0; x <= 399; x++ {
BaseRects[i].Set(x, 0+t, colors[i])
BaseRects[i].Set(x, 399-t, colors[i])
}
// draw vertical lines
for y := 0; y <= 399; y++ {
BaseRects[i].Set(0+t, y, colors[i])
BaseRects[i].Set(399-t, y, colors[i])
}
}
}
log.Printf("%d different combinations loaded\n", len(BaseRects)*len(EYES)*len(NOSE)*len(MOUTH))
}