Cropping Animated GIFs with MiniMagick

Use this method to crop the actual canvas and not the frame

Magic by Andrew Melnychuk is licensed under CC BY 2.0

A recent project I worked on allowed users to resize, rotate, and crop avatar images for their profile using the Guillotine jQuery plugin. However, we ran into problems when the user uploaded an animated GIF. While cropping a single-frame image would correctly change the size of the entire image, cropping an animated GIF would crop each frame of the image within the original canvas, and not change the size of the final image itself.

I have an Avatar model with a mounted CarrierWave uploader. The user uploads the image, and then we allow them to edit it with Guillotine and submit the changes. The editing parameters are passed to the Avatar#reprocess_image method where we apply the changes.

class Avatar < ApplicationRecord
  mount_uploader :image, ImageUploader

  def reprocess_image
    if reprocess?
      image.cache_stored_file!
      image.edit(scale: scale, angle: angle, crop_x: photo_x, crop_y: photo_y)
      save!
      image.recreate_versions!
    end
  end
end

When we get our edit parameters on the Avatar model, we reprocess the image like below:

class ImageUploader < CarrierWave::Uploader
  include CarrierWave::MiniMagick

  DEFAULT_IMAGE_DIMENSIONS = [500, 500].freeze

  def edit(scale: 1.0, angle: 0, crop_x: 0, crop_y: 0)
    manipulate! do |img|
      img.resize "#{img.width * scale.to_f}x#{img.height * scale.to_f}"
      img.rotate angle.to_i
      crop_w, crop_h = DEFAULT_IMAGE_DIMENSIONS
      img.crop("#{crop_w}x#{crop_h}+#{crop_x}+#{crop_y}")
      img
    end
  end
end

On most images, this works fine. But once we throw an animated GIF into the mix:

Collective Idea - no_crop.gif

We see this:

Collective Idea - bad_crop.gif

So what happened? Turns out, when we crop the GIF, it steps through each frame and crops the frame, but not the canvas. There’s a quick trick to get this to work, though. Ultimately, we need to get the following arguments to ImageMagick’s convert:

-coalesce -repage 0x0 -crop 500x500+200+200 +repage

MiniMagick gives us methods for most of those just fine, but how do we get that +repage? Use MiniMagick::Image#<<. When we wrap our crop method, we end up with this:

class ImageUploader < CarrierWave::Uploader
  include CarrierWave::MiniMagick

  DEFAULT_IMAGE_DIMENSIONS = [500, 500].freeze

  def edit(scale: 1.0, angle: 0, crop_x: 0, crop_y: 0)
    manipulate! do |img|
      img.resize "#{img.width * scale.to_f}x#{img.height * scale.to_f}"
      img.rotate angle.to_i
      crop_w, crop_h = DEFAULT_IMAGE_DIMENSIONS
      img.coalesce
      img.repage("0x0")
      img.crop("#{crop_w}x#{crop_h}+#{crop_x}+#{crop_y}")
      img << "+repage"
      img
    end
  end
end

And finally:

Collective Idea - good_crop.gif

Photo of Joshua Kovach

Josh’s skills include web and mobile development and he enjoys developing APIs and Android Apps. He is also a mentor on HackHands, pairing with programmers working through coding issues.

Comments