add automatic github-style avatar for dev auth #106

This commit is contained in:
Umputun
2018-07-10 12:19:10 -05:00
parent b076a06e33
commit b2a8434f9a
14 changed files with 1140 additions and 14 deletions
+7 -1
View File
@@ -126,6 +126,12 @@
packages = ["."]
revision = "542fd4642604d0d0c26112396ce5b1a9d01eee0b"
[[projects]]
branch = "master"
name = "github.com/nullrocks/identicon"
packages = ["."]
revision = "7875f45b0022edded6377e40639d8aa620193a62"
[[projects]]
name = "github.com/patrickmn/go-cache"
packages = ["."]
@@ -232,6 +238,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "af8b7f1817ce6e82746722a745184bd50c341733bcc024311aa956ea32475796"
inputs-digest = "38280a8f373ce1799e64ac1709e9748ae8cf6cca30dfeabca807d2db9fe34ab8"
solver-name = "gps-cdcl"
solver-version = 1
+50 -12
View File
@@ -1,6 +1,7 @@
package auth
import (
"bytes"
"context"
"fmt"
"log"
@@ -9,6 +10,8 @@ import (
"sync"
"time"
"github.com/nullrocks/identicon"
"github.com/pkg/errors"
"golang.org/x/oauth2"
"github.com/umputun/remark/backend/app/store"
@@ -19,22 +22,28 @@ const devAuthPort = 8084
// DevAuthServer is a fake oauth server for development
// it provides stand-alone server running on its own port and pretending to be the real oauth2. It also provides
// Dev Provider the same way as normal providers di, i.e. github, google and others.
// can run in interractive and non-interactive mode. In interactive mode login attempts will show login form to select
// can run in interactive and non-interactive mode. In interactive mode login attempts will show login form to select
// desired user name.
type DevAuthServer struct {
Provider Provider
username string // unsafe, but fine for dev
nonInteractive bool
httpServer *http.Server
lock sync.Mutex
iconGen *identicon.Generator
httpServer *http.Server
lock sync.Mutex
}
// Run oauth2 dev server on port devAuthPort
func (d *DevAuthServer) Run() {
log.Printf("[INFO] run local oauth2 dev server on %d", devAuthPort)
d.lock.Lock()
var err error
d.iconGen, err = identicon.New("github", 5, 3)
if err != nil {
log.Printf("[WARN] can't create identicon, %s", err)
}
d.httpServer = &http.Server{
Addr: fmt.Sprintf(":%d", devAuthPort),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -42,9 +51,9 @@ func (d *DevAuthServer) Run() {
switch {
case strings.HasPrefix(r.URL.Path, "/login/oauth/authorize"):
// first time it will be called without usernam and will ask for onw
// first time it will be called without username and will ask for onw
if !d.nonInteractive && (r.ParseForm() != nil || r.Form.Get("username") == "") {
if _, err := w.Write([]byte(fmt.Sprintf(devUserForm, r.URL.RawQuery))); err != nil {
if _, err = w.Write([]byte(fmt.Sprintf(devUserForm, r.URL.RawQuery))); err != nil {
log.Printf("[WARN] can't write, %s", err)
}
return
@@ -70,19 +79,33 @@ func (d *DevAuthServer) Run() {
"state":"12345678"
}`
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write([]byte(res)); err != nil {
if _, err = w.Write([]byte(res)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
case strings.HasPrefix(r.URL.Path, "/user"):
ava := fmt.Sprintf("http://127.0.0.1:%d/avatar?user=%s", devAuthPort, d.username)
res := fmt.Sprintf(`{
"id": "%s",
"name":"%s"
}`, d.username, d.username)
"name":"%s",
"picture":"%s"
}`, d.username, d.username, ava)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write([]byte(res)); err != nil {
if _, err = w.Write([]byte(res)); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
case strings.HasPrefix(r.URL.Path, "/avatar"):
user := r.URL.Query().Get("user")
b, e := d.genAvatar(user)
if e != nil {
w.WriteHeader(http.StatusNotFound)
return
}
if _, err = w.Write(b); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
@@ -94,7 +117,7 @@ func (d *DevAuthServer) Run() {
}
d.lock.Unlock()
err := d.httpServer.ListenAndServe()
err = d.httpServer.ListenAndServe()
log.Printf("[WARN] dev oauth2 server terminated, %s", err)
}
@@ -128,13 +151,28 @@ func NewDev(p Params) Provider {
userInfo := store.User{
ID: data.value("id"),
Name: data.value("name"),
Picture: "",
Picture: data.value("picture"),
}
return userInfo
},
})
}
func (d *DevAuthServer) genAvatar(user string) ([]byte, error) {
if d.iconGen == nil {
return nil, errors.Errorf("no iconGen, skip avatar generation for %s", user)
}
ii, err := d.iconGen.Draw(user) // Generate an IdentIcon
if err != nil {
return nil, errors.Wrapf(err, "failed to draqw avatar for %s", user)
}
buf := &bytes.Buffer{}
err = ii.Png(300, buf)
return buf.Bytes(), err
}
var devUserForm = `
<html>
<head>
+9 -1
View File
@@ -62,7 +62,15 @@ func TestDevProvider(t *testing.T) {
assert.Nil(t, err)
u := *claims.User
assert.Equal(t, store.User{Name: "dev_user", ID: "dev_user", Picture: "", IP: "",
assert.Equal(t, store.User{Name: "dev_user", ID: "dev_user", Picture: "http://127.0.0.1:8084/avatar?user=dev_user", IP: "",
Admin: true, Blocked: false, Verified: false}, u)
// check avatar
resp, err = client.Get("http://127.0.0.1:8084/avatar?user=dev_user")
require.Nil(t, err)
assert.Equal(t, 200, resp.StatusCode)
body, err = ioutil.ReadAll(resp.Body)
assert.Nil(t, err)
assert.Equal(t, 985, len(body))
t.Logf("headers: %+v", resp.Header)
}
+33
View File
@@ -0,0 +1,33 @@
# Testing and Developing
main/
experimental/
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
.vscode
.idea
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
.vscode/*
# System Files
.DS_Store
Thumbs.db
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Ruben Rivera
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.
+91
View File
@@ -0,0 +1,91 @@
# IdentIcon
![CircleCI](https://img.shields.io/circleci/project/github/RedSparr0w/node-csgo-parser.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/nullrocks/identicon)](https://goreportcard.com/report/github.com/nullrocks/identicon) [![](https://godoc.org/github.com/nullrocks/identicon?status.svg)](http://godoc.org/github.com/nullrocks/identicon)
**IdentIcon** is an open source avatar generator inspired by GitHub avatars.
IdentIcon uses a deterministic algorithm that generates an image (using Golang's stdlib image encoders) based on a text (Generally Usernames, Emails or just random strings), by hashing it and iterating over the bytes of the digest to pick whether to draw a point, pick a color or choose where to go next.
IdentIcon's Generator enables the creation of customized figures: (NxN size, points density, custom color palette) as well as multiple exporting formats in case the developers want to generate their own images.
## Installation
```bash
$ go get github.com/nullrocks/identicon
```
## Usage
```go
import (
"os"
"github.com/nullrocks/identicon"
)
// New Generator: Rehuse
ig, err := identicon.New(
"github", // Namespace
5, // Number of blocks (Size)
3, // Density
)
if err != nil {
panic(err) // Invalid Size or Density
}
username := "nullrocks" // Text - decides the resulting figure
ii, err := ig.Draw(username) // Generate an IdentIcon
if err != nil {
panic(err) // Text is empty
}
// File writer
img, _ := os.Create("icon.png")
defer img.Close()
// Takes the size in pixels and any io.Writer
ii.Png(300, img) // 300px * 300px
```
## Examples
### 5x5
|nullrocks | johndoe | abc123 | modulo |
:------------------------------------------------:|:---------------------------------------------:|:-------------------------------------------:|:--------------------------------------------|
![nullrocks](./examples/5x5/nullrocks.png) | ![johndoe](./examples/5x5/johndoe.png) | ![abc123](./examples/5x5/abc123.png) | ![modulo](./examples/5x5/modulo.png) |
![nullrocks](./examples/5x5/nullrocks_itx.png) | ![johndoe](./examples/5x5/johndoe_itx.png) | ![abc123](./examples/5x5/abc123_itx.png) | ![modulo](./examples/5x5/modulo_itx.png) |
![nullrocks](./examples/5x5/nullrocks_github.png) | ![johndoe](./examples/5x5/johndoe_github.png) | ![abc123](./examples/5x5/abc123_github.png) | ![modulo](./examples/5x5/modulo_github.png) |
### 7x7
|nullrocks | johndoe | abc123 | modulo |
:------------------------------------------------:|:---------------------------------------------:|:-------------------------------------------:|:---------------------------------------------|
![nullrocks](./examples/7x7/nullrocks.png) | ![johndoe](./examples/7x7/johndoe.png) | ![abc123](./examples/7x7/abc123.png) | ![modulo](./examples/7x7/modulo.png) |
![nullrocks](./examples/7x7/nullrocks_itx.png) | ![johndoe](./examples/7x7/johndoe_itx.png) | ![abc123](./examples/7x7/abc123_itx.png) | ![modulo](./examples/7x7/modulo_itx.png) |
![nullrocks](./examples/7x7/nullrocks_github.png) | ![johndoe](./examples/7x7/johndoe_github.png) | ![abc123](./examples/7x7/abc123_github.png) | ![modulo](./examples/7x7/modulo_github.png) |
### 10x10
|nullrocks | johndoe | abc123 | modulo |
:--------------------------------------------------:|:-----------------------------------------------:|:---------------------------------------------:|:----------------------------------------------|
![nullrocks](./examples/10x10/nullrocks.png) | ![johndoe](./examples/10x10/johndoe.png) | ![abc123](./examples/10x10/abc123.png) | ![modulo](./examples/10x10/modulo.png) |
![nullrocks](./examples/10x10/nullrocks_itx.png) | ![johndoe](./examples/10x10/johndoe_itx.png) | ![abc123](./examples/10x10/abc123_itx.png) | ![modulo](./examples/10x10/modulo_itx.png) |
![nullrocks](./examples/10x10/nullrocks_github.png) | ![johndoe](./examples/10x10/johndoe_github.png) | ![abc123](./examples/10x10/abc123_github.png) | ![modulo](./examples/10x10/modulo_github.png) |
[View examples](./examples)
## Documentation
## Changelog
## Contribution
## License
MIT
Copyright (c) 2018-present, Ruben Rivera
+1
View File
@@ -0,0 +1 @@
theme: jekyll-theme-cayman
+121
View File
@@ -0,0 +1,121 @@
package identicon
import (
"image"
"strconv"
)
// Canvas contains what is needed to generate an image. It contains properties
// that could be useful when rendering the image.
// - Having MinY and MaxY allows you to vertically center the figure.
// - VisitedYPoints could be useful to determine whether there is a big empty
// vertical space in the figure.
type Canvas struct {
// Size same value specified in identicon.New(...).
Size int
// PointsMap contains all coordinates and it's values that form the figure.
PointsMap map[int]map[int]int
// MinY is the upper Y-axis that has at least one point drawn.
MinY int
// MaxY is the lower Y-axis that has at least one point drawn.
MaxY int
// VisitedYPoints contains all Y-axis that had been visited. Helpful to
// determine big blank spaces in the resulting figure.
VisitedYPoints map[int]bool
// FilledPoints is the number of points filled at least once.
FilledPoints int
}
// Array generates a two-dimensional array version of the IdentIcon figure.
func (c *Canvas) Array() [][]int {
canvasArray := make([][]int, c.Size)
for i := range canvasArray {
canvasArray[i] = make([]int, c.Size)
}
for y := range c.PointsMap {
for x := range c.PointsMap[y] {
canvasArray[y][x] = c.PointsMap[y][x]
}
}
return canvasArray
}
// ToString generates a string version of the IdentIcon figure.
func (c *Canvas) String(separator string, fillEmptyWith string) string {
tp := c.Size * c.Size
// Total number of characters considering:
strLen := c.Size - 1 // Line Breaks
strLen += (tp - c.Size) * len(separator) // Separators
strLen += c.FilledPoints // Points
strLen += (tp - c.FilledPoints) * len(fillEmptyWith) // Fill Empty
// Concatenating strings with the `+` is slow and uses a lot of memory,
// using `copy` in a slice of bytes has been proved to be a better approach.
bs := make([]byte, strLen)
// Keep track of the length of the bytes array, concatenations will occur
// using `bl` as the right-most index.
bl := 0
for y := 0; y < c.Size; y++ {
if mapY, exists := c.PointsMap[y]; exists {
for x := 0; x < c.Size; x++ {
if value, exists := mapY[x]; exists {
if value > 9 {
value = 9
}
bl += copy(bs[bl:], []byte(strconv.Itoa(value)))
} else {
bl += copy(bs[bl:], []byte(fillEmptyWith))
}
if x < c.Size-1 {
bl += copy(bs[bl:], []byte(separator))
}
}
} else {
// There aren't any values in this row, fill it the row anyway.
for x := 0; x < c.Size; x++ {
bl += copy(bs[bl:], []byte(fillEmptyWith))
if x < c.Size-1 {
bl += copy(bs[bl:], []byte(separator))
}
}
}
if y < c.Size-1 {
// Append a line break except when it's the last line.
bl += copy(bs[bl:], "\n")
}
}
return string(bs)
}
// Points generates an array of points of a two-dimensional plane as [x, y]
// that correspond to all filled points in the IdentIcon figure.
func (c *Canvas) Points() []image.Point {
points := []image.Point{}
for y, value := range c.PointsMap {
for x := range value {
points = append(points, image.Point{X: x, Y: y})
}
}
return points
}
// IntCoordinates generates an array of points of a two-dimensional plane as:
// - [x, y] that correspond to all filled points in the IdentIcon figure.
func (c *Canvas) IntCoordinates() [][]int {
points := [][]int{}
for y, value := range c.PointsMap {
for x := range value {
points = append(points, []int{x, y})
}
}
return points
}
+11
View File
@@ -0,0 +1,11 @@
# Golang CircleCI 2.0 configuration file
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.10
working_directory: /go/src/github.com/nullrocks/identicon
steps:
- checkout
- run: go get -v -t -d ./...
- run: go test -v ./...
+170
View File
@@ -0,0 +1,170 @@
package identicon
import (
"crypto/sha256"
"errors"
"image/color"
"math/rand"
"strconv"
"time"
)
// Generator represents a predefined set of configurations that can be reused to
// create multiple icons by passing a Text string only.
type Generator struct {
// Namespace that will be concatenated previous to the icon generation.
Namespace string
// Size is the number of blocks of the figure.
Size int
// Density * Size = times to iterate over the hash of Text:Namespace:Seed.
Density int
// hashFunction used to generate a fixed length array of bytes.
hashFunction func([]byte) []byte
// fillColorFunction used to pick a color to fill the squares of the figure.
fillColorFunction func([]byte) color.Color
// backgroundColorFunction used to pick a background color for the figure.
backgroundColorFunction func([]byte, color.Color) color.Color
// isRandom flag to decide whether the generated image will be randomized.
isRandom bool
// rand is the source of randomness.
rand *rand.Rand
}
// option Configuration functional approach
type option func(*Generator)
// New returns a pointer to a Generator with the desired configuration.
func New(
namespace string,
size int,
density int,
opts ...option,
) (*Generator, error) {
if size < MinSize {
// Smaller values will generate a meaningless Generator.
return nil, errors.New(
"Size cannot be less than " + strconv.Itoa(MinSize),
)
}
if density < 1 {
return nil, errors.New(
"Density cannot be less than 1",
)
}
g := Generator{
Size: size,
Namespace: namespace,
Density: density,
isRandom: false,
hashFunction: _sha256,
fillColorFunction: _fillColor,
backgroundColorFunction: _backgroundColor,
}
g.Option(opts...)
return &g, nil
}
// Draw returns a pointer to an IdentIcon with a generated figure and a color.
func (g Generator) Draw(text string) (*IdentIcon, error) {
var randomGenerator *rand.Rand
if g.isRandom {
// In order to generate a randomized Canvas, use UnixNano as the source
// of randomess. By doing this, random numbers won't be consistent.
randomGenerator = rand.New(rand.NewSource(time.Now().UnixNano()))
} else {
// rand will generate consistent values since the source is Size.
randomGenerator = rand.New(rand.NewSource(int64(g.Size)))
}
ii, err := newIdentIcon(
text,
g.Namespace,
g.Size,
g.Density,
g.isRandom,
randomGenerator,
g.hashFunction,
g.fillColorFunction,
g.backgroundColorFunction,
)
if err != nil {
return nil, err
}
// Generate Canvas
ii.Draw()
return ii, nil
}
// Option sets the options specified.
func (g *Generator) Option(opts ...option) {
for _, opt := range opts {
opt(g)
}
}
// SetHashFunction replaces the default hash function (Sha256).
func SetHashFunction(hf func([]byte) []byte) option {
return func(g *Generator) {
g.hashFunction = hf
}
}
// SetFillColorFunction replaces the default color generation function (HSL).
func SetFillColorFunction(fcf func([]byte) color.Color) option {
return func(g *Generator) {
g.fillColorFunction = fcf
}
}
// SetBackgroundColorFunction replaces the default background's color generation
// function (HSL).
func SetBackgroundColorFunction(bcf func([]byte, color.Color) color.Color) option {
return func(g *Generator) {
g.backgroundColorFunction = bcf
}
}
// SetRandom to append a random string to the generator text everytime Draw is
// called.
func SetRandom(r bool) option {
return func(g *Generator) {
g.isRandom = r
}
}
func _sha256(b []byte) []byte {
digest := sha256.Sum256(b)
return digest[:]
}
func _fillColor(hashBytes []byte) color.Color {
cb1, cb2 := uint32(hashBytes[0]), uint32(hashBytes[1])
h := (cb1 + cb2) % 360
s := (cb1 % 30) + 60
l := (cb2 % 20) + 40
// Some colors in the HSL color model are too bright and don't play well
// with the default background color. This is a naïve normalization method.
if (h >= 50 && h <= 85) || (h >= 170 && h <= 190) {
s = 80
l -= 20
} else if h > 85 && h < 170 {
l -= 10
}
return HSL{h, s, l}
}
func _backgroundColor(hashBytes []byte, fill color.Color) color.Color {
return color.NRGBA{R: 240, G: 240, B: 240, A: 255}
}
+83
View File
@@ -0,0 +1,83 @@
package identicon
// Identicon WebColor maxium values.
const (
// hueMax is the maximum allowed value for Hue in the HSL color model.
hueMax = 360
// saturationMax is the maximum allowed value for Saturation in the HSL
// color model.
saturationMax = 100
// lightnessMax is the maximum allowed value for lightnessMax in the HSL
// color model.
lightnessMax = 100
// rgbaMax is the maximum allowed value for any R, G, B, A property value.
rgbaMax = 255
)
// HSL is a color model representation based on RGB. HSL facilitates the
// generation of colors that look similar between themselves by changing the
// value of Hue H while keeping Saturation S and Lightness L the same.
type HSL struct {
// Hue [0, 360]
H uint32
// Saturation [0, 100]
S uint32
// Lightness [0, 100]
L uint32
}
// RGBA conversion
func (hsl HSL) RGBA() (r, g, b, a uint32) {
h := 1.0 / float64(hueMax) * float64(hsl.H)
s := float64(hsl.S) / float64(saturationMax)
l := float64(hsl.L) / float64(lightnessMax)
r, g, b = hslToRgb(h, s, l)
a = rgbaMax
r |= r << 8
g |= g << 8
b |= b << 8
a |= a << 8
return
}
// Golang port of Mohen's code in Stack Overflow.
// https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
func hslToRgb(h, s, l float64) (uint32, uint32, uint32) {
var q, p float64
var r, g, b float64
if s == 0 {
r = l
g = l
b = l
} else {
if l < 0.5 {
q = l * (1 + s)
} else {
q = (l + s) - (l * s)
}
p = (2 * l) - q
r = hueToRgb(p, q, h+(1.0/3.0))
g = hueToRgb(p, q, h)
b = hueToRgb(p, q, h-(1.0/3.0))
}
return uint32(r * rgbaMax), uint32(g * rgbaMax), uint32(b * rgbaMax)
}
func hueToRgb(p, q, t float64) float64 {
if t < 0 {
t++
} else if t > 1 {
t--
}
switch {
case 6*t < 1:
return (p + (q-p)*6*t)
case 2*t < 1:
return q
case 3*t < 2:
return p + (q-p)*((2.0/3.0)-t)*6
}
return p
}
+378
View File
@@ -0,0 +1,378 @@
// Package identicon is an open source avatar generator inspired by GitHub avatars.
//
// IdentIcon uses a deterministic algorithm that generates an image (using Golang's
// stdlib image encoders) based on a text (Generally Usernames, Emails or just
// random strings), by hashing it and iterating over the bytes of the digest to pick
// whether to draw a point, pick a color or choose where to go next.
//
// IdentIcon's Generator enables the creation of customized figures: (NxN size,
// points density, custom color palette) as well as multiple exporting formats in
// case the developers want to generate their own images.
package identicon
import (
"errors"
"image"
"image/color"
"math/rand"
"strconv"
)
const (
// Bits used to give continuity
moveUp = 0x80
moveDown = 0x40
moveLeft = 0x20
moveRight = 0x10
// Either 0x8 or 0x2 are active
fillPoint = 0xA
)
// Constrains for the size of the IdentIcon.
const (
// MinSize is the minimal number of blocks allowed, anything lower that this
// wouldn't make sense.
MinSize = 4
)
// IdentIcon represents a mirror-symmetry image generated from a string and a
// set of configurations.
type IdentIcon struct {
// Text is the base string that will generate the canvas after being hashed.
Text string
// Namespace
Namespace string
// Size is the number of blocks of the figure.
Size int
// Density * Size = times to iterate over the hash of Text.
Density int
// Canvas is a map of maps that contains the points and values that has been
// visited and filled.
Canvas Canvas
// FillColor is the color used to fill squares in the figure when encoding
// to PNG or JPEG.
FillColor color.Color
// BackgroundColor is the background color of the figure when encoding it to
// PNG or JPEG.
BackgroundColor color.Color
// fillColorFunction used to pick a color to fill the squares of the figure.
fillColorFunction func([]byte) color.Color
// backgroundColorFunction used to pick a background color for the figure.
backgroundColorFunction func([]byte, color.Color) color.Color
// drawableWidth represents the length of the left half of the canvas.
drawableWidth int
// hasBeenDrawn indicates whether the Draw() has been called before.
hasBeenDrawn bool
// hashFunction used to generate a fixed length array of bytes.
hashFunction func([]byte) []byte
// isRandom flag to decide whether the generated image will be randomized.
isRandom bool
// randomSeed
randomSeed string
// rand is the source of randomness.
rand *rand.Rand
}
// Draw a figure in Canvas.
// - If isRandom == true, the figure will redrawn everytime Draw() is called,
// - If isRandom == false and Draw() was called before, it won't redraw.
func (ii *IdentIcon) Draw() {
if ii.hasBeenDrawn && !ii.isRandom {
// Don't redraw once twice unless isRandom is enabled.
return
} else if ii.isRandom {
// Set a new randomSeed everytime Draw is executed to produce different
// results on each execution.
ii.randomSeed = strconv.Itoa(ii.rand.Int())
}
ii.hasBeenDrawn = true
// Make sure that the canvas has been initialized.
ii.initCanvas()
// current index of the digested bytes array.
var i int
// Number of bytes readed.
var readedBytes int
// Flag to know whether it as completed a full cycle.
var hasCompletedCycle bool
// Position that represents a point in the canvas.
var current image.Point
// Text:Namespace:randomSeed
generatingBytes := []byte(ii.GeneratorText())
// Produce fixed-length array of bytes that will be used to control the
// drawing process.
hashBytes := ii.hashFunction(generatingBytes)
hashBytesLen := len(hashBytes)
ii.FillColor = ii.fillColorFunction(hashBytes)
ii.BackgroundColor = ii.backgroundColorFunction(hashBytes, ii.FillColor)
// Total number of iterations over the digested hash.
bytesToRead := ii.Density * ii.Size
for {
if hasCompletedCycle {
// If the number of bytes to read exceeds the length of the hash,
// it will cycle through it. After it has completed a whole cycle,
// altering the value will produce more varied figures.
//
// XOR pseudo-random produces interesting results.
hashBytes[i] ^= byte(ii.rand.Intn(255))
}
if i == 0 {
// Everytime a new cycle is starting, change the current point to
// cover multiple areas of the canvas.
current = initialPoint(
hashBytes[0],
ii.rand.Intn(ii.drawableWidth),
ii.rand.Intn(ii.Size),
)
}
// value to add in the current point, zeroes will be ignored.
value := getFillValue(hashBytes[i])
if value != 0 {
// Initialize the map for Y-axis, making sure that the map that
// contains X-axis values won't be nil.
createMapIfDoesntExist(&ii.Canvas, current.Y)
firstTimeFilled := false
if ii.Canvas.PointsMap[current.Y][current.X] == 0 {
// Increment FilledPoints the first time this point is visited.
ii.Canvas.FilledPoints++
firstTimeFilled = true
}
// Add the value to current position
ii.Canvas.PointsMap[current.Y][current.X] += value
// Mark Y value as visited. This will be helpful to determine big
// blank spaces in the resulting figure.
ii.Canvas.VisitedYPoints[current.Y] = true
// Update the maximum and minimum Y-axis values, useful to
// vertically center the figure at image creation.
if current.Y < ii.Canvas.MinY {
ii.Canvas.MinY = current.Y
}
if current.Y > ii.Canvas.MaxY {
ii.Canvas.MaxY = current.Y
}
// When Size is an odd number, prevent points in the middle to be
// added twice. By substrating oddDiff to drawableWidth we make sure
// that it doesn't happens.
oddDiff := ii.Size % 2
if current.X < (ii.drawableWidth - oddDiff) {
// Calculate the mirror position for X-axis
mirror := mirrorSymmetric(current, ii.Size)
// Add value to the mirrowed position
ii.Canvas.PointsMap[mirror.Y][mirror.X] += value
if firstTimeFilled {
ii.Canvas.FilledPoints++
}
}
}
// Decide the next position relative to the current position.
current = nextPoint(hashBytes[i], current, ii.drawableWidth, ii.Size)
i++
readedBytes++
if readedBytes >= bytesToRead {
// The total number of bytes to read has been reached, stop.
break
}
if i == hashBytesLen-1 {
// A full cycle has been completed, reset the index to prevent
// getting out of bounds.
i = 0
// Further iterations will add a pesudo-random number to hashBytes.
hasCompletedCycle = true
}
}
}
// GeneratorText returns the string later to be hashed using the format:
// - Text[:Namespace][:randomSeed]
func (ii *IdentIcon) GeneratorText() string {
gt := ii.Text
if ii.Namespace != "" {
gt += ":" + ii.Namespace
}
if ii.isRandom && ii.randomSeed != "" {
gt += ":" + ii.randomSeed
}
return gt
}
// Array generates a two-dimensional array version of the IdentIcon figure.
func (ii *IdentIcon) Array() [][]int {
return ii.Canvas.Array()
}
// ToString generates a string version of the IdentIcon figure.
func (ii *IdentIcon) String(separator string, fillEmptyWith string) string {
return ii.Canvas.String(separator, fillEmptyWith)
}
// Points generates an array of points of a two-dimensional plane as [x, y]
// that correspond to all filled points in the IdentIcon figure.
func (ii *IdentIcon) Points() []image.Point {
return ii.Canvas.Points()
}
// IntCoordinates generates an array of points of a two-dimensional plane as:
// - [x, y] that correspond to all filled points in the IdentIcon figure.
func (ii *IdentIcon) IntCoordinates() [][]int {
return ii.Canvas.IntCoordinates()
}
// New returns a pointer to IdentIcon.
func newIdentIcon(
text string,
namespace string,
size int,
density int,
isRandom bool,
rand *rand.Rand,
hashFunction func([]byte) []byte,
fillColorFunction func([]byte) color.Color,
backgroundColorFunction func([]byte, color.Color) color.Color,
) (*IdentIcon, error) {
if text == "" {
// Text is the minimum requirement to generate an IdentIcon.
return nil, errors.New("Text can't be empty")
}
if size < MinSize {
// Smaller values will generate a meaningless Generator.
return nil, errors.New(
"Size cannot be less than " + strconv.Itoa(MinSize),
)
}
if density < 1 {
return nil, errors.New(
"Density cannot be less than 1",
)
}
identicon := IdentIcon{
Text: text,
Namespace: namespace,
Size: size,
Density: density,
isRandom: isRandom,
rand: rand,
hashFunction: hashFunction,
fillColorFunction: fillColorFunction,
backgroundColorFunction: backgroundColorFunction,
}
// Reflection Line
identicon.drawableWidth = identicon.Size / 2
// Since the canvas is a symmetrical reflection make sure to:
// - Handle even and odd Canvas sizes
if identicon.Size%2 == 1 {
// Is odd, the vertical middle point exist.
identicon.drawableWidth++
}
return &identicon, nil
}
// initCanvas initializes and erases everything that was in the Canvas map.
func (ii *IdentIcon) initCanvas() {
ii.Canvas = Canvas{
Size: ii.Size,
PointsMap: make(map[int]map[int]int),
MinY: ii.Size,
MaxY: 0,
VisitedYPoints: make(map[int]bool),
}
}
func nextPoint(control byte, p image.Point, width, heigth int) image.Point {
// Active bits will decide the destination of the next point.
// - If two opposite bits are active, it will keep its current position.
if control&moveUp == moveUp {
p.Y--
}
if control&moveDown == moveDown {
p.Y++
}
if control&moveLeft == moveLeft {
p.X--
}
if control&moveRight == moveRight {
p.X++
}
// Transform to 0-based indices.
width--
heigth--
// Teleport to opposite bounds when the limit has been reached.
if p.X > width {
p.X = 0
} else if p.X < 0 {
p.X = width
}
if p.Y > heigth {
p.Y = 0
} else if p.Y < 0 {
p.Y = heigth
}
return p
}
func initialPoint(control byte, width, heigth int) image.Point {
return image.Point{
Y: heigth,
X: width,
}
}
func mirrorSymmetric(p image.Point, size int) image.Point {
return image.Point{
Y: p.Y,
X: size - p.X - 1,
}
}
func getFillValue(control byte) int {
if control&fillPoint > 0 {
return 1
}
return 0
}
func createMapIfDoesntExist(canvas *Canvas, y int) {
_, exist := canvas.PointsMap[y]
if !exist {
canvas.PointsMap[y] = make(map[int]int)
}
}
+73
View File
@@ -0,0 +1,73 @@
package identicon
import (
"image"
"image/draw"
"image/jpeg"
"image/png"
"io"
)
// Image genetares an image.Image of size
func (ii *IdentIcon) Image(pixels int) image.Image {
// Padding is relative to the number of blocks.
padding := pixels / (ii.Size * MinSize)
drawableArea := pixels - (padding * 2)
blockSize := drawableArea / ii.Size
// Add the residue (pixels that won't be filled) to the padding.
// Try to center the figure regardless when the drawable area is not
// divisible by the block pixels.
padding += (drawableArea % ii.Size) / 2
img := image.NewNRGBA(image.Rect(0, 0, pixels, pixels))
// Background
draw.Draw(
img,
img.Bounds(),
&image.Uniform{ii.BackgroundColor},
image.ZP,
draw.Src,
)
for y, mapX := range ii.Canvas.PointsMap {
for x := range mapX {
ix := blockSize*x + padding
iy := blockSize*y + padding
draw.Draw(img,
image.Rect(
ix,
iy,
ix+blockSize,
iy+blockSize,
),
&image.Uniform{ii.FillColor},
image.ZP,
draw.Src,
)
}
}
return img
}
// Png writes an image of pixels
func (ii *IdentIcon) Png(pixels int, w io.Writer) error {
img := ii.Image(pixels)
return png.Encode(w, img)
}
// Jpeg writes an image of pixels and quality
func (ii *IdentIcon) Jpeg(pixels int, quality int, w io.Writer) error {
img := ii.Image(pixels)
return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
}
// Svg writes an image of pixels
func (ii *IdentIcon) Svg(pixels int, w io.Writer) error {
return svgEncode(w, ii, pixels)
}
+92
View File
@@ -0,0 +1,92 @@
package identicon
import (
"image/color"
"io"
"strconv"
"text/template"
)
type svgRect struct {
X int
Y int
Width int
Height int
FillColor string
}
type svgTmpl struct {
Pixels int
BackgroundColor string
FillColor string
Rects []svgRect
}
const svgTemplate = `
<!-- <?xml version="1.0"?> -->
<svg version="1.1" baseprofile="full" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" width="{{.Pixels}}" height="{{.Pixels}}" viewBox="0 0 {{.Pixels}} {{.Pixels}}" >
<rect width="100%" height="100%" fill="{{.BackgroundColor}}"/>
{{range $index, $r := .Rects}}
<rect x="{{$r.X}}" y="{{$r.Y}}" width="{{$r.Width}}" height="{{$r.Height}}" fill="{{$r.FillColor}}"/>{{end}}
</svg>`
func colorToRGBAString(c color.Color) string {
r, g, b, a := c.RGBA()
r >>= 8
g >>= 8
b >>= 8
a >>= 8
rs := strconv.Itoa(int(r))
gs := strconv.Itoa(int(g))
bs := strconv.Itoa(int(b))
as := strconv.Itoa(int(a))
return "rgba(" + rs + "," + gs + "," + bs + "," + as + ")"
}
// Encode an IdentIcon to SVG
func svgEncode(w io.Writer, ii *IdentIcon, pixels int) error {
// Padding is relative to the number of blocks.
padding := pixels / (ii.Size * MinSize)
drawableArea := pixels - (padding * 2)
blockSize := drawableArea / ii.Size
// Add the residue (pixels that won't be filled) to the padding.
// Try to center the figure regardless when the drawable area is not
// divisible by the block pixels.
padding += (drawableArea % ii.Size) / 2
fillColor := colorToRGBAString(ii.FillColor)
backgroundColor := colorToRGBAString(ii.BackgroundColor)
b, err := template.New("svg").Parse(svgTemplate)
if err != nil {
return err
}
t := svgTmpl{
Pixels: pixels,
BackgroundColor: backgroundColor,
FillColor: fillColor,
Rects: make([]svgRect, ii.Canvas.FilledPoints),
}
i := 0
for y, mapX := range ii.Canvas.PointsMap {
for x := range mapX {
t.Rects[i] = svgRect{
blockSize*x + padding,
blockSize*y + padding,
blockSize,
blockSize,
fillColor,
}
i++
}
}
return b.Execute(w, t)
}