diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index 586a3e64..41b0685d 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -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 diff --git a/backend/app/rest/auth/dev_provider.go b/backend/app/rest/auth/dev_provider.go index 985f0530..0d5b111f 100644 --- a/backend/app/rest/auth/dev_provider.go +++ b/backend/app/rest/auth/dev_provider.go @@ -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 = `
diff --git a/backend/app/rest/auth/dev_provider_test.go b/backend/app/rest/auth/dev_provider_test.go index 831ff3c1..448ac29c 100644 --- a/backend/app/rest/auth/dev_provider_test.go +++ b/backend/app/rest/auth/dev_provider_test.go @@ -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) } diff --git a/backend/vendor/github.com/nullrocks/identicon/.gitignore b/backend/vendor/github.com/nullrocks/identicon/.gitignore new file mode 100644 index 00000000..8344b618 --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/.gitignore @@ -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 \ No newline at end of file diff --git a/backend/vendor/github.com/nullrocks/identicon/LICENSE b/backend/vendor/github.com/nullrocks/identicon/LICENSE new file mode 100644 index 00000000..ab277bad --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/LICENSE @@ -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. \ No newline at end of file diff --git a/backend/vendor/github.com/nullrocks/identicon/README.md b/backend/vendor/github.com/nullrocks/identicon/README.md new file mode 100644 index 00000000..67ef8c4b --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/README.md @@ -0,0 +1,91 @@ +# IdentIcon + + + + [](https://goreportcard.com/report/github.com/nullrocks/identicon) [](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 | +:------------------------------------------------:|:---------------------------------------------:|:-------------------------------------------:|:--------------------------------------------| + |  |  |  | + |  |  |  | + |  |  |  | + +### 7x7 +|nullrocks | johndoe | abc123 | modulo | +:------------------------------------------------:|:---------------------------------------------:|:-------------------------------------------:|:---------------------------------------------| + |  |  |  | + |  |  |  | + |  |  |  | + +### 10x10 +|nullrocks | johndoe | abc123 | modulo | +:--------------------------------------------------:|:-----------------------------------------------:|:---------------------------------------------:|:----------------------------------------------| + |  |  |  | + |  |  |  | + |  |  |  | + +[View examples](./examples) + +## Documentation + +## Changelog + +## Contribution + +## License +MIT + +Copyright (c) 2018-present, Ruben Rivera + diff --git a/backend/vendor/github.com/nullrocks/identicon/_config.yml b/backend/vendor/github.com/nullrocks/identicon/_config.yml new file mode 100644 index 00000000..c4192631 --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/backend/vendor/github.com/nullrocks/identicon/canvas.go b/backend/vendor/github.com/nullrocks/identicon/canvas.go new file mode 100644 index 00000000..eddd3faa --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/canvas.go @@ -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 +} diff --git a/backend/vendor/github.com/nullrocks/identicon/config.yml b/backend/vendor/github.com/nullrocks/identicon/config.yml new file mode 100644 index 00000000..e79a1b85 --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/config.yml @@ -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 ./... \ No newline at end of file diff --git a/backend/vendor/github.com/nullrocks/identicon/generator.go b/backend/vendor/github.com/nullrocks/identicon/generator.go new file mode 100644 index 00000000..7b024a2b --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/generator.go @@ -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} +} diff --git a/backend/vendor/github.com/nullrocks/identicon/hsl.go b/backend/vendor/github.com/nullrocks/identicon/hsl.go new file mode 100644 index 00000000..b01d95c2 --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/hsl.go @@ -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 +} diff --git a/backend/vendor/github.com/nullrocks/identicon/identicon.go b/backend/vendor/github.com/nullrocks/identicon/identicon.go new file mode 100644 index 00000000..adcb118f --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/identicon.go @@ -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) + } +} diff --git a/backend/vendor/github.com/nullrocks/identicon/image.go b/backend/vendor/github.com/nullrocks/identicon/image.go new file mode 100644 index 00000000..c6d81a19 --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/image.go @@ -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) +} diff --git a/backend/vendor/github.com/nullrocks/identicon/svg.go b/backend/vendor/github.com/nullrocks/identicon/svg.go new file mode 100644 index 00000000..11fa130a --- /dev/null +++ b/backend/vendor/github.com/nullrocks/identicon/svg.go @@ -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 = ` + +` + +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) +}