module ActionController
module Macros
module ImageMagick
DEFAULT_OPTIONS = { :action_name=>:imagemagick,
:cache=>nil,
:prerender=>false,
:max_recipe_level=>:builtin,
:commands_param=>:commands }
def self.append_features(base)
super
base.extend(ClassMethods)
end
module ClassMethods
def imagemagick_for_directory(image_path, options = {})
imagemagick_for(image_path.to_s, options)
end
def imagemagick_filter_for(action_name, options = {})
imagemagick_for(action_name.to_sym, options)
end
def imagemagick_for(image_source, options = {})
require 'RMagick'
configuration = Hash.new.merge!(ActionController::Macros::ImageMagick::DEFAULT_OPTIONS)
configuration.update(options)
if defined?(RAILS_ROOT)
configuration[:cache] = File.expand_path(configuration[:cache], RAILS_ROOT) unless configuration[:cache].nil?
end
configuration[:max_recipe_level] = configuration[:max_recipe_level].to_sym
configuration[:max_recipe_level] = :builtin unless [:builtin, :global, :local].member?(configuration[:max_recipe_level])
configuration[:local_recipes] = RecipeSet.new
if image_source.is_a?(String)
configuration[:image_path] = image_source
configuration[:image_path] = image_source.sub(/\/$/, '') if image_source =~ /\/$/
configuration[:type] = :action
if defined?(RAILS_ROOT)
configuration[:image_path] = File.expand_path(configuration[:image_path], RAILS_ROOT)
end
configuration[:action_name] = :imagemagick unless configuration.has_key?(:action_name)
configuration[:action_name] = configuration[:action_name].to_sym
else
configuration[:type] = :filter
configuration[:for_action] = image_source.to_sym
end
@imagemagick_macro_helper = ImageMagickMacroHelper.new(configuration)
define_method(:imagemagick_macro_helper) do
return self.class.instance_variable_get(:@imagemagick_macro_helper)
end
protected :imagemagick_macro_helper
define_method(:imagemagick_local_recipes) do
return imagemagick_macro_helper.local_recipes
end
protected :imagemagick_local_recipes
define_method(:url_for_imagemagick) do |filename_or_params, p_commands|
imagemagick_macro_helper.controller = self
url_for(imagemagick_macro_helper.url_options_for_imagemagick(filename_or_params, p_commands))
end
if configuration[:type]==:action
implement_as_action(configuration)
elsif configuration[:type]==:filter
implement_as_filter(configuration)
end
end
def imagemagick_recipe(name, recipe, version = nil)
if @imagemagick_macro_helper.nil?
raise(ActionControllerError, "Use imagemagick_for before you use imagemagick_recipe.")
end
@imagemagick_macro_helper.add_recipe(name, recipe, version)
end
private
def implement_as_action(configuration)
define_method(configuration[:action_name]) do
begin
imagemagick_macro_helper.controller = self
identification = @params["id"].to_s
commands = @params["#{configuration[:commands_param]}"]
if imagemagick_macro_helper.prerender? && !imagemagick_macro_helper.cached?(identification, commands)
render :text=>"Image not found.", :status=>404
end
result = imagemagick_macro_helper.render(identification, identification, commands)
if result.nil?
render :text=>"Image not found.", :status=>404
else
@response.headers["Content-type"] = result.mime_type
render :text=>result.to_blob
end
rescue Exception=>e
render :text=>"Image could not be processed. "+e.to_s, :status=>500
end
end
end
def implement_as_filter(configuration)
define_method(:imagemagick_after_filter) do
image = Magick::Image.from_blob(@response.body)
if !image.empty?
imagemagick_macro_helper.controller = self
identification = MD5.md5(@response.body).to_s
commands = @params["#{configuration[:commands_param]}"]
result = imagemagick_macro_helper.render(image.first, identification, commands)
begin
if result.nil?
@response.headers["Status"] = 404
@response.body = "Image not found."
else
@response.headers["Content-type"] = result.mime_type
@response.body = result.to_blob
end
rescue Exception=>e
@response.headers["Status"] = 500
@response.body = "Image could not be processed. "+e.to_s
end
end
end
module_eval do
after_filter :imagemagick_after_filter, :only=>"#{configuration[:for_action]}"
end
end
end
class ImageMagickMacroHelper
def initialize(configuration)
@configuration = configuration
end
def caching?
return !@configuration[:cache].nil?
end
def prerender?
return @configuration[:type]==:action && caching? && @configuration[:prerender]
end
def controller=(controller)
@configuration[:controller] = controller
end
def add_recipe(name, action, version = nil)
@configuration[:local_recipes].add(name, action, version)
end
def local_recipes
@configuration[:local_recipes]
end
def valid_filename?(filename)
pathname = expand_filename(filename)
if pathname =~ /^
return File.readable?(pathname)
else
return false
end
end
def expand_filename(filename)
return File.expand_path(filename, @configuration[:image_path] + "/")
end
def cache_filename(identification, commands)
commands = MagickCommandList.from_anything(commands)
return File.expand_path(@configuration[:controller].class.to_s.gsub(/[^A-Za-z0-9]/, '') + "." +
commands.to_cache_s(@configuration) + identification.gsub(/\//, '.'), @configuration[:cache]+"/")
end
def cached?(identification, commands)
clear_cache_if_stale(identification, commands)
return caching? && File.exists?(cache_filename(identification, commands))
end
def clear_cache_if_stale(filename, commands)
if caching? && @configuration[:type]==:action
source_path = expand_filename(filename)
cache_path = cache_filename(filename, commands)
if File.exists?(cache_path) && File.mtime(cache_path) < File.mtime(source_path)
FileUtils.rm(source_path)
end
end
end
def url_options_for_imagemagick(filename_or_params, p_commands)
if p_commands.nil? && filename_or_params.is_a?(Hash)
p_commands = filename_or_params.delete(@configuration[:commands_param])
elsif p_commands.nil?
p_commands = []
end
commands = MagickCommandList.from_anything(p_commands)
path_options = nil
if @configuration[:type]==:action
path_options = { :action=>@configuration[:action_name].to_s }
path_options[:id] = filename_or_params
path_options[@configuration[:commands_param]] = commands.to_s unless commands.commands.empty?
render(filename_or_params, filename_or_params, commands) if prerender?
elsif @configuration[:type]==:filter
path_options = { }
path_options.merge!(filename_or_params)
path_options[@configuration[:commands_param]] = commands.to_s unless commands.commands.empty?
end
return path_options
end
def render(image, identification, commands)
commands = ImageMagickMacroHelper::MagickCommandList.from_anything(commands)
clear_cache_if_stale(identification, commands)
if cached?(identification, commands)
return Magick::Image.read(cache_filename(identification, commands)).first
else
if image.is_a?(String)
if valid_filename?(image)
image = Magick::Image.read(expand_filename(image)).first
else
return nil
end
end
image = commands.execute_on(image, @configuration)
image.strip!
if caching?
image.write(cache_filename(identification, commands))
end
return image
end
return nil
end
class MagickCommandList
attr_reader :commands
def initialize
@commands = Array.new
end
def to_s
command_string = @commands.empty? ? "" : @commands.join("+")
end
def to_cache_s(configuration)
if !commands.empty?
command_string = to_s + "."
command_string += recipe_versions(configuration)
command_string.gsub!(/[+,]/, '.')
command_string.gsub!(/[^a-zA-Z0-9.]/, '')
else
command_string = ""
end
return command_string
end
def recipe_versions(configuration)
versions = ""
@commands.each { |cmd| versions += cmd.recipe_version(configuration).to_s }
versions += "." unless versions.empty?
return versions
end
def execute_on(picture, configuration)
@commands.each { |cmd| picture = cmd.execute_on(picture, configuration) }
return picture
end
def self.from_s(command_string, entered_by_user = true)
list = MagickCommandList.new
(command_string+"+").scan(/(([^(+]+)(\([^)]+\))?)*/) do |match|
list.commands << MagickCommand.from_s(match[0], entered_by_user) unless match[0].nil?
end
return list
end
def self.from_anything(p_commands, entered_by_user = true)
if p_commands.is_a?(MagickCommandList)
commands = p_commands
elsif p_commands.is_a?(Array)
commands = MagickCommandList.new
p_commands.each { |cmd| commands.commands << MagickCommand.new(cmd[0].to_s, cmd[1], entered_by_user) }
elsif p_commands.is_a?(String)
commands = MagickCommandList.from_s(p_commands, entered_by_user)
else
commands = MagickCommandList.new
end
return commands
end
end
class MagickCommand
attr_accessor :command, :data, :entered_by_user
def initialize(command, data = nil, entered_by_user = true)
@command = command.to_sym
@data = data
@entered_by_user = entered_by_user
end
def to_s
command_string = @command.to_s
command_string += "(" + MagickCommand.encode(data) + ")" unless data.nil?
return command_string
end
def execute_on(picture, configuration)
if configuration[:local_recipes].has_key?(@command.to_sym)
recipe = configuration[:local_recipes][@command.to_sym].first
elsif (!entered_by_user || [:global,:builtin].member?(configuration[:max_recipe_level])) &&
GlobalRecipes.has_key?(@command.to_sym)
recipe = GlobalRecipes[@command.to_sym].first
elsif (!entered_by_user || configuration[:max_recipe_level]==:builtin) &&
BuiltinRecipes.has_key?(@command.to_sym)
recipe = BuiltinRecipes[@command.to_sym].first
else
raise "Command " + @command.to_s + " not found."
end
until recipe.is_a?(Magick::Image)
if recipe.is_a?(Symbol)
recipe = configuration[:controller].method(recipe)
elsif recipe.respond_to?(:execute_recipe_on)
recipe = recipe.method(:execute_recipe_on)
end
begin
if recipe.is_a?(Proc) || recipe.is_a?(Method)
recipe = recipe.call(picture, *@data.to_s.split(/,/))
elsif recipe.is_a?(String)
recipe = MagickCommandList.from_s(recipe, false).execute_on(picture, configuration)
end
rescue Exception=>e
raise "Error while executing " + @command.to_s + ": " + e.to_s
end
end
picture = recipe
return picture
end
def recipe_version(configuration)
if configuration[:local_recipes].has_key?(@command.to_sym)
recipe = configuration[:local_recipes][@command.to_sym]
elsif (!entered_by_user || [:global,:builtin].member?(configuration[:max_recipe_level])) &&
GlobalRecipes.has_key?(@command.to_sym)
recipe = GlobalRecipes[@command.to_sym]
elsif (!entered_by_user || configuration[:max_recipe_level]==:builtin) &&
BuiltinRecipes.has_key?(@command.to_sym)
recipe = BuiltinRecipes[@command.to_sym]
else
raise "Command " + @command.to_s + " not found."
end
version = recipe.last.to_s
if recipe.first.is_a?(String)
version += MagickCommandList.from_s(recipe.first, false).recipe_versions(configuration)
end
return version
end
def self.from_s(command, entered_by_user = true)
return MagickCommand.new(command[/^[^(]+/], command[/\((.+)\)$/, 1], entered_by_user)
end
def self.encode(data)
return data.to_s.tr("()", "[]")
end
def self.decode(data)
return data.to_s.tr("[]", "()")
end
end
end
class RecipeSet < Hash
def add(name = nil, recipe = nil, version = nil, &block)
if block_given?
yield(self)
else
if recipe.is_a?(String) && version.nil?
version = recipe
end
self[name.to_sym] = [recipe, version]
end
return self
end
def add_alias(alias_name, original_name, version = nil)
self[alias_name.to_sym] = [self[original_name.to_sym].first, version]
end
end
GlobalRecipes = RecipeSet.new
BuiltinRecipes = RecipeSet.new.add do |recipes|
recipes.add :blur_image, Proc.new { |image, radius, sigma| image.blur_image(radius.to_f, sigma.to_f) }
recipes.add :border, (Proc.new do |image, width, height, color|
color = "#" + color unless color[0,1]=="#"
image.border!(width.to_i, height.to_i, color)
end )
recipes.add :colorize, (Proc.new do |image, red_pct, green_pct, blue_pct, fill|
fill = "#" + fill unless fill[0,1]=="#"
image.colorize(red_pct.to_f, green_pct.to_f, blue_pct.to_f, fill)
end )
recipes.add :crop, Proc.new { |image, x, y, width, height| image.crop!(x.to_i, y.to_i, width.to_i, height.to_i) }
recipes.add :equalize, Proc.new { |image| image.equalize }
recipes.add :flip, Proc.new { |image| image.flip! }
recipes.add :flop, Proc.new { |image| image.flop! }
recipes.add :geometry, Proc.new { |image, data| image.change_geometry!(data) { |cols, rows, img| img.resize!(cols, rows) } }
recipes.add :implode, Proc.new { |image, amount| image.implode(amount.to_f) }
recipes.add :level, Proc.new { |image, black_point, mid_point| image.level(black_point.to_f, mid_point.to_f) }
recipes.add :normalize, Proc.new { |image| image.normalize }
recipes.add :oilpaint, Proc.new { |image, amount| image.oil_paint(amount.to_f) }
recipes.add :opaque, (Proc.new do |image, target, fill|
target = "#" + target unless target[0,1]=="#"
fill = "#" + fill unless fill[0,1]=="#"
image.opaque(target, fill)
end )
recipes.add :posterize, Proc.new { |image, levels| image.posterize(levels.to_f) }
recipes.add :rotate, Proc.new { |image, amount| image.rotate!(amount.to_i) }
recipes.add :sharpen, Proc.new { |image, radius, sigma| image.sharpen(radius.to_f, sigma.to_f) }
recipes.add :trim, Proc.new { |image| image.trim! }
recipes.add_alias :resize, :geometry
recipes.add_alias :s, :geometry
recipes.add_alias :blur, :blur_image
recipes.add :part, (Proc.new do |image, size|
target_w = size.split(/x/)[0].to_i
target_h = size.split(/x/)[1].to_i
orig_w = image.columns
orig_h = image.rows
if (orig_w.to_f/orig_h) > (target_w.to_f/target_h)
image.change_geometry!("x"+target_h.to_s) { |cols, rows, img| img.resize!(cols, rows) }
image.crop!((image.columns - target_w) / 2, 0, target_w, target_h)
else
image.change_geometry!(target_w.to_s+"x") { |cols, rows, img| img.resize!(cols, rows) }
image.crop!(0, (image.height - target_h) / 2, target_w, target_h)
end
image
end )
end
end
end
end