mirror of https://github.com/go-gitea/gitea.git
Better builtin avatar generator (#17707)
This PR fixes the builtin avatar generator. 1. The random background color makes some images very dirty. So now we only use white background for avatars. 2. We use left-right mirror avatars to satisfy #14799 3. Fix a small padding error in the algorithmpull/17732/head
parent
38347aa16f
commit
a8fd76557b
@ -0,0 +1,135 @@
|
||||
// Copyright 2021 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 identicon
|
||||
|
||||
import "image/color"
|
||||
|
||||
// DarkColors are dark colors for avatar blocks, they come from image/color/palette.WebSafe, and light colors (0xff) are removed
|
||||
var DarkColors = []color.Color{
|
||||
color.RGBA{0x00, 0x00, 0x33, 0xff},
|
||||
color.RGBA{0x00, 0x00, 0x66, 0xff},
|
||||
color.RGBA{0x00, 0x00, 0x99, 0xff},
|
||||
color.RGBA{0x00, 0x00, 0xcc, 0xff},
|
||||
color.RGBA{0x00, 0x33, 0x00, 0xff},
|
||||
color.RGBA{0x00, 0x33, 0x33, 0xff},
|
||||
color.RGBA{0x00, 0x33, 0x66, 0xff},
|
||||
color.RGBA{0x00, 0x33, 0x99, 0xff},
|
||||
color.RGBA{0x00, 0x33, 0xcc, 0xff},
|
||||
color.RGBA{0x00, 0x66, 0x00, 0xff},
|
||||
color.RGBA{0x00, 0x66, 0x33, 0xff},
|
||||
color.RGBA{0x00, 0x66, 0x66, 0xff},
|
||||
color.RGBA{0x00, 0x66, 0x99, 0xff},
|
||||
color.RGBA{0x00, 0x66, 0xcc, 0xff},
|
||||
color.RGBA{0x00, 0x99, 0x00, 0xff},
|
||||
color.RGBA{0x00, 0x99, 0x33, 0xff},
|
||||
color.RGBA{0x00, 0x99, 0x66, 0xff},
|
||||
color.RGBA{0x00, 0x99, 0x99, 0xff},
|
||||
color.RGBA{0x00, 0x99, 0xcc, 0xff},
|
||||
color.RGBA{0x00, 0xcc, 0x00, 0xff},
|
||||
color.RGBA{0x00, 0xcc, 0x33, 0xff},
|
||||
color.RGBA{0x00, 0xcc, 0x66, 0xff},
|
||||
color.RGBA{0x00, 0xcc, 0x99, 0xff},
|
||||
color.RGBA{0x00, 0xcc, 0xcc, 0xff},
|
||||
color.RGBA{0x33, 0x00, 0x00, 0xff},
|
||||
color.RGBA{0x33, 0x00, 0x33, 0xff},
|
||||
color.RGBA{0x33, 0x00, 0x66, 0xff},
|
||||
color.RGBA{0x33, 0x00, 0x99, 0xff},
|
||||
color.RGBA{0x33, 0x00, 0xcc, 0xff},
|
||||
color.RGBA{0x33, 0x33, 0x00, 0xff},
|
||||
color.RGBA{0x33, 0x33, 0x33, 0xff},
|
||||
color.RGBA{0x33, 0x33, 0x66, 0xff},
|
||||
color.RGBA{0x33, 0x33, 0x99, 0xff},
|
||||
color.RGBA{0x33, 0x33, 0xcc, 0xff},
|
||||
color.RGBA{0x33, 0x66, 0x00, 0xff},
|
||||
color.RGBA{0x33, 0x66, 0x33, 0xff},
|
||||
color.RGBA{0x33, 0x66, 0x66, 0xff},
|
||||
color.RGBA{0x33, 0x66, 0x99, 0xff},
|
||||
color.RGBA{0x33, 0x66, 0xcc, 0xff},
|
||||
color.RGBA{0x33, 0x99, 0x00, 0xff},
|
||||
color.RGBA{0x33, 0x99, 0x33, 0xff},
|
||||
color.RGBA{0x33, 0x99, 0x66, 0xff},
|
||||
color.RGBA{0x33, 0x99, 0x99, 0xff},
|
||||
color.RGBA{0x33, 0x99, 0xcc, 0xff},
|
||||
color.RGBA{0x33, 0xcc, 0x00, 0xff},
|
||||
color.RGBA{0x33, 0xcc, 0x33, 0xff},
|
||||
color.RGBA{0x33, 0xcc, 0x66, 0xff},
|
||||
color.RGBA{0x33, 0xcc, 0x99, 0xff},
|
||||
color.RGBA{0x33, 0xcc, 0xcc, 0xff},
|
||||
color.RGBA{0x66, 0x00, 0x00, 0xff},
|
||||
color.RGBA{0x66, 0x00, 0x33, 0xff},
|
||||
color.RGBA{0x66, 0x00, 0x66, 0xff},
|
||||
color.RGBA{0x66, 0x00, 0x99, 0xff},
|
||||
color.RGBA{0x66, 0x00, 0xcc, 0xff},
|
||||
color.RGBA{0x66, 0x33, 0x00, 0xff},
|
||||
color.RGBA{0x66, 0x33, 0x33, 0xff},
|
||||
color.RGBA{0x66, 0x33, 0x66, 0xff},
|
||||
color.RGBA{0x66, 0x33, 0x99, 0xff},
|
||||
color.RGBA{0x66, 0x33, 0xcc, 0xff},
|
||||
color.RGBA{0x66, 0x66, 0x00, 0xff},
|
||||
color.RGBA{0x66, 0x66, 0x33, 0xff},
|
||||
color.RGBA{0x66, 0x66, 0x66, 0xff},
|
||||
color.RGBA{0x66, 0x66, 0x99, 0xff},
|
||||
color.RGBA{0x66, 0x66, 0xcc, 0xff},
|
||||
color.RGBA{0x66, 0x99, 0x00, 0xff},
|
||||
color.RGBA{0x66, 0x99, 0x33, 0xff},
|
||||
color.RGBA{0x66, 0x99, 0x66, 0xff},
|
||||
color.RGBA{0x66, 0x99, 0x99, 0xff},
|
||||
color.RGBA{0x66, 0x99, 0xcc, 0xff},
|
||||
color.RGBA{0x66, 0xcc, 0x00, 0xff},
|
||||
color.RGBA{0x66, 0xcc, 0x33, 0xff},
|
||||
color.RGBA{0x66, 0xcc, 0x66, 0xff},
|
||||
color.RGBA{0x66, 0xcc, 0x99, 0xff},
|
||||
color.RGBA{0x66, 0xcc, 0xcc, 0xff},
|
||||
color.RGBA{0x99, 0x00, 0x00, 0xff},
|
||||
color.RGBA{0x99, 0x00, 0x33, 0xff},
|
||||
color.RGBA{0x99, 0x00, 0x66, 0xff},
|
||||
color.RGBA{0x99, 0x00, 0x99, 0xff},
|
||||
color.RGBA{0x99, 0x00, 0xcc, 0xff},
|
||||
color.RGBA{0x99, 0x33, 0x00, 0xff},
|
||||
color.RGBA{0x99, 0x33, 0x33, 0xff},
|
||||
color.RGBA{0x99, 0x33, 0x66, 0xff},
|
||||
color.RGBA{0x99, 0x33, 0x99, 0xff},
|
||||
color.RGBA{0x99, 0x33, 0xcc, 0xff},
|
||||
color.RGBA{0x99, 0x66, 0x00, 0xff},
|
||||
color.RGBA{0x99, 0x66, 0x33, 0xff},
|
||||
color.RGBA{0x99, 0x66, 0x66, 0xff},
|
||||
color.RGBA{0x99, 0x66, 0x99, 0xff},
|
||||
color.RGBA{0x99, 0x66, 0xcc, 0xff},
|
||||
color.RGBA{0x99, 0x99, 0x00, 0xff},
|
||||
color.RGBA{0x99, 0x99, 0x33, 0xff},
|
||||
color.RGBA{0x99, 0x99, 0x66, 0xff},
|
||||
color.RGBA{0x99, 0x99, 0x99, 0xff},
|
||||
color.RGBA{0x99, 0x99, 0xcc, 0xff},
|
||||
color.RGBA{0x99, 0xcc, 0x00, 0xff},
|
||||
color.RGBA{0x99, 0xcc, 0x33, 0xff},
|
||||
color.RGBA{0x99, 0xcc, 0x66, 0xff},
|
||||
color.RGBA{0x99, 0xcc, 0x99, 0xff},
|
||||
color.RGBA{0x99, 0xcc, 0xcc, 0xff},
|
||||
color.RGBA{0xcc, 0x00, 0x00, 0xff},
|
||||
color.RGBA{0xcc, 0x00, 0x33, 0xff},
|
||||
color.RGBA{0xcc, 0x00, 0x66, 0xff},
|
||||
color.RGBA{0xcc, 0x00, 0x99, 0xff},
|
||||
color.RGBA{0xcc, 0x00, 0xcc, 0xff},
|
||||
color.RGBA{0xcc, 0x33, 0x00, 0xff},
|
||||
color.RGBA{0xcc, 0x33, 0x33, 0xff},
|
||||
color.RGBA{0xcc, 0x33, 0x66, 0xff},
|
||||
color.RGBA{0xcc, 0x33, 0x99, 0xff},
|
||||
color.RGBA{0xcc, 0x33, 0xcc, 0xff},
|
||||
color.RGBA{0xcc, 0x66, 0x00, 0xff},
|
||||
color.RGBA{0xcc, 0x66, 0x33, 0xff},
|
||||
color.RGBA{0xcc, 0x66, 0x66, 0xff},
|
||||
color.RGBA{0xcc, 0x66, 0x99, 0xff},
|
||||
color.RGBA{0xcc, 0x66, 0xcc, 0xff},
|
||||
color.RGBA{0xcc, 0x99, 0x00, 0xff},
|
||||
color.RGBA{0xcc, 0x99, 0x33, 0xff},
|
||||
color.RGBA{0xcc, 0x99, 0x66, 0xff},
|
||||
color.RGBA{0xcc, 0x99, 0x99, 0xff},
|
||||
color.RGBA{0xcc, 0x99, 0xcc, 0xff},
|
||||
color.RGBA{0xcc, 0xcc, 0x00, 0xff},
|
||||
color.RGBA{0xcc, 0xcc, 0x33, 0xff},
|
||||
color.RGBA{0xcc, 0xcc, 0x66, 0xff},
|
||||
color.RGBA{0xcc, 0xcc, 0x99, 0xff},
|
||||
color.RGBA{0xcc, 0xcc, 0xcc, 0xff},
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
// Copied and modified from https://github.com/issue9/identicon/ (MIT License)
|
||||
// Generate pseudo-random avatars by IP, E-mail, etc.
|
||||
|
||||
package identicon
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
const minImageSize = 16
|
||||
|
||||
// Identicon is used to generate pseudo-random avatars
|
||||
type Identicon struct {
|
||||
foreColors []color.Color
|
||||
backColor color.Color
|
||||
size int
|
||||
rect image.Rectangle
|
||||
}
|
||||
|
||||
// New returns an Identicon struct with the correct settings
|
||||
// size image size
|
||||
// back background color
|
||||
// fore all possible foreground colors. only one foreground color will be picked randomly for one image
|
||||
func New(size int, back color.Color, fore ...color.Color) (*Identicon, error) {
|
||||
if len(fore) == 0 {
|
||||
return nil, fmt.Errorf("foreground is not set")
|
||||
}
|
||||
|
||||
if size < minImageSize {
|
||||
return nil, fmt.Errorf("size %d is smaller than min size %d", size, minImageSize)
|
||||
}
|
||||
|
||||
return &Identicon{
|
||||
foreColors: fore,
|
||||
backColor: back,
|
||||
size: size,
|
||||
rect: image.Rect(0, 0, size, size),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Make generates an avatar by data
|
||||
func (i *Identicon) Make(data []byte) image.Image {
|
||||
h := sha256.New()
|
||||
h.Write(data)
|
||||
sum := h.Sum(nil)
|
||||
|
||||
b1 := int(sum[0]+sum[1]+sum[2]) % len(blocks)
|
||||
b2 := int(sum[3]+sum[4]+sum[5]) % len(blocks)
|
||||
c := int(sum[6]+sum[7]+sum[8]) % len(centerBlocks)
|
||||
b1Angle := int(sum[9]+sum[10]) % 4
|
||||
b2Angle := int(sum[11]+sum[12]) % 4
|
||||
foreColor := int(sum[11]+sum[12]+sum[15]) % len(i.foreColors)
|
||||
|
||||
return i.render(c, b1, b2, b1Angle, b2Angle, foreColor)
|
||||
}
|
||||
|
||||
func (i *Identicon) render(c, b1, b2, b1Angle, b2Angle, foreColor int) image.Image {
|
||||
p := image.NewPaletted(i.rect, []color.Color{i.backColor, i.foreColors[foreColor]})
|
||||
drawBlocks(p, i.size, centerBlocks[c], blocks[b1], blocks[b2], b1Angle, b2Angle)
|
||||
return p
|
||||
}
|
||||
|
||||
/*
|
||||
# Algorithm
|
||||
|
||||
Origin: An image is splitted into 9 areas
|
||||
|
||||
```
|
||||
-------------
|
||||
| 1 | 2 | 3 |
|
||||
-------------
|
||||
| 4 | 5 | 6 |
|
||||
-------------
|
||||
| 7 | 8 | 9 |
|
||||
-------------
|
||||
```
|
||||
|
||||
Area 1/3/9/7 use a 90-degree rotating pattern.
|
||||
Area 1/3/9/7 use another 90-degree rotating pattern.
|
||||
Area 5 uses a random patter.
|
||||
|
||||
The Patched Fix: make the image left-right mirrored to get rid of something like "swastika"
|
||||
*/
|
||||
|
||||
// draw blocks to the paletted
|
||||
// c: the block drawer for the center block
|
||||
// b1,b2: the block drawers for other blocks (around the center block)
|
||||
// b1Angle,b2Angle: the angle for the rotation of b1/b2
|
||||
func drawBlocks(p *image.Paletted, size int, c, b1, b2 blockFunc, b1Angle, b2Angle int) {
|
||||
nextAngle := func(a int) int {
|
||||
return (a + 1) % 4
|
||||
}
|
||||
|
||||
padding := (size % 3) / 2 // in cased the size can not be aligned by 3 blocks.
|
||||
|
||||
blockSize := size / 3
|
||||
twoBlockSize := 2 * blockSize
|
||||
|
||||
// center
|
||||
c(p, blockSize+padding, blockSize+padding, blockSize, 0)
|
||||
|
||||
// left top (1)
|
||||
b1(p, 0+padding, 0+padding, blockSize, b1Angle)
|
||||
// center top (2)
|
||||
b2(p, blockSize+padding, 0+padding, blockSize, b2Angle)
|
||||
|
||||
b1Angle = nextAngle(b1Angle)
|
||||
b2Angle = nextAngle(b2Angle)
|
||||
// right top (3)
|
||||
// b1(p, twoBlockSize+padding, 0+padding, blockSize, b1Angle)
|
||||
// right middle (6)
|
||||
// b2(p, twoBlockSize+padding, blockSize+padding, blockSize, b2Angle)
|
||||
|
||||
b1Angle = nextAngle(b1Angle)
|
||||
b2Angle = nextAngle(b2Angle)
|
||||
// right bottom (9)
|
||||
// b1(p, twoBlockSize+padding, twoBlockSize+padding, blockSize, b1Angle)
|
||||
// center bottom (8)
|
||||
b2(p, blockSize+padding, twoBlockSize+padding, blockSize, b2Angle)
|
||||
|
||||
b1Angle = nextAngle(b1Angle)
|
||||
b2Angle = nextAngle(b2Angle)
|
||||
// lef bottom (7)
|
||||
b1(p, 0+padding, twoBlockSize+padding, blockSize, b1Angle)
|
||||
// left middle (4)
|
||||
b2(p, 0+padding, blockSize+padding, blockSize, b2Angle)
|
||||
|
||||
// then we make it left-right mirror, so we didn't draw 3/6/9 before
|
||||
for x := 0; x < size/2; x++ {
|
||||
for y := 0; y < size; y++ {
|
||||
p.SetColorIndex(size-x, y, p.ColorIndexAt(x, y))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
// Copyright 2021 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.
|
||||
|
||||
//go:build test_avatar_identicon
|
||||
// +build test_avatar_identicon
|
||||
|
||||
package identicon
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"image/png"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
dir, _ := os.Getwd()
|
||||
dir = dir + "/testdata"
|
||||
if st, err := os.Stat(dir); err != nil || !st.IsDir() {
|
||||
t.Errorf("can not save generated images to %s", dir)
|
||||
}
|
||||
|
||||
backColor := color.White
|
||||
imgMaker, err := New(64, backColor, DarkColors...)
|
||||
assert.NoError(t, err)
|
||||
for i := 0; i < 100; i++ {
|
||||
s := strconv.Itoa(i)
|
||||
img := imgMaker.Make([]byte(s))
|
||||
|
||||
f, err := os.Create(dir + "/" + s + ".png")
|
||||
if !assert.NoError(t, err) {
|
||||
continue
|
||||
}
|
||||
defer f.Close()
|
||||
err = png.Encode(f, img)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
*
|
@ -1,23 +0,0 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
#vim
|
||||
*.swp
|
||||
|
||||
#osx
|
||||
.DS_Store
|
||||
|
||||
/testdata/*.png
|
||||
|
||||
.idea
|
||||
.vscode
|
@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 caixw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
@ -1,5 +0,0 @@
|
||||
module github.com/issue9/identicon
|
||||
|
||||
require github.com/issue9/assert v1.4.1
|
||||
|
||||
go 1.13
|
@ -1,2 +0,0 @@
|
||||
github.com/issue9/assert v1.4.1 h1:gUtOpMTeaE4JTe9kACma5foOHBvVt1p5XTFrULDwdXI=
|
||||
github.com/issue9/assert v1.4.1/go.mod h1:Yktk83hAVl1SPSYtd9kjhBizuiBIqUQyj+D5SE2yjVY=
|
Loading…
Reference in New Issue