turing 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 :