Add GitHub alike identicons

This commit is contained in:
Paul-Christian Volkmer 2022-01-12 15:32:31 +01:00
parent dac6aa1d8b
commit c93c9a58af
5 changed files with 200 additions and 40 deletions

View File

@ -13,21 +13,19 @@ curl http://localhost:8000/avatar/23463b99b62a72f26ed677cc556c44e8?s=100&c=v2
Instead of requesting identicons for MD5 hashes of usernames or mail addresses, it is possible to use plain username and mail address. The will result in the same generated identicon.
Use request query parameter `s` to request images with specified size. Default value is 80px. The size is limited to a maximum value of 512px.
Query parameter `c` will set color scheme. Available values are `v1` and `v2`.
=== Server settings
Query parameter `c` will set color scheme. Available values are `v1`, `v2` and `gh`.
The latter resembles the color scheme used by GitHub.
You can use `COLORSCHEME` in environment to define the default color scheme to be used. Fallback value will be `v2`.
The request query parameter `d` can be used to request GitHub like patterns by setting the value to `github`.
== Development and local start
=== Configuration
A *Golang* setup is required for development. File `idicon.go` contains the source code. Type
Configuration is available by using environment variables.
....
$ go run
....
You can use `COLORSCHEME` to define the default color scheme to be used. Fallback value will be `v2`.
to run the application on port 8000.
The `PATTERN` environment variable is available to define GitHub like patterns as default by using `github`.
=== Docker build

90
colors.go Normal file
View File

@ -0,0 +1,90 @@
package main
import "image/color"
func colorV1(hash [16]byte) color.RGBA {
r := 32 + (hash[0]%16)/2<<4
g := 32 + (hash[2]%16)/2<<4
b := 32 + (hash[len(hash)-1]%16)/2<<4
if r > g && r > b {
r += 48
} else if g > r && g > b {
g += 48
} else if b > r && b > g {
b += 48
}
return color.RGBA{r, g, b, 255}
}
func colorV2(hash [16]byte) color.RGBA {
var palette = []color.RGBA{
{0x3c, 0x38, 0x36, 0xff},
{0xcc, 0x24, 0x1d, 0xff},
{0x98, 0x97, 0x1a, 0xff},
{0xd7, 0x99, 0x21, 0xff},
{0x45, 0x85, 0x88, 0xff},
{0xb1, 0x62, 0x86, 0xff},
{0x68, 0x9d, 0x6a, 0xff},
{0xa8, 0x99, 0x84, 0xff},
}
return palette[hash[15]%8]
}
func colorGh(hash [16]byte) color.RGBA {
h1 := (uint16(hash[12]) & 0x0f) << 8
h2 := uint16(hash[13])
h := uint32(h1 | h2)
s := uint32(hash[14])
l := uint32(hash[15])
return hslToRgba(
remap(h, 0, 4096, 0, 360),
65.0-remap(s, 0, 255, 0, 20),
75.0-remap(l, 0, 255, 0, 20),
)
}
// http://www.w3.org/TR/css3-color/#hsl-color
func hslToRgba(hue float32, sat float32, lum float32) color.RGBA {
hue = hue / 360.0
sat = sat / 100.0
lum = lum / 100.0
t2 := lum + sat - (lum * sat)
if lum <= 0.5 {
t2 = lum * (sat + 1.0)
}
t1 := lum*2.0 - t2
return color.RGBA{
uint8(hueToRgb(t1, t2, hue+1.0/3.0) * 255),
uint8(hueToRgb(t1, t2, hue) * 255),
uint8(hueToRgb(t1, t2, hue-1.0/3.0) * 255),
0xff,
}
}
func hueToRgb(t1 float32, t2 float32, hue float32) float32 {
if hue < 0.0 {
hue = hue + 1.0
} else if hue >= 1.0 {
hue = hue - 1.0
}
if hue < 1.0/6.0 {
return t1 + (t2-t1)*6.0*hue
}
if hue < 1.0/2.0 {
return t2
}
if hue < 2.0/3.0 {
return t1 + (t2-t1)*(2.0/3.0-hue)*6.0
}
return t1
}

View File

@ -3,7 +3,6 @@ package main
import (
"crypto/md5"
"encoding/hex"
"github.com/gorilla/mux"
"image"
"image/color"
"image/draw"
@ -14,35 +13,49 @@ import (
"regexp"
"strconv"
"strings"
"github.com/gorilla/mux"
)
func colorV1(hash [16]byte) color.RGBA {
r := 32 + (hash[0]%16)/2<<4
g := 32 + (hash[2]%16)/2<<4
b := 32 + (hash[len(hash)-1]%16)/2<<4
if r > g && r > b {
r += 48
} else if g > r && g > b {
g += 48
} else if b > r && b > g {
b += 48
}
return color.RGBA{r, g, b, 255}
// https://processing.org/reference/map_.html
func remap(value uint32, vmin uint32, vmax uint32, dmin uint32, dmax uint32) float32 {
return float32((value-vmin)*(dmax-dmin)) / float32((vmax-vmin)+dmin)
}
func colorV2(hash [16]byte) color.RGBA {
var palette = []color.RGBA{
{0x3c, 0x38, 0x36, 0xff},
{0xcc, 0x24, 0x1d, 0xff},
{0x98, 0x97, 0x1a, 0xff},
{0xd7, 0x99, 0x21, 0xff},
{0x45, 0x85, 0x88, 0xff},
{0xb1, 0x62, 0x86, 0xff},
{0x68, 0x9d, 0x6a, 0xff},
{0xa8, 0x99, 0x84, 0xff},
func nibbles(hash [16]byte) []byte {
nibbles := make([]byte, 32)
for i := 0; i <= 15; i++ {
nibbles[i*2+1] = hash[i] & 0x0f
nibbles[i*2] = hash[i] & 0xf0 >> 4
}
return palette[hash[15]%8]
return nibbles
}
// Based on https://github.com/dgraham/identicon
func genGhIcon(id string, size int, f func([16]byte) color.RGBA) *image.NRGBA {
if size > 512 {
size = 512
}
blocks := 5
hash := hashBytes(id)
nibbles := nibbles(hash)
data := make([]bool, blocks*blocks)
for x := 0; x < blocks; x++ {
for y := 0; y < blocks; y++ {
ni := x + blocks*(blocks-y-1)
if x+blocks*y > 2*blocks {
di := (x + blocks*y) - 2*blocks
data[di] = nibbles[ni]%2 == 0
}
}
}
return drawImage(mirrorData(data, blocks), blocks, size, f(hash))
}
func genIdIcon(id string, size int, f func([16]byte) color.RGBA) *image.NRGBA {
@ -123,16 +136,30 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) {
colorScheme = os.Getenv("COLORSCHEME")
}
w.Header().Add("Content-Type", "image/png")
if colorScheme == "v1" {
err = png.Encode(w, genIdIcon(id, size, colorV1))
} else {
err = png.Encode(w, genIdIcon(id, size, colorV2))
pattern := r.URL.Query().Get("d")
if pattern == "" {
pattern = os.Getenv("PATTERN")
}
w.Header().Add("Content-Type", "image/png")
cFunc := colorV2
if colorScheme == "v1" {
cFunc = colorV1
} else if colorScheme == "gh" {
cFunc = colorGh
}
if pattern == "github" {
err = png.Encode(w, genGhIcon(id, size, cFunc))
} else {
err = png.Encode(w, genIdIcon(id, size, cFunc))
}
}
func main() {
router := mux.NewRouter()
router.HandleFunc("/avatar/{id}", RequestHandler)
log.Println("Starting ...")
log.Fatal(http.ListenAndServe(":8000", router))
}

View File

@ -3,12 +3,13 @@ package main
import (
"bytes"
_ "embed"
"github.com/gorilla/mux"
"image/png"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/gorilla/mux"
)
//go:embed testdata/1a79a4d60de6718e8e5b326e338ae533_v1.png
@ -17,6 +18,9 @@ var v1 []byte
//go:embed testdata/1a79a4d60de6718e8e5b326e338ae533_v2.png
var v2 []byte
//go:embed testdata/1a79a4d60de6718e8e5b326e338ae533_gh.png
var gh []byte
func testRouter() *mux.Router {
router := mux.NewRouter()
router.HandleFunc("/avatar/{id}", RequestHandler)
@ -65,6 +69,20 @@ func TestCorrectResponseForV2ColorScheme(t *testing.T) {
}
}
func TestCorrectResponseForGHColorSchemeAndPattern(t *testing.T) {
req, err := http.NewRequest("GET", "/avatar/1a79a4d60de6718e8e5b326e338ae533?c=gh&d=github", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
testRouter().ServeHTTP(rr, req)
if !reflect.DeepEqual(rr.Body.Bytes(), gh) {
t.Errorf("returned image does not match expected image")
}
}
func TestIgnoreCase(t *testing.T) {
w1 := bytes.NewBuffer([]byte{})
png.Encode(w1, genIdIcon("example", 80, colorV1))
@ -89,3 +107,30 @@ func TestStringMatchesHash(t *testing.T) {
t.Errorf("resulting images do not match")
}
}
func TestHSLtoRGB(t *testing.T) {
red := hslToRgba(0, 100, 50)
if red.R != 255 || red.G != 0 || red.B != 0 {
t.Errorf("Color red not as required")
}
green := hslToRgba(120, 100, 50)
if green.R != 0 || green.G != 255 || green.B != 0 {
t.Errorf("Color green not as required")
}
blue := hslToRgba(240, 100, 50)
if blue.R != 0 || blue.G != 0 || blue.B != 255 {
t.Errorf("Color blue not as required")
}
}
func TestShouldCreateNibbles(t *testing.T) {
hash := [16]byte{}
hash[0] = 0x12
nibbles := nibbles(hash)
if nibbles[0] != 0x01 || nibbles[1] != 02 {
t.Errorf("Nibbles not extracted as expected")
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B