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