From a39fb686a303a713b17976875cba3bafb6680fb9 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Mon, 20 May 2024 18:03:43 +0200 Subject: [PATCH] Add support for SVG identicons --- README.adoc | 2 ++ icons/ghicons.go | 23 +++++++++++++++++++++++ icons/icons.go | 34 ++++++++++++++++++++++++++++++++++ icons/idicons.go | 15 +++++++++++++++ idicon.go | 11 +++++++++-- static/index.html | 21 +++++++++++++++++---- 6 files changed, 100 insertions(+), 6 deletions(-) diff --git a/README.adoc b/README.adoc index 0967ea3..d33fa33 100644 --- a/README.adoc +++ b/README.adoc @@ -34,6 +34,8 @@ The latter resembles the color scheme used by GitHub. The request query parameter `d` can be used to request GitHub like patterns by setting the value to `github`. +Using query param `ct` with value `svg` or using request header `Accept: image/svg+xml` will generate SVG identicon. + ==== Examples Some examples for `/avatar/example` and different params. diff --git a/icons/ghicons.go b/icons/ghicons.go index acadcc3..1b7d203 100644 --- a/icons/ghicons.go +++ b/icons/ghicons.go @@ -42,6 +42,29 @@ func (generator *GhIconGenerator) GenIcon(id string, size int) *image.NRGBA { return drawImage(mirrorData(data, blocks), blocks, size, generator.colorGenerator(hash)) } +func (generator *GhIconGenerator) GenSvg(id string, size int) string { + 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 drawSvg(mirrorData(data, blocks), blocks, size, generator.colorGenerator(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) diff --git a/icons/icons.go b/icons/icons.go index 4b857fc..04f68ec 100644 --- a/icons/icons.go +++ b/icons/icons.go @@ -3,6 +3,7 @@ package icons import ( "crypto/md5" "encoding/hex" + "fmt" "image" "image/color" "image/draw" @@ -12,6 +13,7 @@ import ( type IconGenerator interface { GenIcon(id string, size int) *image.NRGBA + GenSvg(id string, size int) string } func HashBytes(id string) [16]byte { @@ -62,5 +64,37 @@ func drawImage(data []bool, blocks int, size int, c color.Color) *image.NRGBA { } } + drawSvg(data, blocks, size, c) + return img } + +func drawSvg(data []bool, blocks int, size int, c color.Color) string { + + blockSize := size / (blocks + 1) + border := (size - (blocks * blockSize)) / 2 + r, g, b, _ := c.RGBA() + colorHtml := fmt.Sprintf("#%x%x%x", r>>8, g>>8, b>>8) + + blockElems := fmt.Sprintf("", size, size) + + for x := 0; x < blocks; x++ { + for y := 0; y < blocks; y++ { + idx := x*blocks + y + if data[idx] { + blockElems += fmt.Sprintf( + ``, + colorHtml, + blockSize, + blockSize, + border+(x*blockSize), + border+(y*blockSize)) + } + } + } + + return fmt.Sprintf(` +%s`, + size, size, + blockElems) +} diff --git a/icons/idicons.go b/icons/idicons.go index c3f9bc8..7fa7dad 100644 --- a/icons/idicons.go +++ b/icons/idicons.go @@ -34,6 +34,21 @@ func (generator *IdIconGenerator) GenIcon(id string, size int) *image.NRGBA { return drawImage(mirrorData(data, blocks), blocks, size, generator.colorGenerator(hash)) } +func (generator *IdIconGenerator) GenSvg(id string, size int) string { + 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 drawSvg(mirrorData(data, blocks), blocks, size, generator.colorGenerator(hash)) +} + func ColorV1(hash [16]byte) color.RGBA { r := 32 + (hash[0]%16)/2<<4 g := 32 + (hash[2]%16)/2<<4 diff --git a/idicon.go b/idicon.go index b6d60ed..568d3a6 100644 --- a/idicon.go +++ b/idicon.go @@ -62,7 +62,6 @@ func requestHandler(w http.ResponseWriter, r *http.Request) { } } - w.Header().Add("Content-Type", "image/png") cFunc := icons.ColorV2 if colorScheme == "v1" { cFunc = icons.ColorV1 @@ -77,7 +76,15 @@ func requestHandler(w http.ResponseWriter, r *http.Request) { iconGenerator = icons.NewIdIconGenerator().WithColorGenerator(cFunc) } - err = png.Encode(w, iconGenerator.GenIcon(id, size)) + ct := r.URL.Query().Get("ct") + cth := r.Header.Get("Accept") + if ct == "svg" || cth == "image/svg+xml" { + w.Header().Add("Content-Type", "image/svg+xml") + _, err = w.Write([]byte(iconGenerator.GenSvg(id, size))) + } else { + w.Header().Add("Content-Type", "image/png") + err = png.Encode(w, iconGenerator.GenIcon(id, size)) + } } var ( diff --git a/static/index.html b/static/index.html index 0a7eb3e..7e18aa8 100644 --- a/static/index.html +++ b/static/index.html @@ -108,18 +108,18 @@ width: 90%; } - #size-input, #color-input, #type-input { + #size-input, #color-input, #type-input, #contenttype-input { display: flex; flex-direction: column; margin: 0 auto; } - #size-input > input, #color-input > select, #type-input > select { + #size-input > input, #color-input > select, #type-input > select, #contenttype-input > select { width: 8em; } #settings { - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); display: grid; } @@ -185,6 +185,13 @@ +
+ + +
Actions @@ -212,6 +219,7 @@ let currentsize = document.getElementById('size').value; let currentcolor = document.getElementById('color').value; let currenttype = document.getElementById('type').value; + let currentcontenttype = document.getElementById('contenttype').value; idicon(document.getElementById('value').value); function fetchGhId() { @@ -253,12 +261,17 @@ idicon(document.getElementById('value').value); } + function newcontenttype(value) { + currentcontenttype = value; + idicon(document.getElementById('value').value); + } + function idicon(value) { if (value.trim() === '') { document.getElementById('idicon').src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12NgAAIAAAUAAeImBZsAAAAASUVORK5CYII='; return; } - document.getElementById('idicon').src = `./avatar/${value}?s=${currentsize}&c=${currentcolor}&d=${currenttype}` + document.getElementById('idicon').src = `./avatar/${value}?s=${currentsize}&c=${currentcolor}&d=${currenttype}&ct=${currentcontenttype}` } \ No newline at end of file