caleidenticon 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +54 -0
  4. data/lib/caleidenticon.rb +196 -0
  5. 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: []