Undefined Method crop! Using Carrierwave with MiniMagick on rails 3.1.3

counterbeing picture counterbeing · Dec 7, 2011 · Viewed 8.9k times · Source

I was having a heck of a time getting this to work, and still am. I'll get to the heart of it. I'm following Ryan Bates tutorial to make cropping work using Jcrop and Carrierwave. I've opted to use MiniMagick because even after reinstalling ImageMagick and RMagick on my machine I get an error that kills the rails server on my local machine. Anyway switching to MiniMagick fixed that for me. So everything is really nice up until this point. I have different sized images being produced, and they're being uploaded successfully. But once I try to crop I get this error:

undefined method `crop!' for #<MiniMagick::CommandBuilder:0x000001052e4608>

This is confusing the heck out of me because I'm using pretty much the exact same code as Bates:

def crop
if model.crop_x.present?
  resize_to_limit(700, 700)
  manipulate! do |img|
    x = model.crop_x.to_i
    y = model.crop_y.to_i
    w = model.crop_w.to_i
    h = model.crop_h.to_i
    img.crop!(x, y, w, h)
  end
 end
end

Anyway, it's that crop method that's failing. So I thought to myself, that's an ImageMagick command... So I looked at the ImageMagick doco, and I couldn't find the crop method with the bang, so I tried it without, and then the error turns to this:

No such file or directory - /var/folders/dF/dFNM2+Y7FVScn4+OxVHKOU+++TI/-Tmp-/mini_magick20111207-34409-1tnaa07.jpg

Anyway, something isn't making a ton of sense to me, any help would be appreciated! Thanks for reading!

Answer

fny picture fny · Apr 1, 2012

In short:

img.crop("#{size}#{offset}") # Doesn't return an image...
img # ...so you'll need to call it yourself

Here's a better explanation of why this happened as opposed to a cut/paste style solution.

RMagick and MiniMagick aren't interchangeable. RMagick has a very Ruby-like DSL and as such employs methods that take multiple arguments:

rmagick_image.crop(x_offset, y_offset, width, height) # Returns an image object
rmagick_image.crop!(x_offset, y_offset, width, height) # Edits object in place

MiniMagick instead dynamically generates methods by iterating through a list of MOGRIFY_COMMANDS that match up with numerous dash-prefixed options specified in ImageMagick's mogrify documentation. Each of those methods pass their arguments directly to mogrify and none return an image object:

minimagick_image.crop('100x200') # Translates to `mogrify -crop 100x200 image.ext`
minimagick_image.polaroid('12')  # Executes `mogrify -polaroid 12 image.ext`

In kind, RMagick has crop! and MiniMagick doesn't.

According to the ImageMagick docs, mogrify -crop takes an argument geometry. The geometry argument is explained here. You'll notice that all of those arguments are strings, so instead of crop(100,200) you would use crop('100x200') or crop('100%). It's not very Ruby-like, but that's part of what makes MiniMagick so lightweight.

With that knowledge, we can deduce how to crop with MiniMagick. mogrify -crop can take a geometry as a string widthxheight+xoffset+yoffset, so we just need to build a similar string.

Given w,h,x, and y you could use whichever of the following you find most readable:

# Concatenating plus signs with plus signs is atrociously confusing.
# Recommended only if you want to drive your future self insane.
mogrify_arg = w + 'x' + h + '+' + x + '+' + y

# Readable but inefficient
mogrify_arg = [ w, 'x', h, '+', x, '+', y ].join('')

# Questionable readability
mogrify_arg = "#{w}x#{h}+#{x}+#{y}"

# Slick, performant, but potentially risky: `<<` modifies the receiving object in place
# `w` is actually changing here to  "WxH+X+Y"...
mogrify_arg = w << 'x' << h << '+' << x << '+' << y

# A lovely, self-documenting version
size = w << 'x' << h
offset = '+' << x '+' << y
mogrify_arg = "#{size}#{offset}"

Here's a complete example:

def crop
  if model.crop_x.present?
    resize_to_limit(700, 700)

    manipulate! do |img|
      x = model.crop_x
      y = model.crop_y
      w = model.crop_w
      h = model.crop_h

      size = w << 'x' << h
      offset = '+' << x << '+' << y

      img.crop("#{size}#{offset}") # Doesn't return an image...
      img # ...so you'll need to call it yourself
    end

   end
  end