379 lines
10 KiB
Go
379 lines
10 KiB
Go
// 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)
|
|
}
|
|
}
|