turing 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # Turing -- Ruby implementation of Captcha
4
+ #
5
+ # Copyright © 2005 Michal Šafránek <wejn@box.cz>
6
+ #
7
+ # This file is part of http://turing.rubyforge.org/
8
+ #
9
+ # See turing.rb in lib/ directory for license terms.
10
+ #
11
+ require 'rubygems'
12
+ require_gem 'gd2'
13
+ require 'turing'
14
+ require 'pathname'
15
+
16
+ # == Simple obfuscated image generator with plugin design
17
+ #
18
+ # Generates obfuscated image containing given word by using
19
+ # one of its registered plugins.
20
+ #
21
+ # For it's operation it requires gd2 gem, available from http://gd2.rubyforge.org/.
22
+ #
23
+ # Example of use:
24
+ # ti = Turing::Image.new(:width => 280, :height => 115)
25
+ # ti.generate(File.join(Dir.getwd, 'a.jpg'), "randomword")
26
+ # In this case we generate image using random plugin containing
27
+ # word "randomword". It is saved as <tt>`pwd`/a.jpg</tt>.
28
+ #
29
+ # === A word about plugins
30
+ # All plugins are "registered" by subclassing Turing::Image (which is
31
+ # implemented using <tt>self.inherited</tt>). It makes sense to subclass
32
+ # Turing::Image because that way you'll also get goodies like #write_string.
33
+ #
34
+ # Plugins are auto-loaded by require from <tt>lib/turing/image_plugins</tt>
35
+ # after Turing::Image is created but you're free to manually load any plugin
36
+ # you like.
37
+ #
38
+ # For inspiration on how to write new plugin visit any of the existing
39
+ # plugins in +image_plugins+ dir, minimal template would be:
40
+ # class MyCoolPlugin < Turing::Image
41
+ # def initialize(opts = {})
42
+ # super(opts)
43
+ # end
44
+ #
45
+ # def generate(img, word)
46
+ # write_string(img, 'cour.ttf', GD2::Color[0, 0, 0], word, 48)
47
+ # end
48
+ # end
49
+ class Turing::Image # {{{
50
+ # All content generating plugins
51
+ @@plugins = []
52
+
53
+ # Configure instance using options hash.
54
+ #
55
+ # *Warning*: Keys of this hash must be symbols.
56
+ #
57
+ # Accepted options:
58
+ # * +fontdir+: Directory containing .ttf fonts required by plugins. Default: gem's <tt>shared/fonts</tt> directory.
59
+ # * +bgdir+: Directory containing .jpeg files used as background by plugins. Default: gem's <tt>shared/bgs</tt> directory.
60
+ # * +outdir+: Output directory where to put image in case absolute path wasn't specified.
61
+ # * +width+: Width of the image.
62
+ # * +height+: Height of the image.
63
+ # * +method+: Use specified plugin instead of randomly selected. You must give class that implements +generate+ instance method. Default: nil.
64
+ def initialize(opts = {}) # {{{
65
+ raise ArgumentError, "Opts must be hash!" unless opts.kind_of? Hash
66
+
67
+ base = File.join(File.dirname(__FILE__), '..', '..', 'shared')
68
+ @options = {
69
+ :fontdir => File.join(base, 'fonts'),
70
+ :bgdir => File.join(base, 'bgs'),
71
+ :outdir => ENV["TMPDIR"] || '/tmp',
72
+ :width => 280,
73
+ :height => 115,
74
+ }
75
+
76
+ @options.merge!(opts)
77
+ end # }}}
78
+
79
+ # Generate image into +outname+ containing +word+ (using +method+).
80
+ #
81
+ # *Warning*: If you pass absolute filename as +outname+, +outdir+ will have no effect.
82
+ #
83
+ # *Warning*: There's no way to reset +method+ to random if it was specified upon instance creation.
84
+ def generate(outname, word, method = nil) # {{{
85
+ # select appropriate output plugin # {{{
86
+ m = method || @options[:method]
87
+ if m.nil?
88
+ if @@plugins.empty?
89
+ raise RuntimeError, "no generators plugins available!"
90
+ end
91
+ m = @@plugins[rand(@@plugins.size)]
92
+ end
93
+ unless m.instance_methods.include?("generate")
94
+ raise ArgumentError, "plugin #{m} doesn't have generate method"
95
+ end
96
+ # }}}
97
+
98
+ # prepend outname with outdir, if no absolute path given
99
+ unless Pathname.new(outname).absolute?
100
+ outname = File.join(@options[:outdir], outname)
101
+ end
102
+
103
+ img = GD2::Image.new(@options[:width], @options[:height])
104
+
105
+ img.draw do |canvas|
106
+ canvas.color = GD2::Color[255, 255, 255]
107
+ canvas.rectangle(0, 0, img.width - 1, img.height - 1, true)
108
+ end
109
+
110
+ m.new(@options).generate(img, word)
111
+
112
+ img.draw do |canvas|
113
+ canvas.color = GD2::Color[0, 0, 0]
114
+ canvas.rectangle(0, 0, img.width - 1, img.height - 1)
115
+ end
116
+
117
+ begin
118
+ File.open(outname, 'w') { |f| f.write(img.jpeg(90)) }
119
+ rescue
120
+ raise "Unable to write challenge: #{$!}"
121
+ end
122
+
123
+ true
124
+ end # }}}
125
+
126
+ # Private methods
127
+ private
128
+
129
+ # Write +string+ to +img+ using color +fg+ and +font+
130
+ # (with size +req_size+, if possible) at random coordinates
131
+ # and using random angle.
132
+ #
133
+ # Method checks bounding box so the string is guaranteed to stay
134
+ # within image's dimensions.
135
+ #
136
+ # May raise RuntimeError if it's completely impossible to find suitable
137
+ # fontsize for given dimensions.
138
+ def write_string(img, font, fg, string, req_size = nil) # {{{ # :doc:
139
+ # prepend fontname with fontdir, unless absolute path given
140
+ unless Pathname.new(font).absolute?
141
+ font = File.join(@options[:fontdir], font)
142
+ end
143
+ sizes = (16..42).to_a.reverse
144
+ turbulence = 5 # x%
145
+ mult = 0.85 * ((rand(turbulence*2 + 1) - turbulence) / 100.0 + 1.0)
146
+
147
+ # select angle
148
+ angle = -5 + rand(11)
149
+
150
+ # font size determination ...
151
+ chosen = nil # {{{
152
+ sizes.unshift(req_size) unless req_size.nil?
153
+ sizes.each do |size|
154
+ bounds = nil
155
+ begin
156
+ bounds = GD2::Font::TrueType.new(font, size).
157
+ bounding_rectangle(string, angle.degrees)
158
+ rescue
159
+ raise "Unable to detect bounding box: #{$!}"
160
+ end
161
+
162
+ minx, maxx = bounds.values.map { |x| x[0] }.sort.values_at(0, -1)
163
+ miny, maxy = bounds.values.map { |x| x[1] }.sort.values_at(0, -1)
164
+
165
+ bb_width = maxx - minx
166
+ bb_height = maxy - miny
167
+
168
+ if img.width * mult > bb_width && img.height * mult > bb_height
169
+ chosen = {
170
+ :size => size,
171
+ :width => bb_width,
172
+ :height => bb_height,
173
+ }
174
+ break
175
+ end
176
+ end # }}}
177
+
178
+ raise "Unable to select size" if chosen.nil?
179
+
180
+ x_base, y_base = 1, img.height / 2
181
+
182
+ x_offset = rand(((img.width - chosen[:width])*mult).to_i)
183
+ y_offset = rand((((img.height - chosen[:height]) / 2)*mult).to_i)
184
+
185
+ x = x_base + x_offset
186
+ y = y_base + y_offset
187
+
188
+ img.draw do |canvas|
189
+ canvas.move_to(x, y)
190
+ canvas.color = fg
191
+ canvas.font = GD2::Font::TrueType.new(font, chosen[:size])
192
+ canvas.text(string, angle.degrees)
193
+ end
194
+
195
+ img
196
+ end # }}}
197
+
198
+ # Record all plugins available
199
+ # (subclasses which implement class method generate)
200
+ def self.inherited(other) # {{{
201
+ (@@plugins ||= []) << other
202
+ end # }}}
203
+ end # }}}
204
+
205
+ # Load all image plugins ...
206
+ # {{{
207
+ plugin_dir = File.join(File.dirname(__FILE__), 'image_plugins')
208
+ if FileTest.directory?(plugin_dir)
209
+ Dir[File.join(plugin_dir, '*')].each do |f|
210
+ bn = File.basename(f)
211
+ begin
212
+ require 'turing/image_plugins/' + bn
213
+ rescue Object
214
+ raise RuntimeError, "Exception while loading plugin: #{bn}: #{$!}"
215
+ end
216
+ end
217
+ end
218
+ # }}}
219
+
220
+ # Test it out, when executed directly
221
+ if __FILE__ == $0 # {{{
222
+ ti = Turing::Image.new(:width => 280, :height => 115)
223
+
224
+ # debug loop
225
+ begin # {{{
226
+ loop do
227
+ dict_fn = File.join(File.dirname(__FILE__), '..', '..', 'shared',
228
+ 'dictionary')
229
+ dict = File.open(dict_fn).readlines.map! { |x| x.chomp }
230
+ ti.generate(File.join(Dir.getwd, 'a.jpg'),
231
+ dict[rand(dict.size)])
232
+ pid = fork do
233
+ exec("xv", "a.jpg")
234
+ end
235
+ Process.detach(pid)
236
+ sleep 2
237
+ Process.kill(9, pid)
238
+ end
239
+ rescue Interrupt
240
+ ensure
241
+ File.unlink('a.jpg') rescue nil
242
+ end # }}}
243
+ end # }}}
244
+
245
+ # vim: set ts=4 sw=4 :
@@ -0,0 +1,66 @@
1
+ #
2
+ # Turing -- Ruby implementation of Captcha
3
+ #
4
+ # Copyright © 2005 Michal Šafránek <wejn@box.cz>
5
+ #
6
+ # This file is part of http://turing.rubyforge.org/
7
+ #
8
+ # See turing.rb in lib/ directory for license terms.
9
+ #
10
+
11
+ # == Squaring helper
12
+ # Skeleton for Turing::Image::*Squaring (mixin, actually).
13
+ module Turing::Image::SquaringHelper # {{{
14
+ # contract method - generate the challenge
15
+ def generate(img, word, bg = nil) # {{{
16
+ if bg.nil?
17
+ possible = Dir[File.join(@options[:bgdir], '*')]
18
+ bg = possible[rand(possible.size)]
19
+ else
20
+ unless FileTest.exists?(bg)
21
+ raise ArgumentError, "Wrong background!"
22
+ end
23
+ end
24
+
25
+ img_tmp = GD2::Image.load(File.open(bg, 'r'))
26
+
27
+ if img_tmp.width < img.width || img_tmp.height < img.height
28
+ raise "Background has insufficient dimensions"
29
+ end
30
+
31
+ img.merge_from(img_tmp, 0, 0, 0, 0, img.width, img.height, 30.percent)
32
+
33
+ # XXX: is this equivalent to "img_tmp.destroy" ?
34
+ img_tmp = nil
35
+
36
+ fg = GD2::Color[0, 0, 0]
37
+
38
+ write_string(img, 'cour.ttf', fg, word, 35)
39
+
40
+ raise RuntimeError, "no squaring color selected!" if @squaring_color.nil?
41
+ fg = @squaring_color
42
+
43
+ img.draw do |canvas|
44
+ canvas.color = fg
45
+ if rand(2).zero?
46
+ delta = word.size > 8 ? 6 : 4
47
+ 0.step(img.width, delta) { |x| canvas.line(x, 0, x, img.height) }
48
+ 0.step(img.height, delta) { |y| canvas.line(0, y, img.width, y) }
49
+ else
50
+ i = 0
51
+ while i <= img.width
52
+ canvas.line(i, 0, i, img.height)
53
+ i += 3 + rand(4)
54
+ end
55
+
56
+ i = 0
57
+ while i <= img.height
58
+ canvas.line(0, i, img.width, i)
59
+ i += 3 + rand(4)
60
+ end
61
+ end
62
+ end
63
+ end # }}}
64
+ end # }}}
65
+
66
+ # vim: set ts=4 sw=4 :
@@ -0,0 +1,23 @@
1
+ #
2
+ # Turing -- Ruby implementation of Captcha
3
+ #
4
+ # Copyright © 2005 Michal Šafránek <wejn@box.cz>
5
+ #
6
+ # This file is part of http://turing.rubyforge.org/
7
+ #
8
+ # See turing.rb in lib/ directory for license terms.
9
+ #
10
+ require 'turing/image_plugins/__squaring_helper'
11
+
12
+ # == BlackSquaring Turing test
13
+ # Blends text, background and makes square pattern over image.
14
+ class Turing::Image::BlackSquaring < Turing::Image # {{{
15
+ def initialize(opts = {})
16
+ super(opts)
17
+ @squaring_color = GD2::Color[0, 0, 0]
18
+ end
19
+
20
+ include Turing::Image::SquaringHelper
21
+ end # }}}
22
+
23
+ # vim: set ts=4 sw=4 :
@@ -0,0 +1,44 @@
1
+ #
2
+ # Turing -- Ruby implementation of Captcha
3
+ #
4
+ # Copyright © 2005 Michal Šafránek <wejn@box.cz>
5
+ #
6
+ # This file is part of http://turing.rubyforge.org/
7
+ #
8
+ # See turing.rb in lib/ directory for license terms.
9
+ #
10
+
11
+ # == Blending Turing test
12
+ # Blends text with background (highly imperfect for some backgrounds).
13
+ class Turing::Image::Blending < Turing::Image # {{{
14
+ # contract method - generate the challenge
15
+ def generate(img, word, bg = nil) # {{{
16
+ if bg.nil?
17
+ possible = Dir[File.join(@options[:bgdir], '*')]
18
+ bg = possible[rand(possible.size)]
19
+ else
20
+ unless FileTest.exists?(bg)
21
+ raise ArgumentError, "Wrong background!"
22
+ end
23
+ end
24
+
25
+ img_tmp = GD2::Image.load(File.open(bg, 'r'))
26
+
27
+ if img_tmp.width < img.width || img_tmp.height < img.height
28
+ raise "Background has insufficient dimensions"
29
+ end
30
+
31
+ img.copy_from(img_tmp, 0, 0, 0, 0, img.width, img.height)
32
+
33
+ # XXX: equivalent of img_tmp.destroy ?
34
+ img_tmp = nil
35
+
36
+ r = rand(32)
37
+ fg = GD2::Color[r, r, r, 40.percent]
38
+
39
+ write_string(img, 'georgiai.ttf', fg, word, 40)
40
+
41
+ end # }}}
42
+ end # }}}
43
+
44
+ # vim: set ts=4 sw=4 :
@@ -0,0 +1,32 @@
1
+ #
2
+ # Turing -- Ruby implementation of Captcha
3
+ #
4
+ # Copyright © 2005 Michal Šafránek <wejn@box.cz>
5
+ #
6
+ # This file is part of http://turing.rubyforge.org/
7
+ #
8
+ # See turing.rb in lib/ directory for license terms.
9
+ #
10
+
11
+ # == Random Noise turing test
12
+ # Generates random black noise, then text, then random white noise.
13
+ class Turing::Image::RandomNoise < Turing::Image # {{{
14
+ # contract method - generate the challenge
15
+ def generate(img, word, bg = nil) # {{{
16
+ fg = GD2::Color[255, 255, 255]
17
+ bg = GD2::Color[0, 0, 0]
18
+
19
+ 0.upto(img.width * img.height * 0.4) do |i|
20
+ img[rand(img.width), rand(img.height)] = bg
21
+ end
22
+
23
+ write_string(img, 'georgiai.ttf', bg, word, 48)
24
+
25
+ quant = word.size > 7 ? 0.1 : 0.2
26
+ 0.upto(img.width * img.height * quant) do |i|
27
+ img[rand(img.width), rand(img.height)] = fg
28
+ end
29
+ end # }}}
30
+ end # }}}
31
+
32
+ # vim: set ts=4 sw=4 :
@@ -0,0 +1,56 @@
1
+ #
2
+ # Turing -- Ruby implementation of Captcha
3
+ #
4
+ # Copyright © 2005 Michal Šafránek <wejn@box.cz>
5
+ #
6
+ # This file is part of http://turing.rubyforge.org/
7
+ # and is based on PHP captcha implementation "Session captcha"
8
+ # which can be found at http://sourceforge.net/projects/session-captcha/
9
+ #
10
+ # See turing.rb in lib/ directory for license terms.
11
+ #
12
+
13
+ # == Spiral Turing test
14
+ # Renders spirals plus text over them.
15
+ class Turing::Image::Spiral < Turing::Image # {{{
16
+ # contract method - generate the challenge
17
+ def generate(img, word) # {{{
18
+ fg = GD2::Color[185, 140, 140]
19
+ sfg = GD2::Color[225, 190, 190]
20
+
21
+ spiral(img, (img.width/3) + rand(img.width/3), rand(img.height), sfg)
22
+
23
+ write_string(img, 'cour.ttf', fg, word)
24
+
25
+ spiral(img, rand(img.width / 3), rand(img.height), sfg)
26
+ spiral(img, 2*(img.width/3) + rand(img.width/3), rand(img.height), sfg)
27
+ end # }}}
28
+
29
+ private
30
+
31
+ # renders spiral with center at [originx, originy] using color
32
+ def spiral(img, originx, originy, color) # {{{
33
+ theta = 1.0
34
+ thetac = 6.0
35
+ radius = 15.0
36
+ circles = 10.0
37
+ points = 35.0
38
+ img.draw do |canvas|
39
+ canvas.color = color
40
+ 0.upto(circles * points - 1) do |i|
41
+ theta += thetac
42
+ rad = radius * (i.to_f / points)
43
+ x = rad * Math.cos(theta) + originx
44
+ y = rad * Math.sin(theta) + originy
45
+ theta += thetac
46
+ rad1 = radius * ((i.to_f + 1.to_f) / points)
47
+ x1 = rad1 * Math.cos(theta) + originx
48
+ y1 = rad1 * Math.sin(theta) + originy
49
+ canvas.line(x.round, y.round, x1.round, y1.round)
50
+ theta -= thetac
51
+ end
52
+ end
53
+ end # }}}
54
+ end # }}}
55
+
56
+ # vim: set ts=4 sw=4 :