caleidenticon 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +54 -0
- data/lib/caleidenticon.rb +196 -0
- metadata +77 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 75df1085befc50d701892758bee9b560c178d9a6
|
4
|
+
data.tar.gz: af37991c60830a9a4ae6e4b8be6a3da0fcbf3bba
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 96438c0b5eb76b2d7f63c9f0fd5229fd7363db8073080a2d211dd9261ee02e52544a77e8639f6190958d867bacd9c4d0740430244194fff23fba471f7e5c65e0
|
7
|
+
data.tar.gz: 32deffeef8fcf18e777f93aa5783c0ad9889a48fd81fe9cfaf5d02acb1f383f608a89a1035bec55fa29566317958c394574b42f40b4214c0f5ce568cdbf0284b
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
# Caleidenticon
|
3
|
+
|
4
|
+
![Example Identicons 1](https://dl.dropboxusercontent.com/s/8t5oww83d5vcagw/identicons.png)
|
5
|
+
|
6
|
+
Caleidenticon creates caleidoscope-like [identicons](https://en.wikipedia.org/wiki/Identicon).
|
7
|
+
|
8
|
+
It is based on [RubyIdenticon](https://github.com/chrisbranson/ruby_identicon) by Chris Branson which in turn was based on [go-identicon](https://github.com/dgryski/go-identicon) by Damian Gryski.
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
require 'caleidenticon'
|
13
|
+
|
14
|
+
Create an identicon png and save it to a path:
|
15
|
+
|
16
|
+
Caleidenticon.create_and_save(user.email, user.identicon_path, {salt: my_salt})
|
17
|
+
|
18
|
+
create_and_save takes 3 parameters:
|
19
|
+
|
20
|
+
input # a string on which to base the identicon (usually an email or username)
|
21
|
+
save_path # the storage path for the resulting identicon png file
|
22
|
+
options # a Hash (not required)
|
23
|
+
|
24
|
+
## Options
|
25
|
+
|
26
|
+
complexity: # Integer (2..n) number of elements per image. affects image size.
|
27
|
+
scale: # Integer (1..n) resolution of each element. affects image size.
|
28
|
+
density: # Integer (2..10) how densely the image is covered with elements
|
29
|
+
spikiness: # Integer (1..n) higher values produce a more pointy overall shape
|
30
|
+
corner_sprinkle: # Integer (0..n) decorates bare corners if spikiness is > 0
|
31
|
+
colors: # Array of 4 arrays with 3 Integers (0..255) each.
|
32
|
+
salt: # String of 21 alphanumeric chars, used for hashing the input.
|
33
|
+
debug: # Bool - if true, the gem will print a lot of debug information.
|
34
|
+
|
35
|
+
Using your own salt is optional, but it is recommended, as it ensures that the mapping from input to identicon will be unique to your application.
|
36
|
+
|
37
|
+
With the colors: option you can make the identicons fit in more closely with the color scheme of your app. E.g. if your app has bright green and blue colors you could do something like:
|
38
|
+
|
39
|
+
options = {salt: my_salt, colors: [[80,255,100], [80,100,255], [0,200,255], [0,255,200]]}
|
40
|
+
Caleidenticon.create_and_save(user.email, user.identicon_path, options)
|
41
|
+
|
42
|
+
Which will produce Identicons like these:
|
43
|
+
|
44
|
+
![Example Identicons 2](https://dl.dropboxusercontent.com/s/cvjlprdev4ibt0f/identicon_bluegreen.png)
|
45
|
+
|
46
|
+
Higher values for complexity, density and scale make the generation more expensive, but the results look better at a large scale:
|
47
|
+
|
48
|
+
More complex and dense →
|
49
|
+
|
50
|
+
![Example Identicons 3](https://dl.dropboxusercontent.com/s/zupywnv0lhst3nz/identicon_options.png)
|
51
|
+
|
52
|
+
To test your settings, create a large number of identicons by running:
|
53
|
+
|
54
|
+
Caleidenticon.run_test(my_output_dir, number_of_identicons, my_options)
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Caleidenticon
|
2
|
+
|
3
|
+
['bcrypt', 'oily_png'].each {|gem| require gem}
|
4
|
+
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
complexity: 6, # (2..n) number of elements per image. affects image size.
|
7
|
+
scale: 10, # (1..n) resolution of each element. affects image size.
|
8
|
+
density: 6, # (2..10) how densely the image is covered with elements
|
9
|
+
spikiness: 2, # (1..n) higher values produce a more pointy overall shape
|
10
|
+
corner_sprinkle: 4, # (0..n) decorates bare corners if spikiness is > 0
|
11
|
+
colors: [ [255,10,125], [255,50,10], [15,50,255], [140,255,10] ],
|
12
|
+
salt: "KGvNwCoZtioSTC07piREn",
|
13
|
+
debug: false
|
14
|
+
}
|
15
|
+
|
16
|
+
BCRYPT_SALT_HEAD = '$2a$10$'
|
17
|
+
|
18
|
+
def self.create_and_save(input, save_path, options = {})
|
19
|
+
blob = create_blob(input, options)
|
20
|
+
if blob.nil?
|
21
|
+
debug('blob creation failed')
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
File.open(save_path, 'wb') {|f| f.write(blob)}
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.create_blob(input, options)
|
28
|
+
options = DEFAULT_OPTIONS.merge(options)
|
29
|
+
|
30
|
+
full_salt = "#{BCRYPT_SALT_HEAD}#{options[:salt]}."
|
31
|
+
|
32
|
+
# ensure workable settings
|
33
|
+
options[:complexity] = [options[:complexity], 2].max
|
34
|
+
options[:scale] = [options[:scale], 1].max
|
35
|
+
options[:density] = [[options[:density], 2].max, 10].min
|
36
|
+
options[:spikiness] = [options[:spikiness], 1].max
|
37
|
+
|
38
|
+
raise ArgumentError.new('input cannot be empty') if input.nil? || input == ''
|
39
|
+
raise ArgumentError.new('salt must be a string of 21 ASCII chars') unless BCrypt::Engine.valid_salt?(full_salt)
|
40
|
+
raise ArgumentError.new('invalid colors') unless options[:colors].map{|array| array.size} == [3,3,3,3] &&
|
41
|
+
options[:colors].flatten.all?{|val| (0..255).include?(val)}
|
42
|
+
|
43
|
+
@debug = options[:debug]
|
44
|
+
debug("creating blob for input '#{input}'")
|
45
|
+
|
46
|
+
hash = BCrypt::Engine.hash_secret(input, full_salt)
|
47
|
+
hash.slice!(0, BCRYPT_SALT_HEAD.length) # else the first few hash bits would be the same for all inputs
|
48
|
+
hash = (hash * options[:complexity]).bytes.inject {|a, b| (a << 8) + b}
|
49
|
+
|
50
|
+
debug("hash bits: #{hash.to_s(2).length}")
|
51
|
+
|
52
|
+
# use the first few bytes of the hash to define a tint color for the identicon
|
53
|
+
tint_rgb = Array.new(3) {hash >>= 8; hash >> 8 & 0xff}
|
54
|
+
|
55
|
+
debug("tint color: #{tint_rgb.inspect}")
|
56
|
+
|
57
|
+
colors = options[:colors].map do |option_rgb|
|
58
|
+
# use a saturated version of each color from options that is only lightly influenced by the input
|
59
|
+
[ChunkyPNG::Color.rgba(
|
60
|
+
(option_rgb[0] + tint_rgb[0]) / 2, # r
|
61
|
+
(option_rgb[1] + tint_rgb[1]) / 2, # g
|
62
|
+
(option_rgb[2] + tint_rgb[2]) / 2, # b
|
63
|
+
0xff), # alpha
|
64
|
+
# ... and a darker version that is more strongly influenced by the input
|
65
|
+
ChunkyPNG::Color.rgba(
|
66
|
+
(option_rgb[0] * 2 + tint_rgb[0] * 5) / 9, # r
|
67
|
+
(option_rgb[1] * 2 + tint_rgb[1] * 5) / 9, # g
|
68
|
+
(option_rgb[2] * 2 + tint_rgb[2] * 5) / 9, # b
|
69
|
+
0xff)] # alpha
|
70
|
+
end.flatten
|
71
|
+
|
72
|
+
# creat color shifts and size multiplicators from the next bytes,
|
73
|
+
# so that colors dont have to appear in the same array order for all users,
|
74
|
+
# and so that some elements are larger (in an input-specific order)
|
75
|
+
color_shifts = Array.new(10) {hash >>= 3; hash >> 3 & 7} # 0-7
|
76
|
+
size_multiplicators = Array.new(10) {hash >>= 2; [(hash >> 2 & 3), 1].max} # 1-3
|
77
|
+
|
78
|
+
debug("shifts: #{color_shifts.inspect} mults: #{size_multiplicators.inspect}")
|
79
|
+
|
80
|
+
# -------
|
81
|
+
# create the actual graphics
|
82
|
+
# -------
|
83
|
+
|
84
|
+
grid_size = options[:complexity] * 2 + 1
|
85
|
+
unused_cells = [((options[:complexity] * options[:spikiness])/4), (grid_size - 2)].min
|
86
|
+
circle_radius = options[:scale]/2
|
87
|
+
margin = circle_radius * 3 # circles can have up to 3x their default radius
|
88
|
+
image_size = (grid_size * options[:scale] + margin) * 2
|
89
|
+
image = ChunkyPNG::Image.new(image_size, image_size, ChunkyPNG::Color::TRANSPARENT)
|
90
|
+
|
91
|
+
# use up to (options[:corner_sprinkle] ** 2) cells in the
|
92
|
+
# outer corner of a quadrant to add a bit of "sprinkle"
|
93
|
+
sprinkle_inset = options[:complexity]/2 + 1
|
94
|
+
usable_space = [unused_cells - sprinkle_inset, 0].max
|
95
|
+
space_to_use = [usable_space, options[:corner_sprinkle]].min
|
96
|
+
sprinkle_range = space_to_use > 0 ? sprinkle_inset..(sprinkle_inset + space_to_use) : []
|
97
|
+
|
98
|
+
debug("grid size: #{grid_size} non-main: #{unused_cells} sprinkled: #{space_to_use}")
|
99
|
+
|
100
|
+
element_scarcity = 10 - options[:density]
|
101
|
+
rectangle_amount = (hash >> 1 & 1) + 1
|
102
|
+
rect_matcher = rectangle_amount == 1 ? 1 : 3
|
103
|
+
|
104
|
+
column, row = 0, 0
|
105
|
+
rectangles_per_row = {}
|
106
|
+
|
107
|
+
# fill the single grid cells
|
108
|
+
(grid_size ** 2).times do |i|
|
109
|
+
|
110
|
+
if column > grid_size # a row was just completed, proceed with first column of next row
|
111
|
+
row += 1
|
112
|
+
column = 0
|
113
|
+
end
|
114
|
+
|
115
|
+
# skip this cell if we are not in the primary cells or in a sprinkleable corner
|
116
|
+
in_sprinkle_range = sprinkle_range.include?(row) && sprinkle_range.include?(column)
|
117
|
+
in_primary_cells = row >= unused_cells
|
118
|
+
unless in_primary_cells || in_sprinkle_range
|
119
|
+
column += 1
|
120
|
+
next
|
121
|
+
end
|
122
|
+
|
123
|
+
hash >>= element_scarcity
|
124
|
+
if (hash >> element_scarcity & element_scarcity) == element_scarcity
|
125
|
+
|
126
|
+
x = (column + 1) * circle_radius * 2 + margin
|
127
|
+
y = (row + 1) * circle_radius * 2 + margin
|
128
|
+
|
129
|
+
# draw the elements mirrored within the quadrant, and all quadrants point-symmetric to the center.
|
130
|
+
# despite drawing each element eight times, this is actually much less expensive than drawing once
|
131
|
+
# and then rotating, flipping and merging the result!
|
132
|
+
vectors = [[x, y], [y, x], # top left quadrant
|
133
|
+
[image_size-x, y], [image_size-y, x], # top right quadrant
|
134
|
+
[x, image_size-y], [y, image_size-x], # bottom left quadrant
|
135
|
+
[image_size-x, image_size-y], [image_size-y, image_size-x]] # bottom right quadrant
|
136
|
+
|
137
|
+
# choose element color and size by using color shift and size at index 0-9
|
138
|
+
color = colors[color_shifts[i%10]]
|
139
|
+
element_size = circle_radius * size_multiplicators[i%10]
|
140
|
+
|
141
|
+
rectangles_per_row[row] = 0 unless rectangles_per_row.has_key?(row)
|
142
|
+
|
143
|
+
# create lengthy rectangles for a bit more than half of all cells
|
144
|
+
hash >>= rectangle_amount
|
145
|
+
if ((hash >> rectangle_amount & rect_matcher) != rect_matcher || rectangles_per_row[row] == 2)
|
146
|
+
rectangles_per_row[row] += 1
|
147
|
+
rect_width, rect_length = element_size, element_size/4
|
148
|
+
rect_width, rect_length = rect_length, rect_width if hash >> 1 & 1 == 1 # rotate half of all rects
|
149
|
+
hash >>= 1
|
150
|
+
vectors.each_with_index do |v, idx|
|
151
|
+
x_size, y_size = rect_width, rect_length
|
152
|
+
# rects are lengthy, so their 2nd drawing in each quadrant must be rotated by 90°
|
153
|
+
x_size, y_size = y_size, x_size if idx % 2 != 0
|
154
|
+
image.rect(v[0]-x_size, v[1]-y_size, v[0]+x_size, v[1]+y_size, color, color)
|
155
|
+
end
|
156
|
+
type = 'rect'
|
157
|
+
# create circles for other cells
|
158
|
+
else
|
159
|
+
vectors.each {|v| image.circle(v[0], v[1], element_size, color, color)}
|
160
|
+
type = 'circle'
|
161
|
+
end
|
162
|
+
debug("cell #{i} color: #{color}, size: #{size_multiplicators[i%10]}, type: #{type}")
|
163
|
+
end
|
164
|
+
column += 1
|
165
|
+
end
|
166
|
+
|
167
|
+
image.to_blob color_mode: ChunkyPNG::COLOR_INDEXED
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.run_test(output_dir, iterations = 20, options = {})
|
171
|
+
options[:debug] = true
|
172
|
+
start = Time.now.to_i
|
173
|
+
|
174
|
+
if output_dir && File.directory?(output_dir)
|
175
|
+
output_dir = File.join(output_dir, "caleidenticon_test_#{start}")
|
176
|
+
Dir.mkdir(output_dir)
|
177
|
+
else
|
178
|
+
raise ArgumentError.new('no output_dir')
|
179
|
+
end
|
180
|
+
|
181
|
+
iterations.times do
|
182
|
+
random_string = (0...8).map{(65 + rand(26)).chr}.join
|
183
|
+
save_path = File.join(output_dir, "#{random_string}.png")
|
184
|
+
self.create_and_save(random_string, save_path, options)
|
185
|
+
end
|
186
|
+
|
187
|
+
debug("creating #{iterations} pngs took #{Time.now.to_i - start} seconds")
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def self.debug(string)
|
193
|
+
puts string if @debug
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: caleidenticon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.8.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Janosch Müller
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bcrypt
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: oily_png
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.1'
|
41
|
+
description: Caleidenticon is a customizable generator for caleidoscope-like identicons.
|
42
|
+
email: janosch84@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files:
|
46
|
+
- README.md
|
47
|
+
- LICENSE
|
48
|
+
files:
|
49
|
+
- LICENSE
|
50
|
+
- README.md
|
51
|
+
- lib/caleidenticon.rb
|
52
|
+
homepage: https://github.com/janosch-x/caleidenticon
|
53
|
+
licenses:
|
54
|
+
- MIT
|
55
|
+
metadata: {}
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options:
|
58
|
+
- "--charset=UTF-8"
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 2.0.0
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
requirements: []
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 2.2.2
|
74
|
+
signing_key:
|
75
|
+
specification_version: 2
|
76
|
+
summary: Creates caleidoscopic identicons.
|
77
|
+
test_files: []
|