From b84f291d2bbd64feb6c417d1c6a87fb525b36524 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Mon, 26 Dec 2022 12:14:41 +0100 Subject: [PATCH] Move icon generation into dedicated files --- colors.go | 90 ---------------------------- icons/ghicons.go | 107 +++++++++++++++++++++++++++++++++ icons/ghicons_test.go | 30 ++++++++++ icons/icons.go | 64 ++++++++++++++++++++ icons/idicons.go | 54 +++++++++++++++++ icons/idicons_test.go | 36 ++++++++++++ idicon.go | 133 ++++-------------------------------------- idicon_test.go | 54 ----------------- 8 files changed, 303 insertions(+), 265 deletions(-) delete mode 100644 colors.go create mode 100644 icons/ghicons.go create mode 100644 icons/ghicons_test.go create mode 100644 icons/icons.go create mode 100644 icons/idicons.go create mode 100644 icons/idicons_test.go diff --git a/colors.go b/colors.go deleted file mode 100644 index 04a7d2d..0000000 --- a/colors.go +++ /dev/null @@ -1,90 +0,0 @@ -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 -} diff --git a/icons/ghicons.go b/icons/ghicons.go new file mode 100644 index 0000000..bd49c23 --- /dev/null +++ b/icons/ghicons.go @@ -0,0 +1,107 @@ +package icons + +import ( + "image" + "image/color" +) + +type GhIconGenerator struct { +} + +// Based on https://github.com/dgraham/identicon +func (generator *GhIconGenerator) GenIcon(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%32]%2 == 0 + } + } + } + + return drawImage(mirrorData(data, blocks), blocks, size, f(hash)) +} + +// 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 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 nibbles +} + +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 +} diff --git a/icons/ghicons_test.go b/icons/ghicons_test.go new file mode 100644 index 0000000..4746676 --- /dev/null +++ b/icons/ghicons_test.go @@ -0,0 +1,30 @@ +package icons + +import "testing" + +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") + } +} diff --git a/icons/icons.go b/icons/icons.go new file mode 100644 index 0000000..ea37922 --- /dev/null +++ b/icons/icons.go @@ -0,0 +1,64 @@ +package icons + +import ( + "crypto/md5" + "encoding/hex" + "image" + "image/color" + "image/draw" + "regexp" +) + +type IconGenerator interface { + GenIcon(id string, size int, f func([16]byte) color.RGBA) *image.NRGBA +} + +func HashBytes(id string) [16]byte { + hash := [16]byte{} + md5RegExp := regexp.MustCompile("[a-f0-9]{32}") + if !md5RegExp.Match([]byte(id)) { + hash = md5.Sum([]byte(id)) + } else { + dec, _ := hex.DecodeString(id) + for idx, b := range dec { + hash[idx] = b + } + } + return hash +} + +func mirrorData(data []bool, blocks int) []bool { + for x := 0; x < blocks; x++ { + min := x*blocks + 1 + for y := 0; y < blocks; y++ { + a := ((blocks - x - 1) * blocks) + y + b := min + y - 1 + if data[a] { + data[b] = true + } + } + } + return data +} + +func drawImage(data []bool, blocks int, size int, c color.Color) *image.NRGBA { + img := image.NewNRGBA(image.Rect(0, 0, size, size)) + + draw.Draw(img, img.Bounds(), &image.Uniform{color.Gray{240}}, image.Point{0, 0}, draw.Src) + + blockSize := size / (blocks + 1) + border := (size - (blocks * blockSize)) / 2 + + for x := border; x < blockSize*blocks+border; x++ { + bx := (x - border) / blockSize + for y := border; y < blockSize*blocks+border; y++ { + by := (y - border) / blockSize + idx := bx*blocks + by + if data[idx] && (bx < blocks || by < blocks) { + img.Set(x, y, c) + } + } + } + + return img +} diff --git a/icons/idicons.go b/icons/idicons.go new file mode 100644 index 0000000..c287bb9 --- /dev/null +++ b/icons/idicons.go @@ -0,0 +1,54 @@ +package icons + +import ( + "image" + "image/color" + "strings" +) + +type IdIconGenerator struct { +} + +func (generator *IdIconGenerator) GenIcon(id string, size int, f func([16]byte) color.RGBA) *image.NRGBA { + id = strings.ToLower(id) + blocks := 5 + if size > 512 { + size = 512 + } + + hash := HashBytes(id) + data := make([]bool, blocks*blocks) + for i := 0; i < len(hash)-1; i++ { + data[i] = hash[i]%2 != hash[i+1]%2 + } + return drawImage(mirrorData(data, blocks), blocks, size, f(hash)) +} + +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] +} diff --git a/icons/idicons_test.go b/icons/idicons_test.go new file mode 100644 index 0000000..255d7c4 --- /dev/null +++ b/icons/idicons_test.go @@ -0,0 +1,36 @@ +package icons + +import ( + "bytes" + "image/png" + "testing" +) + +func TestIgnoreCase(t *testing.T) { + iconGenerator := IdIconGenerator{} + + w1 := bytes.NewBuffer([]byte{}) + png.Encode(w1, iconGenerator.GenIcon("example", 80, ColorV1)) + + w2 := bytes.NewBuffer([]byte{}) + png.Encode(w2, iconGenerator.GenIcon("Example", 80, ColorV1)) + + if bytes.Compare(w1.Bytes(), w2.Bytes()) != 0 { + t.Errorf("resulting images do not match") + } +} + +func TestStringMatchesHash(t *testing.T) { + iconGenerator := IdIconGenerator{} + + w1 := bytes.NewBuffer([]byte{}) + // MD5 of lowercase 'example' + png.Encode(w1, iconGenerator.GenIcon("1a79a4d60de6718e8e5b326e338ae533", 80, ColorV2)) + + w2 := bytes.NewBuffer([]byte{}) + png.Encode(w2, iconGenerator.GenIcon("Example", 80, ColorV2)) + + if bytes.Compare(w1.Bytes(), w2.Bytes()) != 0 { + t.Errorf("resulting images do not match") + } +} diff --git a/idicon.go b/idicon.go index 8c0717d..a79b9b1 100644 --- a/idicon.go +++ b/idicon.go @@ -1,130 +1,18 @@ package main import ( - "crypto/md5" - "encoding/hex" "flag" "fmt" - "image" - "image/color" - "image/draw" + "github.com/BurntSushi/toml" + "github.com/gorilla/mux" + "idicon/icons" "image/png" "log" "net/http" "os" - "regexp" "strconv" - "strings" - - "github.com/BurntSushi/toml" - "github.com/gorilla/mux" ) -// 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 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 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%32]%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 { - id = strings.ToLower(id) - blocks := 5 - if size > 512 { - size = 512 - } - - hash := hashBytes(id) - data := make([]bool, blocks*blocks) - for i := 0; i < len(hash)-1; i++ { - data[i] = hash[i]%2 != hash[i+1]%2 - } - return drawImage(mirrorData(data, blocks), blocks, size, f(hash)) -} - -func hashBytes(id string) [16]byte { - hash := [16]byte{} - md5RegExp := regexp.MustCompile("[a-f0-9]{32}") - if !md5RegExp.Match([]byte(id)) { - hash = md5.Sum([]byte(id)) - } else { - dec, _ := hex.DecodeString(id) - for idx, b := range dec { - hash[idx] = b - } - } - return hash -} - -func mirrorData(data []bool, blocks int) []bool { - for x := 0; x < blocks; x++ { - min := x*blocks + 1 - for y := 0; y < blocks; y++ { - a := ((blocks - x - 1) * blocks) + y - b := min + y - 1 - if data[a] { - data[b] = true - } - } - } - return data -} - -func drawImage(data []bool, blocks int, size int, c color.Color) *image.NRGBA { - img := image.NewNRGBA(image.Rect(0, 0, size, size)) - - draw.Draw(img, img.Bounds(), &image.Uniform{color.Gray{240}}, image.Point{0, 0}, draw.Src) - - blockSize := size / (blocks + 1) - border := (size - (blocks * blockSize)) / 2 - - for x := border; x < blockSize*blocks+border; x++ { - bx := (x - border) / blockSize - for y := border; y < blockSize*blocks+border; y++ { - by := (y - border) / blockSize - idx := bx*blocks + by - if data[idx] && (bx < blocks || by < blocks) { - img.Set(x, y, c) - } - } - } - - return img -} - func requestHandler(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] @@ -144,7 +32,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) { } for _, userConfig := range config.Users { - if hashBytes(id) == hashBytes(userConfig.ID) { + if icons.HashBytes(id) == icons.HashBytes(userConfig.ID) { id = userConfig.Alias if len(userConfig.ColorScheme) > 0 { colorScheme = userConfig.ColorScheme @@ -156,19 +44,22 @@ func requestHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Add("Content-Type", "image/png") - cFunc := colorV2 + cFunc := icons.ColorV2 if colorScheme == "v1" { - cFunc = colorV1 + cFunc = icons.ColorV1 } else if colorScheme == "gh" { - cFunc = colorGh + cFunc = icons.ColorGh } + var iconGenerator icons.IconGenerator if pattern == "github" { - err = png.Encode(w, genGhIcon(id, size, cFunc)) + iconGenerator = &icons.GhIconGenerator{} + } else { - err = png.Encode(w, genIdIcon(id, size, cFunc)) + iconGenerator = &icons.IdIconGenerator{} } + err = png.Encode(w, iconGenerator.GenIcon(id, size, cFunc)) } var ( diff --git a/idicon_test.go b/idicon_test.go index 1988ef6..6bc3b6a 100644 --- a/idicon_test.go +++ b/idicon_test.go @@ -1,9 +1,7 @@ package main import ( - "bytes" _ "embed" - "image/png" "net/http" "net/http/httptest" "os" @@ -87,31 +85,6 @@ func TestCorrectResponseForGHColorSchemeAndPattern(t *testing.T) { } } -func TestIgnoreCase(t *testing.T) { - w1 := bytes.NewBuffer([]byte{}) - png.Encode(w1, genIdIcon("example", 80, colorV1)) - - w2 := bytes.NewBuffer([]byte{}) - png.Encode(w2, genIdIcon("Example", 80, colorV1)) - - if bytes.Compare(w1.Bytes(), w2.Bytes()) != 0 { - t.Errorf("resulting images do not match") - } -} - -func TestStringMatchesHash(t *testing.T) { - w1 := bytes.NewBuffer([]byte{}) - // MD5 of lowercase 'example' - png.Encode(w1, genIdIcon("1a79a4d60de6718e8e5b326e338ae533", 80, colorV2)) - - w2 := bytes.NewBuffer([]byte{}) - png.Encode(w2, genIdIcon("Example", 80, colorV2)) - - if bytes.Compare(w1.Bytes(), w2.Bytes()) != 0 { - t.Errorf("resulting images do not match") - } -} - func TestUsesConfig(t *testing.T) { configure("./testdata/testconfig.toml") @@ -154,30 +127,3 @@ func TestCorrectResponseForUserConfig(t *testing.T) { t.Errorf("returned image does not match expected image for mapped alias '42'") } } - -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") - } -}