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
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 imagemagick_for_directory 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 /var/lib/photos
# directory. It is processed with the commands resize(100x50), which means
# that ImageMagick will shrink it to fit it in an area of 100x50 pixels, and
# flop, 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 @params['commands']
# and finally sends the resulting image to the browser (or to another filter, if there is one).
#
# The
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 :commands 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 imagemagick_tag. For example, if in our directory /var/lib/photos
# 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 @response.body
#
# The filter expects your action to put the image in @response.body. If you use
# render(:file) or render(:text) 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,
# prerendering does not work with imagemagick_filter_for.
#
#
# == Recipes
#
# It is possible to define your own commands, or 'recipes', for use with imagemagick_tag.
# See the documentation for ActionController::Macros::ImageMagick::RecipeSet 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 :max_recipe_level option of imagemagick_for.
#
#
# == 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 ?command=, you can add the following routes to
# your routes.rb:
# 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 @params["command"].
#
# 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.
#
# This method is also available as +imagemagick_for_directory+ and +imagemagick_filter_for+.
#
# The following +options+ can be specified:
#
# * :cache: 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.
# * :prerender: set this to +true+ if you want pre-rendering. This only works if
# you have enabled caching. By default, pre-rendering is off.
# * :action_name: the name of the imagemagick action. The default is +imagemagick+,
# you can change it by specifying this option.
# * :commands_param: the name of the commands parameter in the @params hash,
# :commands by default.
# * :max_recipe_level: 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:
# * :local: only local recipes can be used
# * :global: only local and global recipes can be used
# * :builtin (the default): local, global and the built-in recipes can be used
#
# To change the system-wide defaults, see the ActionController::Macros::ImageMagick::DEFAULT_OPTIONS
#
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: resize(100x10)
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 with a String. If you always render your
# profile images with 'resize(100)+border(1,1,fff)', 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 resize(100)+border(1,1,fff) 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 RMagick::Image-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, all arguments passed to the Proc are Strings.
# 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).
#
# * Your proc should return the +image+ object. 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 :max_recipe_level as an option for imagemagick_for:
#
# 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 :max_recipe_level, recipes from all three levels (:local,
# :global and :builtin) 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