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
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.
[ show source ]
# 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
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.
[ show source ]
# 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
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.
[ show source ]
# 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