Skip to content

Add more random Avatar generators (DiceBear, Robot) and make it configurable #17701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1639,8 +1639,20 @@ ROUTER = console
;AVATAR_UPLOAD_PATH = data/avatars
;REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
;;
;; How Gitea deals with missing user avatars
;; * random = random avatar will be displayed
;; * image = default image will be used
;USER_AVATAR_FALLBACK = random
;;
;; How Gitea deals with missing organization avatars
;; * random = random avatar will be displayed;
;; * image = default image will be used
;ORGANIZATION_AVATAR_FALLBACK = random
;;
;; How Gitea deals with missing repository avatars
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
;; * none = no avatar will be displayed
;; * random = random avatar will be displayed
;; * image = default image will be used
;REPOSITORY_AVATAR_FALLBACK = none
;REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png
;;
Expand Down
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.18
require (
code.gitea.io/gitea-vet v0.2.2-0.20220122151748-48ebc902541b
code.gitea.io/sdk/gitea v0.15.1
codeberg.org/Codeberg/avatars v1.0.0
gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb
gitea.com/go-chi/cache v0.2.0
gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5
Expand All @@ -27,6 +28,7 @@ require (
github.com/emirpasic/gods v1.18.1
github.com/ethantkoenig/rupture v1.0.1
github.com/felixge/fgprof v0.9.2
github.com/fogleman/gg v1.3.0
github.com/gliderlabs/ssh v0.3.4
github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b
github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d
Expand Down Expand Up @@ -60,6 +62,8 @@ require (
github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4
github.com/klauspost/compress v1.15.3
github.com/klauspost/cpuid/v2 v2.0.12
github.com/lafriks/go-avatars v0.3.0
github.com/lafriks/go-svg v0.3.2
github.com/lib/pq v1.10.5
github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
github.com/markbates/goth v1.72.0
Expand Down Expand Up @@ -183,6 +187,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
Expand Down Expand Up @@ -276,6 +281,7 @@ require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ code.gitea.io/gitea-vet v0.2.2-0.20220122151748-48ebc902541b/go.mod h1:zcNbT/aJE
code.gitea.io/sdk/gitea v0.11.3/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY=
code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M=
code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA=
codeberg.org/Codeberg/avatars v1.0.0 h1:MRx5QxuT/oVCcPvC5rXwgwWKD7hc6J0GnZ0Kl67lYEM=
codeberg.org/Codeberg/avatars v1.0.0/go.mod h1:ML/htpPRb3+owhkm4+qG2ZrXnk5WXaQLASOZ5GLCPi8=
contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA=
contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0=
contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw=
Expand Down Expand Up @@ -434,6 +436,8 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o=
github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
Expand Down Expand Up @@ -682,6 +686,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 h1:+eHOFJl1BaXrQxKX+T06f78590z4qA2ZzBTqahsKSE4=
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -1064,6 +1070,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/go-gypsy v1.0.0/go.mod h1:chkXM0zjdpXOiqkCW1XcCHDfjfk14PH2KKkQWxfJUcU=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lafriks/go-avatars v0.3.0 h1:+azteSFi6cr8TlvglfaBuwueeBwF8n84YYpKW0EWVEU=
github.com/lafriks/go-avatars v0.3.0/go.mod h1:EW0FFXaF0VZo4/vRoh1ASxqwKizRWfkbfrC21gEwxJY=
github.com/lafriks/go-svg v0.3.2 h1:wuSgV5Jh+aSGe+zPwXIOo2qb3UBRMKtAQmGfWwgIPqM=
github.com/lafriks/go-svg v0.3.2/go.mod h1:Gh57dXusEHiJBarKTI+t+LJVLU3Y7EvU/OUHWQUIBV0=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
Expand Down Expand Up @@ -1701,6 +1711,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down
4 changes: 2 additions & 2 deletions integrations/user_avatar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ import (

func TestUserAvatar(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) // owner of the repo3, is an org
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) // normal user

seed := user2.Email
if len(seed) == 0 {
seed = user2.Name
}

img, err := avatar.RandomImage([]byte(seed))
img, err := avatar.RandomImage(avatar.KindUser, []byte(seed))
if err != nil {
assert.NoError(t, err)
return
Expand Down
1 change: 1 addition & 0 deletions models/organization/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ func CreateOrganization(org *Organization, owner *user_model.User) (err error) {
if err = db.Insert(ctx, org); err != nil {
return fmt.Errorf("insert organization: %v", err)
}

if err = user_model.GenerateRandomAvatar(ctx, org.AsUser()); err != nil {
return fmt.Errorf("generate random avatar: %v", err)
}
Expand Down
15 changes: 10 additions & 5 deletions models/repo/avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,16 @@ func generateRandomAvatar(ctx context.Context, repo *Repository) error {
idToString := fmt.Sprintf("%d", repo.ID)

seed := idToString
img, err := avatar.RandomImage([]byte(seed))
img, err := avatar.RandomImage(avatar.KindRepo, []byte(seed))
if err != nil {
return fmt.Errorf("RandomImage: %v", err)
}

if img == nil {
// use default repo image
return nil
}

repo.Avatar = idToString

if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error {
Expand Down Expand Up @@ -66,13 +71,13 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
switch mode := setting.RepoAvatar.Fallback; mode {
case "image":
return setting.RepoAvatar.FallbackImage
case "random":
case "none":
// default behaviour: do not display avatar
return ""
default:
if err := generateRandomAvatar(ctx, repo); err != nil {
log.Error("generateRandomAvatar: %v", err)
}
default:
// default behaviour: do not display avatar
return ""
}
}
return setting.AppSubURL + "/repo-avatars/" + url.PathEscape(repo.Avatar)
Expand Down
13 changes: 11 additions & 2 deletions models/user/avatar.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,20 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
seed = u.Name
}

img, err := avatar.RandomImage([]byte(seed))
avatarKind := avatar.KindUser
if u.IsOrganization() {
avatarKind = avatar.KindOrg
}
img, err := avatar.RandomImage(avatarKind, []byte(seed))
if err != nil {
return fmt.Errorf("RandomImage: %v", err)
}

if img == nil {
// use default user image
return nil
}

u.Avatar = avatars.HashEmail(seed)

// Don't share the images so that we can delete them easily
Expand All @@ -53,7 +62,7 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
return err
}

log.Info("New random avatar created: %d", u.ID)
log.Info("New random avatar for user[%d] created", u.ID)
return nil
}

Expand Down
87 changes: 78 additions & 9 deletions modules/avatar/avatar.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
Expand All @@ -8,13 +9,15 @@ import (
"bytes"
"fmt"
"image"
"image/color"

_ "image/gif" // for processing gif images
_ "image/jpeg" // for processing jpeg images
_ "image/png" // for processing png images

"code.gitea.io/gitea/modules/avatar/dicebear"
"code.gitea.io/gitea/modules/avatar/identicon"
"code.gitea.io/gitea/modules/avatar/none"
"code.gitea.io/gitea/modules/avatar/robot"
"code.gitea.io/gitea/modules/setting"

"github.com/nfnt/resize"
Expand All @@ -24,21 +27,87 @@ import (
// AvatarSize returns avatar's size
const AvatarSize = 290

// Kind represent the type an avatar will be generated for
type Kind uint

const (
// User represent users
KindUser Kind = 0
// Repo represent repositorys
KindRepo Kind = 1
// Org represent organisations
KindOrg Kind = 2
)

type randomImageGenerator interface {
Name() string
}

type randomUserImageGenerator interface {
randomImageGenerator
RandomUserImage(int, []byte) (image.Image, error)
}

type randomOrgImageGenerator interface {
randomImageGenerator
RandomOrgImage(int, []byte) (image.Image, error)
}

type randomRepoImageGenerator interface {
randomImageGenerator
RandomRepoImage(int, []byte) (image.Image, error)
}

var (
userImageGenerator randomUserImageGenerator = identicon.Identicon{}
orgImageGenerator randomOrgImageGenerator = identicon.Identicon{}
repoImageGenerator randomRepoImageGenerator = identicon.Identicon{}
generators = []randomImageGenerator{
dicebear.DiceBear{},
identicon.Identicon{},
none.None{},
robot.Robot{},
}
)

// TODO: Init()
func init() {
userPreference := "none"
orgPreference := "none"
repoPreference := setting.RepoAvatar.FallbackImage

for _, g := range generators {
if g, ok := g.(randomUserImageGenerator); ok && userPreference == g.Name() {
userImageGenerator = g
}
if g, ok := g.(randomOrgImageGenerator); ok && orgPreference == g.Name() {
orgImageGenerator = g
}
if g, ok := g.(randomRepoImageGenerator); ok && repoPreference == g.Name() {
repoImageGenerator = g
}
}
}

// RandomImageSize generates and returns a random avatar image unique to input data
// in custom size (height and width).
func RandomImageSize(size int, data []byte) (image.Image, error) {
// we use white as background, and use dark colors to draw blocks
imgMaker, err := identicon.New(size, color.White, identicon.DarkColors...)
if err != nil {
return nil, fmt.Errorf("identicon.New: %v", err)
func RandomImageSize(kind Kind, size int, seed []byte) (image.Image, error) {
switch kind {
case KindUser:
return userImageGenerator.RandomUserImage(size, seed)
case KindOrg:
return orgImageGenerator.RandomOrgImage(size, seed)
case KindRepo:
return repoImageGenerator.RandomRepoImage(size, seed)
default:
return nil, fmt.Errorf("avatar kind %v not supported", kind)
}
return imgMaker.Make(data), nil
}

// RandomImage generates and returns a random avatar image unique to input data
// in default size (height and width).
func RandomImage(data []byte) (image.Image, error) {
return RandomImageSize(AvatarSize, data)
func RandomImage(kind Kind, seed []byte) (image.Image, error) {
return RandomImageSize(kind, AvatarSize, seed)
}

// Prepare accepts a byte slice as input, validates it contains an image of an
Expand Down
6 changes: 3 additions & 3 deletions modules/avatar/avatar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import (
)

func Test_RandomImageSize(t *testing.T) {
_, err := RandomImageSize(0, []byte("gitea@local"))
_, err := RandomImageSize(KindOrg, 0, []byte("gitea@local"))
assert.Error(t, err)

_, err = RandomImageSize(64, []byte("gitea@local"))
_, err = RandomImageSize(KindRepo, 64, []byte("gitea@local"))
assert.NoError(t, err)
}

func Test_RandomImage(t *testing.T) {
_, err := RandomImage([]byte("gitea@local"))
_, err := RandomImage(KindUser, []byte("gitea@local"))
assert.NoError(t, err)
}

Expand Down
60 changes: 60 additions & 0 deletions modules/avatar/dicebear/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package dicebear

import (
"fmt"
"image"
"image/draw"
"strings"

"codeberg.org/Codeberg/avatars"
"github.com/fogleman/gg"
"github.com/lafriks/go-svg"
"github.com/lafriks/go-svg/renderer"
rendr_gg "github.com/lafriks/go-svg/renderer/gg"
)

// DiceBear is used to generate pseudo-random avatars
type DiceBear struct{}

func (DiceBear) Name() string {
return "dicebear"
}

func (DiceBear) RandomUserImage(size int, data []byte) (image.Image, error) {
return randomImageSize(size, data)
}

func (DiceBear) RandomOrgImage(size int, data []byte) (image.Image, error) {
size /= 2
space := size / 20
img := image.NewRGBA(image.Rect(0, 0, size*2, size*2))

for i := 0; i < 4; i++ {
av, err := randomImageSize(size, []byte(fmt.Sprintf("%s-%d", string(data), i)))
if err != nil {
return nil, err
}
pos := image.Rect((i-(i/2)*2)*(size+space), (i/2)*(size+space), ((i-(i/2)*2)+1)*(size+space), ((i/2)+1)*(size+space))
draw.Draw(img, pos, av, image.Point{}, draw.Over)
}

return img, nil
}

func randomImageSize(size int, data []byte) (image.Image, error) {
svgAvatar := avatars.MakeAvatar(string(data))

s, err := svg.Parse(strings.NewReader(svgAvatar), svg.IgnoreErrorMode)
if err != nil {
return nil, err
}

gc := gg.NewContext(size, size)
rendr_gg.Draw(gc, s, renderer.Target(0, 0, float64(size), float64(size)))

return gc.Image(), nil
}
Loading