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</