Simple obfuscated image generator with plugin design

Generates obfuscated image containing given word by using one of its registered plugins.

For it‘s operation it requires gd2 gem, available from gd2.rubyforge.org/.

Example of use:

  gem 'turing'
  require 'turing'
  ti = Turing::Image.new(:width => 280, :height => 115)
  ti.generate(File.join(Dir.getwd, 'a.jpg'), "randomword")

In this case we generate image using random plugin containing word "randomword". It is saved as `pwd`/a.jpg.

Example Rails controller (action):

  # Could be placed in config/environment.rb
  gem 'turing'
  require 'turing'

  # Could be part of app/controllers/site_controller.rb
  class SiteController < ApplicationController
      def image
          ti = ::Turing::Image.new(:width => 280, :height => 115)
          fn = get_tmpname
          ti.generate(fn, rand(1e8).to_s)
          send_file fn, :type => "image/jpeg", :disposition => "inline"
      end

      def get_tmpname
          pat = "tmpf-%s-%s-%s"
          fn = pat % [Process::pid, Time.now.to_f.to_s.tr(".",""), rand(1e8)]
          File.join(Dir::tmpdir, fn)
      end
      private :get_tmpname
  end

A word about plugins

All plugins are "registered" by subclassing Turing::Image (which is implemented using self.inherited). It makes sense to subclass Turing::Image because that way you‘ll also get goodies like write_string.

Plugins are auto-loaded by require from lib/turing/image_plugins after Turing::Image is created but you‘re free to manually load any plugin you like.

For inspiration on how to write new plugin visit any of the existing plugins in image_plugins dir, minimal template would be:

 class MyCoolPlugin < Turing::Image
     def initialize(opts = {})
         super(opts)
     end

     def generate(img, word)
         write_string(img, 'cour.ttf', GD2::Color[0, 0, 0], word, 48)
     end
 end
Methods
Classes and Modules
Module Turing::Image::SquaringHelper
Class Turing::Image::BlackSquaring
Class Turing::Image::Blending
Class Turing::Image::RandomNoise
Class Turing::Image::Spiral
Class Turing::Image::WhiteSquaring
Public Class methods
new(opts = {})

Configure instance using options hash.

Warning: Keys of this hash must be symbols.

Accepted options:

  • fontdir: Directory containing .ttf fonts required by plugins. Default: gem‘s shared/fonts directory.
  • bgdir: Directory containing .jpeg files used as background by plugins. Default: gem‘s shared/bgs directory.
  • outdir: Output directory where to put image in case absolute path wasn‘t specified.
  • width: Width of the image.
  • height: Height of the image.
  • method: Use specified plugin instead of randomly selected. You must give class that implements generate instance method. Default: nil.
     # File lib/turing/image.rb, line 89
 89:         def initialize(opts = {}) # {{{
 90:                 raise ArgumentError, "Opts must be hash!" unless opts.kind_of? Hash
 91:                 
 92:                 base = File.join(File.dirname(__FILE__), '..', '..', 'shared')
 93:                 @options = {
 94:                         :fontdir => File.join(base, 'fonts'),
 95:                         :bgdir => File.join(base, 'bgs'),
 96:                         :outdir => ENV["TMPDIR"] || '/tmp',
 97:                         :width => 280,
 98:                         :height => 115,
 99:                 }
100: 
101:                 @options.merge!(opts)
102:         end
Public Instance methods
generate(outname, word, method = nil)

Generate image into outname containing word (using method).

Warning: If you pass absolute filename as outname, outdir will have no effect.

Warning: There‘s no way to reset method to random if it was specified upon instance creation.

     # File lib/turing/image.rb, line 109
109:         def generate(outname, word, method = nil) # {{{
110:                 # select appropriate output plugin # {{{
111:                 m = method || @options[:method]
112:                 if m.nil?
113:                         if @@plugins.empty?
114:                                 raise RuntimeError, "no generators plugins available!"
115:                         end
116:                         m = @@plugins[rand(@@plugins.size)]
117:                 end
118:                 unless m.instance_methods.include?("generate")
119:                         raise ArgumentError, "plugin #{m} doesn't have generate method"
120:                 end
121:                 # }}}
122: 
123:                 # prepend outname with outdir, if no absolute path given
124:                 unless Pathname.new(outname).absolute?
125:                         outname = File.join(@options[:outdir], outname)
126:                 end
127:                 
128:                 img = GD2::Image.new(@options[:width], @options[:height])
129: 
130:                 img.draw do |canvas|
131:                         canvas.color = GD2::Color[255, 255, 255]
132:                         canvas.rectangle(0, 0, img.width - 1, img.height - 1, true)
133:                 end
134: 
135:                 m.new(@options).generate(img, word)
136:                 
137:                 img.draw do |canvas|
138:                         canvas.color = GD2::Color[0, 0, 0]
139:                         canvas.rectangle(0, 0, img.width - 1, img.height - 1)
140:                 end
141: 
142:                 begin
143:                         File.open(outname, 'w') { |f| f.write(img.jpeg(90)) }
144:                 rescue
145:                         raise "Unable to write challenge: #{$!}"
146:                 end
147: 
148:                 true
149:         end
Private Instance methods
write_string(img, font, fg, string, req_size = nil)

Write string to img using color fg and font (with size req_size, if possible) at random coordinates and using random angle.

Method checks bounding box so the string is guaranteed to stay within image‘s dimensions.

May raise RuntimeError if it‘s completely impossible to find suitable fontsize for given dimensions.

     # File lib/turing/image.rb, line 163
163:         def write_string(img, font, fg, string, req_size = nil) # {{{ # :doc:
164:                 # prepend fontname with fontdir, unless absolute path given
165:                 unless Pathname.new(font).absolute?
166:                         font = File.join(@options[:fontdir], font)
167:                 end
168:                 sizes = (16..42).to_a.reverse
169:                 turbulence = 5 # x%
170:                 mult = 0.85 * ((rand(turbulence*2 + 1) - turbulence) / 100.0 + 1.0)
171: 
172:                 # select angle
173:                 angle = -5 + rand(11)
174:                 
175:                 # font size determination ...
176:                 chosen = nil # {{{
177:                 sizes.unshift(req_size) unless req_size.nil?
178:                 sizes.each do |size|
179:                         bounds = nil
180:                         begin
181:                                 bounds = GD2::Font::TrueType.new(font, size).
182:                                         bounding_rectangle(string, angle.degrees)
183:                         rescue
184:                                 raise "Unable to detect bounding box: #{$!}"
185:                         end
186: 
187:                         minx, maxx = bounds.values.map { |x| x[0] }.sort.values_at(0, -1)
188:                         miny, maxy = bounds.values.map { |x| x[1] }.sort.values_at(0, -1)
189: 
190:                         bb_width = maxx - minx
191:                         bb_height = maxy - miny
192: 
193:                         if img.width * mult > bb_width && img.height * mult > bb_height
194:                                 chosen = {
195:                                         :size => size,
196:                                         :width => bb_width,
197:                                         :height => bb_height,
198:                                 }
199:                                 break
200:                         end
201:                 end # }}}
202: 
203:                 raise "Unable to select size" if chosen.nil?
204:                 
205:                 x_base, y_base = 1, img.height / 2
206: 
207:                 x_offset = rand(((img.width - chosen[:width])*mult).to_i)
208:                 y_offset = rand((((img.height - chosen[:height]) / 2)*mult).to_i)
209: 
210:                 x = x_base + x_offset
211:                 y = y_base + y_offset
212: 
213:                 img.draw do |canvas|
214:                         canvas.move_to(x, y)
215:                         canvas.color = fg
216:                         canvas.font = GD2::Font::TrueType.new(font, chosen[:size])
217:                         canvas.text(string, angle.degrees)
218:                 end
219: 
220:                 img
221:         end