package main import ( "bytes" "crypto/sha256" "errors" "image" "image/color" _ "image/gif" _ "image/jpeg" "image/png" "math" "sync" "golang.org/x/image/draw" _ "golang.org/x/image/webp" ) const avatarSize = 256 var decoding sync.Mutex func cropAvatar(data []byte, x, y, size int) ([]byte, error) { cfg, _, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { return nil, errors.New("Avatar must be a PNG, JPEG, GIF or WebP image") } if cfg.Width > 6000 || cfg.Height > 6000 || cfg.Width*cfg.Height > 16_000_000 { return nil, errors.New("Avatar image is too large, keep it under 16 megapixels") } decoding.Lock() defer decoding.Unlock() img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { return nil, errors.New("Avatar must be a PNG, JPEG, GIF or WebP image") } b := img.Bounds() if size <= 0 || size > b.Dx() || size > b.Dy() { size = min(b.Dx(), b.Dy()) x = (b.Dx() - size) / 2 y = (b.Dy() - size) / 2 } x = max(0, min(x, b.Dx()-size)) y = max(0, min(y, b.Dy()-size)) src := image.Rect(b.Min.X+x, b.Min.Y+y, b.Min.X+x+size, b.Min.Y+y+size) dst := image.NewRGBA(image.Rect(0, 0, avatarSize, avatarSize)) draw.CatmullRom.Scale(dst, dst.Bounds(), img, src, draw.Src, nil) var out bytes.Buffer if err := png.Encode(&out, dst); err != nil { return nil, err } return out.Bytes(), nil } var blocks = [][][2]float64{ {{0, 0}, {1, 0}, {1, 1}, {0, 1}}, {{0, 0}, {1, 0}, {0, 1}}, {{0, 0}, {1, .5}, {0, 1}}, {{.5, 0}, {1, .5}, {.5, 1}, {0, .5}}, {{.25, .25}, {.75, .25}, {.75, .75}, {.25, .75}}, {{0, 0}, {1, 0}, {.5, .5}}, {{.5, 0}, {1, 1}, {0, 1}}, {{0, 0}, {1, 0}, {1, .5}, {0, .5}}, {{0, 0}, {.5, 0}, {1, 1}, {.5, 1}}, {{0, .5}, {.5, 0}, {1, .5}, {1, 1}, {0, 1}}, } var centres = []int{0, 3, 4} var identicons sync.Map func identicon(seed string) []byte { if b, ok := identicons.Load(seed); ok { return b.([]byte) } b := drawIdenticon(seed) identicons.Store(seed, b) return b } func drawIdenticon(seed string) []byte { sum := sha256.Sum256([]byte(seed)) h := float64(uint16(sum[28])<<8|uint16(sum[29])) / 65536 fg := hsl(h, 0.5+float64(sum[30])/255*0.2, 0.42+float64(sum[31])/255*0.12) const pad, cell = 14, 76 img := image.NewRGBA(image.Rect(0, 0, avatarSize, avatarSize)) draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{255, 255, 255, 255}}, image.Point{}, draw.Src) corner, side := int(sum[0])%len(blocks), int(sum[1])%len(blocks) cornerTurn, sideTurn := int(sum[2])%4, int(sum[3])%4 centre := centres[int(sum[4])%len(centres)] invert := sum[5]&1 == 1 place := func(shape, col, row, turn int, flip bool) { poly := blocks[shape] for y := 0; y < cell; y++ { for x := 0; x < cell; x++ { u, v := (float64(x)+.5)/cell, (float64(y)+.5)/cell for i := 0; i < turn; i++ { u, v = v, 1-u } if inside(poly, u, v) != flip { img.SetRGBA(pad+col*cell+x, pad+row*cell+y, fg) } } } } for i, p := range [][2]int{{0, 0}, {2, 0}, {2, 2}, {0, 2}} { place(corner, p[0], p[1], (cornerTurn+i)%4, false) } for i, p := range [][2]int{{1, 0}, {2, 1}, {1, 2}, {0, 1}} { place(side, p[0], p[1], (sideTurn+i)%4, invert) } place(centre, 1, 1, 0, false) var out bytes.Buffer png.Encode(&out, img) return out.Bytes() } func inside(poly [][2]float64, x, y float64) bool { in := false for i, j := 0, len(poly)-1; i < len(poly); j, i = i, i+1 { a, b := poly[i], poly[j] if (a[1] > y) != (b[1] > y) && x < (b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0] { in = !in } } return in } func hsl(h, s, l float64) color.RGBA { q := l + s - l*s if l < 0.5 { q = l * (1 + s) } p := 2*l - q ch := func(t float64) uint8 { t -= math.Floor(t) switch { case t < 1.0/6: return uint8(255 * (p + (q-p)*6*t)) case t < 0.5: return uint8(255 * q) case t < 2.0/3: return uint8(255 * (p + (q-p)*(2.0/3-t)*6)) } return uint8(255 * p) } return color.RGBA{ch(h + 1.0/3), ch(h), ch(h - 1.0/3), 255} }