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:
We see this:
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:
Comments