module ActionController
  
module Macros
    
module ImageMagick
      
#
      
# The default configuration options.
      
#
      
DEFAULT_OPTIONS = { :action_name=>:imagemagick,
                          
:cache=>nil,
                          
:prerender=>false,
                          
:max_recipe_level=>:builtin,
                          
:commands_param=>:commands }
      
      
def self.append_features(base) #:nodoc:
        
super
        
        
base.extend(ClassMethods)
      
end

      
# This macro can be used to add ImageMagick functionality to a controller.
      
# You can use it to access ImageMagick commands with a simple template tag.
      
# ImageMagick is a set of image processing tools, that can do nearly everything
      
# you'd ever want to do with an image. Resize, rotate, crop and much more.
      
#
      
# This needs the RMagick library (and ImageMagick, of course) to work.
      
#
      
# * http://www.imagemagick.org/
      
# * http://rmagick.rubyforge.org/
      
#
      
#
      
# == As an action, with images from a directory
      
#
      
# The simplest way to use this extension, is by adding the behaviour to a directory.
      
# For example:
      
#
      
#   # Controller
      
#   class Photo < ApplicationController
      
#     imagemagick_for_directory '/var/lib/photos'
      
#   end
      
#
      
#   # View
      
#   <%= imagemagick_tag 'presentation.jpg', 'resize(100x50)+flop' %>
      
#
      
# This example generates an <tt><img></tt> tag in the view, which points to
      
# the +imagemagick+ action of the Photo controller. All you need to do to
      
# generate this controller, is adding the <tt>imagemagick_for_directory</tt> method to your
      
# controller class. The +imagemagick+ action opens the image, sends the
      
# commands to RMagick for processing and then sends the result to the browser.
      
#
      
# This example shows you the image presentation.jpg from the <tt>/var/lib/photos</tt>
      
# directory. It is processed with the commands <tt>resize(100x50)</tt>, which means
      
# that ImageMagick will shrink it to fit it in an area of 100x50 pixels, and
      
# <tt>flop</tt>, which means that left and right are reversed.
      
#
      
#
      
# == As an +after_filter+, with images provided by your action
      
#
      
# Or perhaps you have stored your images not in a directory, but in a database or
      
# somewhere else. Even then you can use the ImageMagick extension. This works by
      
# adding the ImageMagick processing as an +after_filter+ for the action that
      
# retrieves and sends the original image. For example:
      
#
      
#   # Controller
      
#   class Photo < ApplicationController
      
#     imagemagick_filter_for :display_photo
      
#     
      
#     def display_photo
      
#       # do some work to retrieve and send the photo (that's your job)
      
#     end
      
#   end
      
#   
      
#   # View
      
#   <%= imagemagick_tag :action=>'display_photo', :commands=>'resize(100)', ... other parameters for display_photo ... %>
      
#
      
# The action +display_photo+ gets the image from the database or where you've stored
      
# it and renders it normally. Then after that, the imagemagick filter is executed. It
      
# reads the output from your action, processes it using the commands from <tt>@params['commands']</tt>
      
# and finally sends the resulting image to the browser (or to another filter, if there is one).
      

      
# The <tt><img></tt> tag for this rendered image is generated by +imagemagick_tag+:
      
# you give it the +url_for+ options hash that your action needs to find and display the image,
      
# plus the additional <tt>:commands</tt> parameter that is to be used by ImageMagick.
      
#
      
#
      
# == The image source
      
#
      
# === Action: the image directory
      
#
      
# If you used +imagemagick_for_directory+, the extension expects to find all images in a directory,
      
# the name of which is specified with the +imagemagick_for_directory+ method. In this directory,
      
# you can simply place the image files. The names and extensions are not really important,
      
# ImageMagick is smart enough to determine the file type from the contents of the file.
      
#
      
# In the image directory, you can have subdirectories. Just give the name of the
      
# subdirectory to <tt>imagemagick_tag</tt>. For example, if in our directory <tt>/var/lib/photos</tt>
      
# there is a subdirectory called rails, we can refer to an image in that subdirectory
      
# like this:
      
#
      
#   # View
      
#   <%= imagemagick_tag 'rails/presentation.jpg' %>
      
#
      
# You can use the ImageMagick extension in multiple controllers, but you might
      
# want to specify a different image directory for each of them. (But even that is not
      
# required.)
      
#
      
# === Filter: the <tt>@response.body</tt>
      
#
      
# The filter expects your action to put the image in <tt>@response.body</tt>. If you use
      
# <tt>render(:file)</tt> or <tt>render(:text)</tt> to render the image, it should work right. You
      
# can't use the streaming methods +send_file+ and +send_data+.
      
#
      
#
      
# == Caching
      
#
      
# If you don't take any precautions, the rendered image will not be saved, and
      
# it has to be rendered from scratch for every new request. This can make it a
      
# little slow. This extension therefore provides a caching mechanism, which caches
      
# the processed images. For the requests that follow after the first rendering,
      
# it simply sends the cached image, instead of re-rendering it.
      
#
      
# To enable caching, you need a cache directory that is writable by the webserver.
      
# If you have that directory, all you have to do is specify it with the +cache+ option
      
# of the +imagemagick_for+ method.
      
#
      
#   # Controller
      
#   class Photo < ApplicationController
      
#     imagemagick_for_directory '/var/lib/photos', :cache=>'/var/lib/photo-cache'
      
#   end
      
#
      
# You can use the same cache directory for multiple controllers. Caching is also
      
# possible if you use +imagemagick_filter_for+.
      
#
      
#
      
# == Pre-rendering
      
#
      
# By default, the commands are transferred in the URL of the image, and are executed
      
# when the user requests the image. This is reasonably safe, but it does allow your
      
# visitors to specify any ImageMagick command they want by simply changing the URL.
      
# If you think this is a problem, you can enable the pre-rendering function.
      
#
      
# Pre-rendering means that the image is rendered earlier in the process: when the
      
# +imagemagick_tag+ is displayed by the view. The image is rendered and cached, and the
      
# name of that cached image is sent to the browser, which then uses it to request the
      
# image. This means that the user does not have the possibility to influence the
      
# rendering of the image, as rendering is done from the view. It also means that the
      
# initial delay of the rendering of the image is on the web page, instead of on the
      
# loading of the images.
      
#
      
# The pre-rendered images are stored by the caching system, so you'll need to
      
# enable caching if you want to use pre-rendering.
      
#
      
#   # Controller
      
#   class Photo < ApplicationController
      
#     imagemagick_for_directory '/var/lib/photos', :cache=>'/var/lib/photo-cache', :prerender=>true
      
#   end
      
#
      
# Prerendering is only possible if you're using the +imagemagick_for_directory+ action,
      
# <b>prerendering does not work with <tt>imagemagick_filter_for</tt></b>.
      
#
      
#
      
# == Recipes
      
#
      
# It is possible to define your own commands, or 'recipes', for use with <tt>imagemagick_tag</tt>.
      
# See the documentation for <tt>ActionController::Macros::ImageMagick::RecipeSet</tt> for more
      
# information.
      

      
# If you have your own set of recipes, you can restrict the commands that can be executed
      
# to the recipes in this set. See the <tt>:max_recipe_level</tt> option of <tt>imagemagick_for</tt>.
      

      
#
      
# == Routes.rb trick
      
#
      
# +imagemagick_for+ works without any additional routes. It uses urls like:
      
#   /photo/imagemagick/presentation.jpg?command=resize(100x100)
      
#
      
# If you'd like to get rid of the <tt>?command=</tt>, you can add the following routes to
      
# your <tt>routes.rb</tt>:
      
#     map.connect ':controller/imagemagick/:id', :action=>'imagemagick', :commands=>''
      
#     map.connect ':controller/imagemagick/:commands/:id', :action=>'imagemagick'
      
#
      
# The above url will now be rendered as:
      
#   /photo/imagemagick/resize(100x100)/presentation.jpg
      
#
      
module ClassMethods
        
#
        
# Enables the ImageMagick extension for the given +image_path+. For a description
        
# of the +options+, see the documentation for +imagemagick_for+.
        
#
        
def imagemagick_for_directory(image_path, options = {})
          
# call imagemagick_for with a String
          
imagemagick_for(image_path.to_s, options)
        
end
        
        
#
        
# Enables the ImageMagick extension as an +after_filter+ for +action_name+.
        
# The response of the action will be processed as an image, with the commands found
        
# in <tt>@params["command"]</tt>.
        
#
        
# For a description of the +options+, see the documentation for +imagemagick_for+.
        
#
        
def imagemagick_filter_for(action_name, options = {})
          
# call imagemagick_for with a Symbol
          
imagemagick_for(action_name.to_sym, options)
        
end
        
        
#
        
# Enables the ImageMagick extension for this controller. If +image_source+ is a +String+,
        
# the extension will be enabled for the directory with that name. If +image_source+ is a
        
# +Symbol+, the extension will be enabled as a +after_filter+ that does its work on the
        
# result of the action with that name.
        
#
        
# <i>This method is also available as +imagemagick_for_directory+ and +imagemagick_filter_for+.</i>
        
#
        
# The following +options+ can be specified:
        
#
        
# * <tt>:cache</tt>: the directory where cached images are stored, or +nil+ (the default)
        
#   if you don't want caching. The directory has to be writable by the web server.
        
# * <tt>:prerender</tt>: set this to +true+ if you want pre-rendering. This only works if
        
#   you have enabled caching. By default, pre-rendering is off.
        
# * <tt>:action_name</tt>: the name of the imagemagick action. The default is +imagemagick+,
        
#   you can change it by specifying this option.
        
# * <tt>:commands_param</tt>: the name of the commands parameter in the <tt>@params</tt> hash,
        
#   <tt>:commands</tt> by default.
        
# * <tt>:max_recipe_level</tt>: whether or not you can use only local or global recipes
        
#   (see the documentation for +RecipeSet+ for more about these levels). This can have one
        
#   of those values:
        
#   * <tt>:local</tt>: only local recipes can be used
        
#   * <tt>:global</tt>: only local and global recipes can be used
        
#   * <tt>:builtin</tt> (the default): local, global and the built-in recipes can be used
        
#
        
# To change the system-wide defaults, see the <tt>ActionController::Macros::ImageMagick::DEFAULT_OPTIONS</tt>
        
#
        
def imagemagick_for(image_source, options = {})
          
require 'RMagick'
          
          
configuration = Hash.new.merge!(ActionController::Macros::ImageMagick::DEFAULT_OPTIONS)
          
configuration.update(options)
          
          
# expand the path if it is relative
          
if defined?(RAILS_ROOT)
            
configuration[:cache] = File.expand_path(configuration[:cache], RAILS_ROOT) unless configuration[:cache].nil?
          
end
          
          
# set the maximum recipe level that is allowed
          
configuration[:max_recipe_level] = configuration[:max_recipe_level].to_sym
          
configuration[:max_recipe_level] = :builtin unless [:builtin, :global, :local].member?(configuration[:max_recipe_level])
          
          
# initialize the list of recipes specific to this controller.
          
configuration[:local_recipes] = RecipeSet.new
          
          
# what is the source of the images?
          
if image_source.is_a?(String)
            
# images from a directory
            
# strip the trailing slash
            
configuration[:image_path] = image_source
            
configuration[:image_path] = image_source.sub(/\/$/, '') if image_source =~ /\/$/
            
configuration[:type] = :action
            
            
# expand 
            
if defined?(RAILS_ROOT)
              
configuration[:image_path] = File.expand_path(configuration[:image_path], RAILS_ROOT)
            
end
            
            
# the name of the action can be specified, but it has to be a Symbol
            
configuration[:action_name] = :imagemagick unless configuration.has_key?(:action_name)
            
configuration[:action_name] = configuration[:action_name].to_sym
          
else
            
# images from the response of an action
            
configuration[:type] = :filter
            
configuration[:for_action] = image_source.to_sym
          
end
          
          
#
          
# Every controller-class gets a imagemagick_macro_helper-variable,
          
# which contains the configuration options.
          
#
          @
imagemagick_macro_helper = ImageMagickMacroHelper.new(configuration)
          
          
#
          
# Makes the class helper available from the controller instance.
          
#
          
define_method(:imagemagick_macro_helper) do
            
return self.class.instance_variable_get(:@imagemagick_macro_helper)
          
end
          
protected :imagemagick_macro_helper
          
          
#
          
# Returns the +RecipeSet+ with recipes local to this controller.
          
#
          
define_method(:imagemagick_local_recipes) do
            
return imagemagick_macro_helper.local_recipes
          
end
          
protected :imagemagick_local_recipes
          
          
#
          
# Shortcut to the +url_for_imagemagick+ method of the helper.
          
# Gives the url to the +imagemagick+ action for this filename, with these commands.
          
#
          
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
          
          
#
          
# Implement the extension.
          
#
          
if configuration[:type]==:action
            
implement_as_action(configuration)
          
elsif configuration[:type]==:filter
            
implement_as_filter(configuration)
          
end
        
end
        
        
#
        
# Adds a custom recipe with the given +name+. +recipe+ can be a +String+,
        
# a +Proc+, a +Class+ or a +Symbol+.
        
#
        
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
          
#
          
# Implements the extension as an action.
          
#
          
def implement_as_action(configuration)
            
#
            
# The +imagemagick+ action of the controller.
            
# Reads the filename and the commands from the request and sends an image.
            
#
            
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)
                  
# prerendering is required, but there is no cached image
                  
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

          
#
          
# Implements the extension using +after_filter+.
          
#
          
def implement_as_filter(configuration)
            
#
            
# The +imagemagick+ filter method of the controller.
            
# Reads the response result and the commands from the request and sends an image.
            
#
            
define_method(:imagemagick_after_filter) do
              
image = Magick::Image.from_blob(@response.body)
              
              
# if the result is not an image, we don't have to process it
              
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
      
      
#
      
# The class that contains all methods of the ImageMagick extension.
      
# Every controller with imagemagick_for gets an instance of this, filled
      
# with its own configuration options.
      
#
      
class ImageMagickMacroHelper #:nodoc: all
        
def initialize(configuration)
          @
configuration = configuration
        
end
        
        
def caching?
          
return !@configuration[:cache].nil?
        
end
        
        
def prerender?
          
# only if caching is available and the +:action+ method is used
          
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
        
        
# check the filename: it may contain directory names, but it cannot
        
# go to a higher directory than configuration[:image_path]
        
def valid_filename?(filename)
          
pathname = expand_filename(filename)
          
if pathname =~ /^#{@configuration[:image_path]}\//
            
return File.readable?(pathname)
          
else
            
return false
          
end
        
end
        
        
#
        
# Expands the filename with the +image_path+.
        
#
        
def expand_filename(filename)
          
return File.expand_path(filename, @configuration[:image_path] + "/")
        
end
        
        
#
        
# Returns the filename the cached image will have if it is renderd
        
# with the given commands.
        
#
        
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
        
        
#
        
# Returns true if this image with these commands is cached.
        
#
        
def cached?(identification, commands)
          
clear_cache_if_stale(identification, commands)
          
return caching? && File.exists?(cache_filename(identification, commands))
        
end
        
        
#
        
# Removes the cached image for this file+commands if the original image
        
# is newer than the file in the cache. 
        
#
        
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)
              
# remove the cached file, the original file is newer
              
FileUtils.rm(source_path)
            
end
          
end
        
end
        
        
#
        
# Returns a Hash of url_for options that render the given filename
        
# with the given ImageMagick commands.
        
#
        
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
        
        
#
        
# Renders the +image+ with the given +commands+.
        
# +image+ can be the name of a file, which will be read, or a Magick::Image-object.
        
# +identification+ is the unique identifier string for this image, for use in the cache.
        
#
        
def render(image, identification, commands)
          
commands = ImageMagickMacroHelper::MagickCommandList.from_anything(commands)
          
          
clear_cache_if_stale(identification, commands)
          
          
if cached?(identification, commands)
            
# return the file
            
return Magick::Image.read(cache_filename(identification, commands)).first
            
          
else
            
# not cached, we have to render the image
            
            
# is it a string (filename) or already an image?
            
if image.is_a?(String)
              
if valid_filename?(image)
                
image = Magick::Image.read(expand_filename(image)).first
              
else
                
return nil
              
end
            
end
            
            
# process the image
            
image = commands.execute_on(image, @configuration)
            
image.strip!
            
            
# save in the cache?
            
if caching?
              
image.write(cache_filename(identification, commands))
            
end
            
            
return image
          
end
          
          
return nil
        
end
        
        
#
        
# Contains a list of ImageMagick commands (of the type MagickCommand).
        
#
        
class MagickCommandList
          
attr_reader :commands
          
          
def initialize
            @
commands = Array.new
          
end
          
          
#
          
# Returns a String of the commands for use in a url.
          
# (The commands are chained with a +.)
          
#    => resize(100x1)+chop
          
#
          
def to_s
            
command_string = @commands.empty? ? "" : @commands.join("+")
          
end
          
          
#
          
# Returns a String of the commands for use in the cache-filename.
          
# (All strange characters are removed.)
          
#    => resize.100x1.chop.
          
#
          
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
          
          
#
          
# Executes the commands on this list on the picture.
          
# picture should be an Magick::Image-object.
          
#
          
def execute_on(picture, configuration)
            @
commands.each { |cmd| picture = cmd.execute_on(picture, configuration) }
            
return picture
          
end
          
          
#
          
# Parses a String of commands and returns the MagickCommandList.
          
# String is of the type generated by MagickCommandList::to_s
          
#     resize(100x1)+chop
          
#
          
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
          
          
#
          
# Returns the MagickCommandList for p_commands.
          
# p_commands can be:
          
#
          
# * a +MagickCommandList+: it will be returned
          
# * an +Array+: a list of commands. Commands can be Strings and
          
#      arrays: [command, args]:
          
#         [ "resize(1x100)", "resize", ["resize", 100] ]
          
# * a +String+: will be parsed by MagickCommandList.from_s
          
# * anything else: an empty MagickCommandList will be returned
          
#
          
# +entered_by_user+ indicates whether this command list is prepared
          
# by the user (+true+), or that it is prepared by a higher-level recipe (+false+).
          
# If it is set to +true+, the +max_recipe_level+ option will be followed.
          
#
          
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
        
        
#
        
# A command for in a MagickCommandList.
        
# (A command is a command name, and possibly a data string.)
        
#
        
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
          
          
#
          
# Converts the command to a String version for use in the image url.
          
#    "command"  or  "command(args)"
          
#
          
def to_s
            
command_string = @command.to_s
            
command_string += "(" + MagickCommand.encode(data) + ")" unless data.nil?
            
return command_string
          
end
          
          
#
          
# Executes this command on the picture and returns the result.
          
#
          
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
            
            
# recipes may return Strings. continue solving those
            
# Strings until the result is an image, at which point
            
# the rendering is finished.
            
until recipe.is_a?(Magick::Image)
              
# reorganize: Symbol points to a method on the controller
              
#             Class should have a method execute_recipe_on
              
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)
                  
# a string with other imagemagick commands
                  
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)
              
# a string with other imagemagick commands.
              
# append the version numbers of those too.
              
version += MagickCommandList.from_s(recipe.first, false).recipe_versions(configuration)
            
end
            
            
return version
          
end
          
          
#
          
# Reads a command string (of the MagickCommand::to_s format) and returns
          
# a MagickCommand.
          
#   resize(100x10)  becomes @command = "resize", @data = "100x10"
          
#
          
def self.from_s(command, entered_by_user = true)
            
return MagickCommand.new(command[/^[^(]+/], command[/\((.+)\)$/, 1], entered_by_user)
          
end
          
          
#
          
# Encodes the data string for use in the url command string.
          
#
          
def self.encode(data)
            
# in the command string, ( and ) are used to separate
            
# the argument from the command: <tt>resize(100x10)</tt>
            
return data.to_s.tr("()", "[]")
          
end
          
          
#
          
# Decodes the data string from the url command string.
          
#
          
def self.decode(data)
            
return data.to_s.tr("[]", "()")
          
end
        
end
      
end
      
      
      
      
# == Creating recipes
      
#
      
# It is possible to write your own commands to use with +imagemagick_tag+. You do
      
# this by creating +recipes+. Your own recipes can have arguments and you can mix
      
# them with normal ImageMagick methods. Such as:
      
#
      
#   <%= imagemagick_tag 'picture.jpg', 'customthumbnail+resize(100)+myborder(f00)' %>
      
#
      
# There are multiple ways to write your own recipes: as a +String+ of normal
      
# commands, as an inline method (+Proc+), as a method reference (+Symbol+) or as
      
# an external +Class+.
      
#
      
#
      
# === Recipe type: String
      
#
      
# The easiest way is <b>with a String</b>. If you always render your
      
# profile images with <tt>'resize(100)+border(1,1,fff)'</tt>, you can shorten
      
# that with this recipe:
      
#
      
#   # in the controller
      
#   class UserController < ApplicationController
      
#     imagemagick_for_directory '/var/lib/profiles'
      
#     imagemagick_recipe :userprofile, 'resize(100)+border(1,1,fff)'
      
#   end
      
#   
      
#   # in the view
      
#   <%= imagemagick_tag 'john.jpg', 'userprofile' %>
      

      
# As you see, you've now created your own 'command'. +userprofile+ will be
      
# expanded to <tt>resize(100)+border(1,1,fff)</tt> when rendering.
      

      
#
      
# === Recipe type: Proc
      
#
      
# A more sophisticated method is by using a Proc. You can use this
      
# if you want more control than you can get with the String method. And if you
      
# need custom parameters for your recipes, you simply can't use a String.
      
#
      
# With a proc, adding a recipe works like this:
      
#
      
#   # in the controller
      
#   class UserController < ApplicationController
      
#     imagemagick_for_directory '/var/lib/profiles'
      
#     imagemagick_recipe :square, Proc.new { |image, size| image.resize!(size.to_i, size.to_i) }
      
#   end
      
#   
      
#   # in the view
      
#   <%= imagemagick_tag 'john.jpg', 'square(100)' %>
      

      
# This example renders the image as a 100x100 pixel square. (If you look closely, you'll
      
# notice that we're passing the size of 100 pixel as a parameter to the recipe.)
      
# Proc-recipes work by accepting an +image+ object and perhaps some parameters. In this case,
      
# the only parameter is +size+. The +image+ object is the real <tt>RMagick::Image</tt>-object
      
# of the image that is currently being processed. In your proc, you can execute any of the
      
# RMagick methods[http://studio.imagemagick.org/RMagick/doc/] you like.
      

      
# There are two caveats you should keep in mind when using Proc recipes:
      
#
      
# * Except for the +image+ object, <b>all arguments passed to the Proc are Strings</b>.
      
#   The incoming parameter string is split by the commas, so 100,100 would give you
      
#   two arguments. In your proc the "100" and "100" are +String+s, but most RMagick commands
      
#   expect +Integer+s or +Float+s. Remember to convert them with +to_i+ or +to_f+ (as in
      
#   the example above).
      

      
# * <b>Your proc should return the +image+ object.</b> Not every RMagick method is
      
#   available as an in-place method (which is destructive, with a trailing !). Thus, to make
      
#   sure that the changes your recipe makes are saved, you should return the changed image.
      
#   (When a destructive method is available, use that, but returning the image object is still
      
#   required.)
      
#
      

      
# === Recipe type: Method or Class
      
#
      
# If you don't like providing the recipe as a Proc, you can also give your recipes the form
      
# of methods of the controller, or as a separate class.
      
#
      
# If using a method, give the method name as a Symbol:
      
#
      
#   class UserController < ApplicationController
      
#     imagemagick_for_directory '/var/lib/profiles'
      
#     imagemagick_recipe :ownresize, :my_own_resize_method
      
#     
      
#     private
      
#       def my_own_resize_method(image, width, height)
      
#         image.resize!(width.to_i, height.to_i)
      
#       end
      
#   end
      
#
      
# If using a class, it should have a class method called +execute_recipe_on+.
      
# Provide the class to imagemagick_recipe:
      
#
      
#   class MyResizeRecipe
      
#     def self.execute_recipe_on(image, width, height)
      
#       image.resize!(width.to_i, height.to_i)
      
#     end
      
#   end
      
#
      
#   class UserController < ApplicationController
      
#     imagemagick_for_directory '/var/lib/profiles'
      
#     imagemagick_recipe :ownresize, MyResizeRecipe
      
#   end
      
#
      
# As for the arguments and the expected return, the method and class approaches are
      
# exactly the same as with a proc.
      
#
      
#
      
# == Recipe levels
      
#
      
# The ImageMagick extension uses three different levels of recipes. Each level has it
      
# own +RecipeSet+, which is a Hash-like object that contains the recipes of that level.
      
#
      
# At the deepest level, there are the +BuiltinRecipes+. Those recipes implement the
      
# standard commands that come with ImageMagick. These recipes are available in every
      
# controller. (You normally shouldn't have to change the recipes at this level.)
      
#
      
# At a little higher level, you have the +GlobalRecipes+. This is a list of recipes that
      
# are specific to the application, i.e. written by you. If you want your recipes to be
      
# available in every controller, you can add them to the +GlobalRecipes+ RecipeSet.
      
#
      
# At the highest level are the local recipes of a controller. These recipes are only
      
# available in one controller. Recipes defined with +imagemagick_recipe+ are local recipes.
      
#
      
#
      
# === Defining the +GlobalRecipes+
      
#
      
# Creating global recipes works a lot like creating routes. You should do it somewhere
      
# in your +environment.rb+, or in a file that is included by +environment.rb+. Example:
      
#
      
#   ActionController::Macros::ImageMagick::GlobalRecipes.add do |recipes|
      
#     recipes.add :customthumbnail, 'resize(100x100)'
      
#     recipes.add :myborder, Proc.new { |image| image.border(1, 1, '#f00") }
      
#   end
      
#
      
# You can define your global recipes as a +String+, as a +Proc+ or as a +Class+ (see above).
      
# Defining them as a +Method+ of the controller is not possible.
      
#
      
#
      
# === Defining local recipes
      
#
      
# You can define local recipes in your controller with the +imagemagick_recipe+ method.
      
# Example:
      
#
      
#   class  < ApplicationController
      
#     imagemagick_for '/var/lib/profiles'
      
#     imagemagick_recipe :customthumbnail, 'resize(100x100)'
      
#   end
      
#
      
# You can define your local recipes as a +String+, as a +Proc+, as a +Method+ or as
      
# a +Class+ (see above).
      

      
# It's also possible to add local recipes on the fly. For example, if you have stored your
      
# recipes a database, you write a method to retrieve and add them. To do this, you can
      
# add the recipe-adding method as a filter for the +imagemagick+ action. For example:
      
#
      
#   class  < ApplicationController
      
#     imagemagick_for '/var/lib/profiles'
      
#     
      
#     # add a filter that is executed before the imagemagick action
      
#     before_filter :add_local_recipes, :only_action=>:imagemagick
      
#     
      
#     private
      
#       def add_local_recipes
      
#         imagemagick_local_recipes.add { |recipes|
      
#           recipes.add :customthumbnail, 'resize(100x100)'
      
#           recipes.add :myborder, Proc.new { |image| image.border(1, 1, '#f00") }
      
#         end
      
#       end
      
#   end
      
#
      
#
      
# == Restricting recipes
      
#
      
# It is possible to limit the commands that one can execute on an image. You can restrict
      
# it to only accept the recipes from the local recipe set, or only from the local and
      
# global recipe sets. Set this <tt>:max_recipe_level</tt> as an option for <tt>imagemagick_for</tt>:
      
#
      
#   class  < ApplicationController
      
#     # if you want only local recipes
      
#     imagemagick_for '/var/lib/profiles', :max_recipe_level=>:local
      
#
      
#     # if you want only local and global recipes
      
#     imagemagick_for '/var/lib/profiles', :max_recipe_level=>:global
      
#   end
      
#
      
# If you don't specify a <tt>:max_recipe_level</tt>, recipes from all three levels (<tt>:local</tt>, 
      
# <tt>:global</tt> and <tt>:builtin</tt>) may be used.
      
#
      
#
      
# == Recipe versions
      
#
      
# (This only applies if you're using the cache.) Combining custom recipes with the image
      
# cache can lead to problems. In the cache, the images are stored with the names of the
      
# recipes they were generated with. Now, if you change a recipe but do not change the name,
      
# the cached images will not be updated. The extension will still serve images that were
      
# rendered with the old recipe.
      

      
# The solution to this can be very simple: remember to empty the cache directory whenever
      
# you change your recipes. This is a perfect way to solve the problem. But there is another
      
# option, which is a little more sophisticated: add version numbers to your recipes, and
      
# update the number when you change the recipe.
      

      
# Recipes can be numbers or strings. The order is not important (version 3 can be older than
      
# version 1). As long as you're using new version numbers that you haven't used before for the
      
# same recipe, you'll be fine.
      

      
# You can add the version number when you define the recipe. For example:
      
#
      
#   class  < ApplicationController
      
#     imagemagick_for '/var/lib/profiles', :cache=>'/my/cache'
      
#     imagemagick_recipe :myborder, Proc.new({ |image| image.border(1, 1, '#f00") }), 'version1'
      
#   end
      
#
      
# The rendered images are now cached with the version number, 'version1'. Now if you want
      
# to change the recipe, just be sure to bump the version number:
      
#
      
#   class  < ApplicationController
      
#     imagemagick_for '/var/lib/profiles', :cache=>'/my/cache'
      
#     imagemagick_recipe :myborder, Proc.new({ |image| image.border(2, 2, '#f00") }), 'version2'
      
#   end
      
#
      
# That's about all there is to recipe versions. Just two last notes:
      
#
      
# * If you don't define a version number, the default is +nil+. This means that you can
      
#   start setting version numbers starting with the second version of your recipe. (The first
      
#   version has number +nil+, the second gets something like +'version2'+ etc.)
      
# * If your recipe is a +String+, you don't have to bother adding version numbers. By default,
      
#   the version number is set to the value of the +String+, which means that if you change the
      
#   recipe +String+ the version will be automatically updated too.
      
#
      
class RecipeSet < Hash
        
#
        
# Adds a custom recipe with the given +name+ and +version+. +recipe+ can be a +String+,
        
# a +Proc+, a +Class+ or a +Symbol+. If +recipe+ is a +String+ and the +version+ is nil,
        
# +version+ will be set to the +String+.
        
#
        
# If a block is given, the block will be executed with this +self+
        
# as the only argument. (This can be used as a way to add many recipes in a row.)
        
#
        
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
        
        
#
        
# Adds the command +alias_name+ as an alias for the command +original_name+.
        
#
        
def add_alias(alias_name, original_name, version = nil)
          
self[alias_name.to_sym] = [self[original_name.to_sym].first, version]
        
end
      
end
      
      
#
      
# Global recipes, available in every controller.
      
#
      
GlobalRecipes = RecipeSet.new
      
      
#
      
# Recipes for the standard RMagick commands.
      
#
      
BuiltinRecipes = RecipeSet.new.add do |recipes|
        
# standard RMagick commands
        
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! }
        
        
# aliases
        
recipes.add_alias :resize, :geometry
        
recipes.add_alias :s, :geometry
        
recipes.add_alias :blur, :blur_image
        
        
        
        
# This method is not available from imagemagick, but
        
# it is useful: it will always give you an image of
        
# exactly the required size. part(100x100) gives you
        
# a 100x100 pixel crop from the center of the image.
        
#
        
# For example: the image is   ********
        
#                             ********
        
#                             ********
        

        
# And you want this:          **
        
#                             **
        
# It will then first resize the image: ****
        
#                                      ****
        
# And then crops it:         *[**]*
        
#                            *[**]*
        
#
        
# If the source image was this:
        
#        **                    --
        
#        **  it would crop:    **
        
#        **                    **
        
#        **                    --
        
#
        
# The command also scales up, if the image is too small
        
# to cover the whole part.
        
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)
            
# scale to correct height
            
image.change_geometry!("x"+target_h.to_s) { |cols, rows, img| img.resize!(cols, rows) }
            
            
# remove left and right
            
image.crop!((image.columns - target_w) / 2, 0, target_w, target_h)
            
          
else
            
# scale to correct width
            
image.change_geometry!(target_w.to_s+"x") { |cols, rows, img| img.resize!(cols, rows) }
            
            
# remove top and bottom
            
image.crop!(0, (image.height - target_h) / 2, target_w, target_h)
          
end
          
          
image
        
end )
      
end
    
end
  
end
end