-
Notifications
You must be signed in to change notification settings - Fork 13
/
identicon.go
378 lines (320 loc) · 10.2 KB
/
identicon.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
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)
}
}