2020-08-02 19:16:58 +02:00
|
|
|
// Vikunja is a to-do list application to facilitate your life.
|
|
|
|
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
2020-12-23 16:41:52 +01:00
|
|
|
// it under the terms of the GNU Affero General Public Licensee as published by
|
2020-08-02 19:16:58 +02:00
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
2020-12-23 16:41:52 +01:00
|
|
|
// GNU Affero General Public Licensee for more details.
|
2020-08-02 19:16:58 +02:00
|
|
|
//
|
2020-12-23 16:41:52 +01:00
|
|
|
// You should have received a copy of the GNU Affero General Public Licensee
|
2020-08-02 19:16:58 +02:00
|
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
package initials
|
|
|
|
|
|
|
|
import (
|
2020-10-10 18:53:59 +02:00
|
|
|
"bytes"
|
2020-10-11 22:10:03 +02:00
|
|
|
"image"
|
|
|
|
"image/color"
|
|
|
|
"image/draw"
|
|
|
|
"image/png"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2020-08-02 19:16:58 +02:00
|
|
|
"code.vikunja.io/api/pkg/log"
|
2020-10-10 18:53:59 +02:00
|
|
|
"code.vikunja.io/api/pkg/modules/keyvalue"
|
2020-08-02 19:16:58 +02:00
|
|
|
"code.vikunja.io/api/pkg/user"
|
|
|
|
"github.com/disintegration/imaging"
|
|
|
|
"github.com/golang/freetype/truetype"
|
|
|
|
"golang.org/x/image/font"
|
|
|
|
"golang.org/x/image/font/gofont/goregular"
|
|
|
|
"golang.org/x/image/math/fixed"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Provider represents the provider implementation of the initials provider
|
|
|
|
type Provider struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
avatarBgColors = []*color.RGBA{
|
2020-10-11 22:10:03 +02:00
|
|
|
{R: 69, G: 189, B: 243, A: 255},
|
|
|
|
{R: 224, G: 143, B: 112, A: 255},
|
|
|
|
{R: 77, G: 182, B: 172, A: 255},
|
|
|
|
{R: 149, G: 117, B: 205, A: 255},
|
|
|
|
{R: 176, G: 133, B: 94, A: 255},
|
|
|
|
{R: 240, G: 98, B: 146, A: 255},
|
|
|
|
{R: 163, G: 211, B: 108, A: 255},
|
|
|
|
{R: 121, G: 134, B: 203, A: 255},
|
|
|
|
{R: 241, G: 185, B: 29, A: 255},
|
2020-08-02 19:16:58 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
dpi = 72
|
|
|
|
defaultSize = 1024
|
|
|
|
)
|
|
|
|
|
|
|
|
func drawImage(text rune, bg *color.RGBA) (img *image.RGBA64, err error) {
|
|
|
|
|
|
|
|
size := defaultSize
|
|
|
|
fontSize := float64(size) * 0.8
|
|
|
|
|
|
|
|
// Inspired by https://github.com/holys/initials-avatar
|
|
|
|
|
|
|
|
// Get the font
|
|
|
|
f, err := truetype.Parse(goregular.TTF)
|
|
|
|
if err != nil {
|
|
|
|
return img, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build the image background
|
|
|
|
img = image.NewRGBA64(image.Rect(0, 0, size, size))
|
|
|
|
draw.Draw(img, img.Bounds(), &image.Uniform{C: bg}, image.Point{}, draw.Src)
|
|
|
|
|
|
|
|
// Add the text
|
|
|
|
drawer := &font.Drawer{
|
|
|
|
Dst: img,
|
|
|
|
Src: image.White,
|
|
|
|
Face: truetype.NewFace(f, &truetype.Options{
|
|
|
|
Size: fontSize,
|
|
|
|
DPI: dpi,
|
|
|
|
Hinting: font.HintingNone,
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Font Index
|
|
|
|
fi := f.Index(text)
|
|
|
|
|
|
|
|
// Glyph example: http://www.freetype.org/freetype2/docs/tutorial/metrics.png
|
|
|
|
var gbuf truetype.GlyphBuf
|
|
|
|
fsize := fixed.Int26_6(fontSize * dpi * (64.0 / 72.0))
|
|
|
|
err = gbuf.Load(f, fsize, fi, font.HintingFull)
|
|
|
|
if err != nil {
|
|
|
|
drawer.DrawString("")
|
|
|
|
return img, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Center
|
|
|
|
dY := (size - int(gbuf.Bounds.Max.Y-gbuf.Bounds.Min.Y)>>6) / 2
|
|
|
|
dX := (size - int(gbuf.Bounds.Max.X-gbuf.Bounds.Min.X)>>6) / 2
|
|
|
|
y := int(gbuf.Bounds.Max.Y>>6) + dY
|
|
|
|
x := 0 - int(gbuf.Bounds.Min.X>>6) + dX
|
|
|
|
|
|
|
|
drawer.Dot = fixed.Point26_6{
|
|
|
|
X: fixed.I(x),
|
|
|
|
Y: fixed.I(y),
|
|
|
|
}
|
|
|
|
drawer.DrawString(string(text))
|
|
|
|
|
|
|
|
return img, err
|
|
|
|
}
|
|
|
|
|
2020-10-10 18:53:59 +02:00
|
|
|
func getCacheKey(prefix string, keys ...int64) string {
|
|
|
|
result := "avatar_initials_" + prefix
|
|
|
|
for i, key := range keys {
|
|
|
|
result += strconv.Itoa(int(key))
|
|
|
|
if i < len(keys) {
|
|
|
|
result += "_"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2020-08-02 19:16:58 +02:00
|
|
|
func getAvatarForUser(u *user.User) (fullSizeAvatar *image.RGBA64, err error) {
|
2020-10-10 18:53:59 +02:00
|
|
|
cacheKey := getCacheKey("full", u.ID)
|
|
|
|
|
2021-01-31 12:32:46 +01:00
|
|
|
a, exists, err := keyvalue.Get(cacheKey)
|
|
|
|
if err != nil {
|
2020-10-10 18:53:59 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-01-31 12:32:46 +01:00
|
|
|
if !exists {
|
2020-08-02 19:16:58 +02:00
|
|
|
log.Debugf("Initials avatar for user %d not cached, creating...", u.ID)
|
|
|
|
firstRune := []rune(strings.ToUpper(u.Username))[0]
|
|
|
|
bg := avatarBgColors[int(u.ID)%len(avatarBgColors)] // Random color based on the user id
|
|
|
|
|
|
|
|
fullSizeAvatar, err = drawImage(firstRune, bg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-10-10 18:53:59 +02:00
|
|
|
err = keyvalue.Put(cacheKey, fullSizeAvatar)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fullSizeAvatar = a.(*image.RGBA64)
|
2020-08-02 19:16:58 +02:00
|
|
|
}
|
|
|
|
|
2020-10-10 18:53:59 +02:00
|
|
|
return fullSizeAvatar, nil
|
2020-08-02 19:16:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetAvatar returns an initials avatar for a user
|
|
|
|
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
|
2020-10-10 18:53:59 +02:00
|
|
|
cacheKey := getCacheKey("resized", u.ID, size)
|
|
|
|
|
2021-01-31 12:32:46 +01:00
|
|
|
a, exists, err := keyvalue.Get(cacheKey)
|
|
|
|
if err != nil {
|
2020-10-10 18:53:59 +02:00
|
|
|
return nil, "", err
|
|
|
|
}
|
2020-08-02 19:16:58 +02:00
|
|
|
|
2021-01-31 12:32:46 +01:00
|
|
|
if !exists {
|
2020-08-02 19:16:58 +02:00
|
|
|
log.Debugf("Initials avatar for user %d and size %d not cached, creating...", u.ID, size)
|
|
|
|
fullAvatar, err := getAvatarForUser(u)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
img := imaging.Resize(fullAvatar, int(size), int(size), imaging.Lanczos)
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
err = png.Encode(buf, img)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
avatar = buf.Bytes()
|
2020-10-10 18:53:59 +02:00
|
|
|
err = keyvalue.Put(cacheKey, avatar)
|
|
|
|
if err != nil {
|
|
|
|
return nil, "", err
|
|
|
|
}
|
2020-08-02 19:16:58 +02:00
|
|
|
} else {
|
2020-10-10 18:53:59 +02:00
|
|
|
avatar = a.([]byte)
|
2020-08-02 19:16:58 +02:00
|
|
|
log.Debugf("Serving initials avatar for user %d and size %d from cache", u.ID, size)
|
|
|
|
}
|
|
|
|
|
2020-10-10 18:53:59 +02:00
|
|
|
return avatar, "image/png", nil
|
2020-08-02 19:16:58 +02:00
|
|
|
}
|