Skip to content

CustomImageOperators

Squeegy edited this page Sep 12, 2010 · 5 revisions

RMagick lets you you do a lot things to images. The image Operators that come with Fleximage allow to handle lots of common tasks, but there is quite a bit that simply isn’t covered. The general rule is that anytime you need to interface directly with RMagick, you should use a custom image operator. You then call your operator from view templates to activate it.

Fleximage provides a Fleximage::Operator::Base class that all the Operators inherit from. This class sets up the interface with operate blocks and gets and sets the currently rendering image for you. All you have to do is define what goes in the operate method, letting you focus on just the image processing.

An instance variable @image is provided for your implementation. This is the Magick::Image that represents the current rendering image. “Currently rendering” means this may or not be the master image. Since you can chain multiple image operations together, @image is the result of all previously called operators.

You also have a @model variable that is the model instance the current image rendering is happening in. It can be used to retrieve information in your model to make decisions about rendering, or even rendering custom text from a database field.

Here is the general format of an operator.

class Fleximage::Operator::MyOperatorName < Fleximage::Operator::Base
  def operate(*args)
    # ... Do RMagick stuff with @image
    # return a Maick::Image instance
  end
end

Utility methods

size_to_xy(size)

Converts a size object to an [x, y] array. Acceptible formats are:

  • 10
  • “10”
  • “10×20”
  • [10, 20]

Usage:

x, y = size_to_xy(“10×20”)

scale(size, img = nil)

Scale the image, respecting aspect ratio. This will not remove any portion of the image, fitting the whole image as best it can inside size. Operation will happen in the main @image unless you supply the img argument to make it happen to a different image object.

scale_and_crop(size, img = nil)

Scale to the desired size and crop edges off to get the exact dimensions needed. Operation will happen in the main @image unless you supply the img argument to make it happen to a different image object.

stretch(size, img = nil)

Resize the image, with no respect to aspect ratio. Operation will happen in the main @image unless you supply the img argument to make it happen to a different image object.

symbol_to_blending_mode

Convert a symbol to an RMagick blending mode. The blending mode governs how image get composited together. You can get some funky effects with modes like :copy_cyan or :screen. For a full list of blending
modes checkout the RMagick documentation.

Usage:

symbol_to_blending_mode(:multiply) #=> Magick::MultiplyCompositeOp

Polaroid Example

The Operator

But no one likes general formats, it’s all about the examples for me.

Lets say you like the classic polaroid effect. To get started with our operator, we would stick this in lib/polaroid.rb for our app.

class Fleximage::Operator::Polaroid < Fleximage::Operator::Base
  def operate
    # ...
  end
end

And of course, don’t forget to load this in your rails initialization:

require ‘polaroid’

We can take that complete polaroid effect RMagick script and make an operator out of it. The entire standalone script looks like this:

require "RMagick"

if !ARGV[0]
    puts "Usage: polaroid.rb path-to-image"
    exit
end

image = Magick::Image.read(ARGV[0]).first

image.border!(18, 18, "#f0f0ff")

# Bend the image
image.background_color = "none"

amplitude = image.columns * 0.01        # vary according to taste
wavelength = image.rows  * 2

image.rotate!(90)
image = image.wave(amplitude, wavelength)
image.rotate!(-90)

# Make the shadow
shadow = image.flop
shadow = shadow.colorize(1, 1, 1, "gray75")     # shadow color can vary to taste
shadow.background_color = "white"       # was "none"
shadow.border!(10, 10, "white")
shadow = shadow.blur_image(0, 3)        # shadow blurriness can vary according to taste

# Composite image over shadow. The y-axis adjustment can vary according to taste.
image = shadow.composite(image, -amplitude/2, 5, Magick::OverCompositeOp)

image.rotate!(-5)                       # vary according to taste
image.trim!

# Add -print to image basename, write to file.
out = ARGV[0].sub(/\./, "-print.")
puts "Writing #{out}"
image.write(out)

We can trim off quite a bit of this. Basically all the image loading and saving is not something we need to worry about.

So RMagick is already loaded by Fleximage, and the image object is already sent to our operator, so we get rid of this loading code:

require "RMagick"

if !ARGV[0]
  puts "Usage: polaroid.rb path-to-image"
  exit
end

And we don’t need to worry about the saving the image to a file, so we can get rid of this code:

# Add -print to image basename, write to file.
out = ARGV[0].sub(/\./, "-print.")
puts "Writing #{out}"
image.write(out)

Now the meat of script uses a user provided image as its base. For us, that will be the currently rendering image provided by the record we are performing image operations on. This is automatically added into our operator instance via the @image instance variable. All we need to do now is do a find replace, replacing image with @image so that the script operates on the right image object. Take the result of these edits, and puts them into your operate method and you get this:

class Fleximage::Operator::Polaroid < Fleximage::Operator::Base
  def operate
    @image.border!(18, 18, "#f0f0ff")

    # Bend the image
    @image.background_color = "none"

    amplitude = @image.columns * 0.01        # vary according to taste
    wavelength = @image.rows  * 2

    @image.rotate!(90)
    @image = @image.wave(amplitude, wavelength)
    @image.rotate!(-90)

    # Make the shadow
    shadow = @image.flop
    shadow = shadow.colorize(1, 1, 1, "gray75")     # shadow color can vary to taste
    shadow.background_color = "white"       # was "none"
    shadow.border!(10, 10, "white")
    shadow = shadow.blur_image(0, 3)        # shadow blurriness can vary according to taste

    # Composite image over shadow. The y-axis adjustment can vary according to taste.
    @image = shadow.composite(@image, -amplitude/2, 5, Magick::OverCompositeOp)

    @image.rotate!(-5)                       # vary according to taste
    @image.trim!
  end
end

The view

Now that we have the operator we need to use it in a template. In any .flexi view, simply call the lowercase and underscored name of your operator class. So MyCoolTransformation would translate to:

@photo.operate do |image|
  image.my_cool_transformation
end

So to call our polaroid operator:

@photo.operate do |image|
  image.polaroid
end

Hit this image in your browser and you should be getting Polaroid-ized image outout.

Arguments

Passing arguments in is easy. The argument you define your operators operate method are the same arguments you need to pass in when you call it from a view.

With a small tweak, we can pass in the angle to rotate our polaroid.

class Fleximage::Operator::Polaroid < Fleximage::Operator::Base
  def operate(angle)
    @image.border!(18, 18, "#f0f0ff")

    # Bend the image
    @image.background_color = "none"

    amplitude = @image.columns * 0.01        # vary according to taste
    wavelength = @image.rows  * 2

    @image.rotate!(90)
    @image = @image.wave(amplitude, wavelength)
    @image.rotate!(-90)

    # Make the shadow
    shadow = @image.flop
    shadow = shadow.colorize(1, 1, 1, "gray75")     # shadow color can vary to taste
    shadow.background_color = "white"       # was "none"
    shadow.border!(10, 10, "white")
    shadow = shadow.blur_image(0, 3)        # shadow blurriness can vary according to taste

    # Composite image over shadow. The y-axis adjustment can vary according to taste.
    @image = shadow.composite(@image, -amplitude/2, 5, Magick::OverCompositeOp)

    @image.rotate!(angle)                       # vary according to taste
    @image.trim!
  end
end

Now our view code changes to:

So to call our polaroid operator:

@photo.operate do |image|
  image.polaroid(25) # polaroid-ize with 25 degree clockwise rotation
end