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 algorithm
This commit is contained in:
wxiaoguang 2021-11-20 01:10:41 +08:00 committed by GitHub
parent 38347aa16f
commit a8fd76557b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 417 additions and 376 deletions

View File

@ -50,7 +50,7 @@ import (
"bytes"
"fmt"
"image"
"image/color/palette"
"image/color"
_ "image/gif" // for processing gif images
_ "image/jpeg" // for processing jpeg images
@ -76,7 +76,7 @@ import (
"bytes"
"fmt"
"image"
"image/color/palette"
"image/color"
_ "image/gif" // for processing gif images
_ "image/jpeg" // for processing jpeg images

1
go.mod
View File

@ -63,7 +63,6 @@ require (
github.com/hashicorp/go-version v1.3.1
github.com/hashicorp/golang-lru v0.5.4
github.com/huandu/xstrings v1.3.2
github.com/issue9/identicon v1.2.0
github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
github.com/json-iterator/go v1.1.11
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51

4
go.sum
View File

@ -661,10 +661,6 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/issue9/assert v1.4.1 h1:gUtOpMTeaE4JTe9kACma5foOHBvVt1p5XTFrULDwdXI=
github.com/issue9/assert v1.4.1/go.mod h1:Yktk83hAVl1SPSYtd9kjhBizuiBIqUQyj+D5SE2yjVY=
github.com/issue9/identicon v1.2.0 h1:ek+UcTTyMW/G0iNbLOAlrPC13eSzXTWhbJSs8PHhHGQ=
github.com/issue9/identicon v1.2.0/go.mod h1:A9toNT0ky/1WP5iNFyDmrkNiYH6eX3HcN5V6uH0g0ec=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=

View File

@ -8,16 +8,15 @@ import (
"bytes"
"fmt"
"image"
"image/color/palette"
"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/identicon"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/issue9/identicon"
"github.com/nfnt/resize"
"github.com/oliamb/cutter"
)
@ -28,20 +27,8 @@ const AvatarSize = 290
// 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) {
randExtent := len(palette.WebSafe) - 32
integer, err := util.RandomInt(int64(randExtent))
if err != nil {
return nil, fmt.Errorf("util.RandomInt: %v", err)
}
colorIndex := int(integer)
backColorIndex := colorIndex - 1
if backColorIndex < 0 {
backColorIndex = randExtent - 1
}
// Define size, background, and forecolor
imgMaker, err := identicon.New(size,
palette.WebSafe[backColorIndex], palette.WebSafe[colorIndex:colorIndex+32]...)
// 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)
}

View File

@ -1,25 +1,26 @@
// SPDX-License-Identifier: MIT
// 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)
package identicon
import "image"
var (
// 可以出现在中间的方块,一般为了美观,都是对称图像。
// the blocks can appear in center, these blocks can be more beautiful
centerBlocks = []blockFunc{b0, b1, b2, b3, b19, b26, b27}
// 所有方块
// all blocks
blocks = []blockFunc{b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19, b20, b21, b22, b23, b24, b25, b26, b27}
)
// 所有 block 函数的类型
type blockFunc func(img *image.Paletted, x, y, size int, angle int)
// 将多边形 points 旋转 angle 个角度,然后输出到 img 上,起点为 x,y 坐标
//
// points 中的坐标是基于左上角是原点的坐标系。
// draw a polygon by points, and the polygon is rotated by angle.
func drawBlock(img *image.Paletted, x, y, size int, angle int, points []int) {
if angle > 0 { // 0 角度不需要转换
if angle != 0 {
m := size / 2
rotate(points, m, m, angle)
}
@ -33,7 +34,7 @@ func drawBlock(img *image.Paletted, x, y, size int, angle int, points []int) {
}
}
// 全空白
// blank
//
// --------
// | |
@ -42,7 +43,7 @@ func drawBlock(img *image.Paletted, x, y, size int, angle int, points []int) {
// --------
func b0(img *image.Paletted, x, y, size int, angle int) {}
// 全填充正方形
// full-filled
//
// --------
// |######|
@ -57,7 +58,7 @@ func b1(img *image.Paletted, x, y, size int, angle int) {
}
}
// 中间小方块
// a small block
// ----------
// | |
// | #### |
@ -66,8 +67,8 @@ func b1(img *image.Paletted, x, y, size int, angle int) {
// ----------
func b2(img *image.Paletted, x, y, size int, angle int) {
l := size / 4
x = x + l
y = y + l
x += l
y += l
for i := x; i < x+2*l; i++ {
for j := y; j < y+2*l; j++ {
@ -76,7 +77,7 @@ func b2(img *image.Paletted, x, y, size int, angle int) {
}
}
// 菱形
// diamond
//
// ---------
// | # |
@ -133,7 +134,7 @@ func b5(img *image.Paletted, x, y, size int, angle int) {
})
}
// b6 矩形
// b6
//
// --------
// |### |
@ -151,7 +152,7 @@ func b6(img *image.Paletted, x, y, size int, angle int) {
})
}
// b7 斜放的锥形
// b7 italic cone
//
// ---------
// | # |
@ -170,7 +171,7 @@ func b7(img *image.Paletted, x, y, size int, angle int) {
})
}
// b8 三个堆叠的三角形
// b8 three small triangles
//
// -----------
// | # |
@ -184,7 +185,7 @@ func b8(img *image.Paletted, x, y, size int, angle int) {
m := size / 2
mm := m / 2
// 顶部三角形
// top
drawBlock(img, x, y, size, angle, []int{
m, 0,
3 * mm, m,
@ -192,7 +193,7 @@ func b8(img *image.Paletted, x, y, size int, angle int) {
m, 0,
})
// 底下左边
// bottom left
drawBlock(img, x, y, size, angle, []int{
mm, m,
m, size,
@ -200,7 +201,7 @@ func b8(img *image.Paletted, x, y, size int, angle int) {
mm, m,
})
// 底下右边
// bottom right
drawBlock(img, x, y, size, angle, []int{
3 * mm, m,
size, size,
@ -209,7 +210,7 @@ func b8(img *image.Paletted, x, y, size int, angle int) {
})
}
// b9 斜靠的三角形
// b9 italic triangle
//
// ---------
// |# |
@ -257,7 +258,7 @@ func b10(img *image.Paletted, x, y, size int, angle int) {
})
}
// b11 左上角1/4大小的方块
// b11
//
// ----------
// |#### |

View File

@ -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},
}

View File

@ -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))
}
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,69 @@
// 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)
package identicon
var (
// cos(0),cos(90),cos(180),cos(270)
cos = []int{1, 0, -1, 0}
// sin(0),sin(90),sin(180),sin(270)
sin = []int{0, 1, 0, -1}
)
// rotate the points by center point (x,y)
// angle: [0,1,2,3] means [090180270] degree
func rotate(points []int, x, y int, angle int) {
// the angle is only used internally, and it has been guaranteed to be 0/1/2/3, so we do not check it again
for i := 0; i < len(points); i += 2 {
px, py := points[i]-x, points[i+1]-y
points[i] = px*cos[angle] - py*sin[angle] + x
points[i+1] = px*sin[angle] + py*cos[angle] + y
}
}
// check whether the point is inside the polygon (defined by the points)
// the first and the last point must be the same
func pointInPolygon(x, y int, polygonPoints []int) bool {
if len(polygonPoints) < 8 { // a valid polygon must have more than 2 points
return false
}
// reference: nonzero winding rule, https://en.wikipedia.org/wiki/Nonzero-rule
// split the plane into two by the check point horizontally:
// y>0includes (x>0 && y==0)
// y<0includes (x<0 && y==0)
//
// then scan every point in the polygon.
//
// if current point and previous point are in different planes (eg: curY>0 && prevY<0),
// check the clock-direction from previous point to current point (use check point as origin).
// if the direction is clockwise, then r++, otherwise then r--
// finally, if 2==abs(r), then the check point is inside the polygon
r := 0
prevX, prevY := polygonPoints[0], polygonPoints[1]
prev := (prevY > y) || ((prevX > x) && (prevY == y))
for i := 2; i < len(polygonPoints); i += 2 {
currX, currY := polygonPoints[i], polygonPoints[i+1]
curr := (currY > y) || ((currX > x) && (currY == y))
if curr == prev {
prevX, prevY = currX, currY
continue
}
if mul := (prevX-x)*(currY-y) - (currX-x)*(prevY-y); mul >= 0 {
r++
} else { // mul < 0
r--
}
prevX, prevY = currX, currY
prev = curr
}
return r == 2 || r == -2
}

View File

@ -0,0 +1 @@
*

View File

@ -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

View File

@ -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.

View File

@ -1,38 +0,0 @@
# identicon
[![Go](https://github.com/issue9/identicon/actions/workflows/go.yml/badge.svg)](https://github.com/issue9/identicon/actions/workflows/go.yml)
[![codecov](https://codecov.io/gh/issue9/identicon/branch/master/graph/badge.svg)](https://codecov.io/gh/issue9/identicon)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/issue9/identicon)](https://pkg.go.dev/github.com/issue9/identicon)
![Go version](https://img.shields.io/github/go-mod/go-version/issue9/identicon)
![License](https://img.shields.io/github/license/issue9/identicon)
根据用户的 IP 、邮箱名等任意数据为用户产生漂亮的随机头像。
![screenshot.1](https://raw.github.com/issue9/identicon/master/screenshot/1.png)
![screenshot.4](https://raw.github.com/issue9/identicon/master/screenshot/4.png)
![screenshot.5](https://raw.github.com/issue9/identicon/master/screenshot/5.png)
![screenshot.6](https://raw.github.com/issue9/identicon/master/screenshot/6.png)
![screenshot.7](https://raw.github.com/issue9/identicon/master/screenshot/7.png)
```go
// 根据用户访问的IP为其生成一张头像
img, _ := identicon.Make(128, color.NRGBA{},color.NRGBA{}, []byte("192.168.1.1"))
fi, _ := os.Create("/tmp/u1.png")
png.Encode(fi, img)
fi.Close()
// 或者
ii, _ := identicon.New(128, color.NRGBA{}, color.NRGBA{}, color.NRGBA{}, color.NRGBA{})
img := ii.Make([]byte("192.168.1.1"))
img = ii.Make([]byte("192.168.1.2"))
```
## 安装
```shell
go get github.com/issue9/identicon
```
## 版权
本项目采用 [MIT](https://opensource.org/licenses/MIT) 开源授权许可证,完整的授权说明可在 [LICENSE](LICENSE) 文件中找到。

View File

@ -1,35 +0,0 @@
// SPDX-License-Identifier: MIT
// Package identicon 一个基于 hash 值生成随机图像的包
//
// identicon 并没有统一的标准,一般用于在用户注册时,
// 取用户的邮箱或是访问 IP 等数据(也可以是其它任何数据)
// 进行 hash 运算,之后根据 hash 数据,产生一张图像,
// 这样即可以为用户产生一张独特的头像,又不会泄漏用户的隐藏。
//
// 在 identicon 中,把图像分成以下九个部分:
// -------------
// | 1 | 2 | 3 |
// -------------
// | 4 | 5 | 6 |
// -------------
// | 7 | 8 | 9 |
// -------------
// 其中 1、3、9、7 为不同角度(依次增加 90 度)的同一张图片,
// 2、6、8、4 也是如此,这样可以保持图像是对称的,比较美观。
// 5 则单独使用一张图片。
//
// // 根据用户访问的 IP ,为其生成一张头像
// img, _ := identicon.Make(128, color.NRGBA{},color.NRGBA{}, []byte("192.168.1.1"))
// fi, _ := os.Create("/tmp/u1.png")
// png.Encode(fi, img)
// fi.Close()
//
// // 或者
// ii, _ := identicon.New(128, color.NRGBA{}, color.NRGBA{}, color.NRGBA{})
// img := ii.Make([]byte("192.168.1.1"))
// img = ii.Make([]byte("192.168.1.2"))
//
// NOTE: go test 会在当前目录的 testdata 文件夹下产生大量的随机图片。
// 要运行测试,必须保证该文件夹是存在的,且有相应的写入权限。
package identicon

View File

@ -1,5 +0,0 @@
module github.com/issue9/identicon
require github.com/issue9/assert v1.4.1
go 1.13

View File

@ -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=

View File

@ -1,137 +0,0 @@
// SPDX-License-Identifier: MIT
package identicon
import (
"crypto/md5"
"fmt"
"image"
"image/color"
"math/rand"
)
const (
minSize = 16 // 图片的最小尺寸
maxForeColors = 32 // 在New()函数中可以指定的最大颜色数量
)
// Identicon 用于产生统一尺寸的头像
//
// 可以根据用户提供的数据,经过一定的算法,自动产生相应的图案和颜色。
type Identicon struct {
foreColors []color.Color
backColor color.Color
size int
rect image.Rectangle
}
// New 声明一个 Identicon 实例
//
// size 表示整个头像的大小;
// back 表示前景色;
// fore 表示所有可能的前景色,会为每个图像随机挑选一个作为其前景色。
func New(size int, back color.Color, fore ...color.Color) (*Identicon, error) {
if len(fore) == 0 || len(fore) > maxForeColors {
return nil, fmt.Errorf("前景色数量必须介于[1]~[%d]之间,当前为[%d]", maxForeColors, len(fore))
}
if size < minSize {
return nil, fmt.Errorf("参数 size 的值(%d)不能小于 %d", size, minSize)
}
return &Identicon{
foreColors: fore,
backColor: back,
size: size,
// 画布坐标从0开始其长度应该是 size-1
rect: image.Rect(0, 0, size, size),
}, nil
}
// Make 根据 data 数据产生一张唯一性的头像图片
func (i *Identicon) Make(data []byte) image.Image {
h := md5.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
color := int(sum[11]+sum[12]+sum[15]) % len(i.foreColors)
return i.render(c, b1, b2, b1Angle, b2Angle, color)
}
// Rand 随机生成图案
func (i *Identicon) Rand(r *rand.Rand) image.Image {
b1 := r.Intn(len(blocks))
b2 := r.Intn(len(blocks))
c := r.Intn(len(centerBlocks))
b1Angle := r.Intn(4)
b2Angle := r.Intn(4)
color := r.Intn(len(i.foreColors))
return i.render(c, b1, b2, b1Angle, b2Angle, color)
}
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
}
// Make 根据 data 数据产生一张唯一性的头像图片
//
// size 头像的大小。
// back, fore头像的背景和前景色。
func Make(size int, back, fore color.Color, data []byte) (image.Image, error) {
i, err := New(size, back, fore)
if err != nil {
return nil, err
}
return i.Make(data), nil
}
// 将九个方格都填上内容。
// p 为画板;
// c 为中间方格的填充函数;
// b1、b2 为边上 8 格的填充函数;
// b1Angle 和 b2Angle 为 b1、b2 的起始旋转角度。
func drawBlocks(p *image.Paletted, size int, c, b1, b2 blockFunc, b1Angle, b2Angle int) {
incr := func(a int) int {
if a >= 3 {
a = 0
} else {
a++
}
return a
}
padding := (size % 6) / 2 // 不能除尽的,边上留白。
blockSize := size / 3
twoBlockSize := 2 * blockSize
c(p, blockSize+padding, blockSize+padding, blockSize, 0)
b1(p, 0+padding, 0+padding, blockSize, b1Angle)
b2(p, blockSize+padding, 0+padding, blockSize, b2Angle)
b1Angle = incr(b1Angle)
b2Angle = incr(b2Angle)
b1(p, twoBlockSize+padding, 0+padding, blockSize, b1Angle)
b2(p, twoBlockSize+padding, blockSize+padding, blockSize, b2Angle)
b1Angle = incr(b1Angle)
b2Angle = incr(b2Angle)
b1(p, twoBlockSize+padding, twoBlockSize+padding, blockSize, b1Angle)
b2(p, blockSize+padding, twoBlockSize+padding, blockSize, b2Angle)
b1Angle = incr(b1Angle)
b2Angle = incr(b2Angle)
b1(p, 0+padding, twoBlockSize+padding, blockSize, b1Angle)
b2(p, 0+padding, blockSize+padding, blockSize, b2Angle)
}

View File

@ -1,65 +0,0 @@
// SPDX-License-Identifier: MIT
package identicon
var (
// 4 个元素分别表示 cos(0),cos(90),cos(180),cos(270)
cos = []int{1, 0, -1, 0}
// 4 个元素分别表示 sin(0),sin(90),sin(180),sin(270)
sin = []int{0, 1, 0, -1}
)
// 将 points 中的所有点,以 x,y 为原点旋转 angle 个角度。
// angle 取值只能是 [0,1,2,3],分别表示 [090180270]
func rotate(points []int, x, y int, angle int) {
if angle < 0 || angle > 3 {
panic("rotate:参数angle必须0,1,2,3三值之一")
}
for i := 0; i < len(points); i += 2 {
px, py := points[i]-x, points[i+1]-y
points[i] = px*cos[angle] - py*sin[angle] + x
points[i+1] = px*sin[angle] + py*cos[angle] + y
}
}
// 判断某个点是否在多边形之内,不包含构成多边形的线和点
// x,y 需要判断的点坐标
// points 组成多边形的所顶点,每两个元素表示一点顶点,其中最后一个顶点必须与第一个顶点相同。
func pointInPolygon(x, y int, points []int) bool {
if len(points) < 8 { // 只有2个以上的点才能组成闭合多边形
return false
}
// 大致算法如下:
// 把整个平面以给定的测试点为原点分两部分:
// - y>0包含(x>0 && y==0)
// - y<0包含(x<0 && y==0)
// 依次扫描每一个点,当该点与前一个点处于不同部分时(即一个在 y>0 区,一个在 y<0 区),
// 则判断从前一点到当前点是顺时针还是逆时针(以给定的测试点为原点),如果是顺时针 r++,否则 r--。
// 结果为2==abs(r)。
r := 0
x1, y1 := points[0], points[1]
prev := (y1 > y) || ((x1 > x) && (y1 == y))
for i := 2; i < len(points); i += 2 {
x2, y2 := points[i], points[i+1]
curr := (y2 > y) || ((x2 > x) && (y2 == y))
if curr == prev {
x1, y1 = x2, y2
continue
}
if mul := (x1-x)*(y2-y) - (x2-x)*(y1-y); mul >= 0 {
r++
} else if mul < 0 {
r--
}
x1, y1 = x2, y2
prev = curr
}
return r == 2 || r == -2
}

3
vendor/modules.txt vendored
View File

@ -507,9 +507,6 @@ github.com/hashicorp/hcl/json/token
github.com/huandu/xstrings
# github.com/imdario/mergo v0.3.12
github.com/imdario/mergo
# github.com/issue9/identicon v1.2.0
## explicit
github.com/issue9/identicon
# github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7
## explicit
github.com/jaytaylor/html2text