Watermarking Images with Active Storage

Watermarking image attachments with Active Storage can be a little tricky so here’s a quick write up of what I did to get it working. 

The TL;DR
Use a variant and pass a draw command to mogrify. Like this.

The non-TL;DR

Behind the scenes Active Storage 5.2  is using minimagick for image transformations aka variants.  I am specifically calling out 5.2 because from the looks of the code on Github Rails 6 will have something different going on. 

Using a variant allows you to do things like resize, crop, monochrome, and a lot more. For example, this is how you would render a black and white resized image in your view:

<%= image_tag @post.image.variant(resize: '47x47', monochrome: true) %>

How does this work? The Rails Guide for Active Storage says this:

To create a variation of the image, call variant on the Blob. You can pass any MiniMagick supported transformation to the method.

To enable variants, add mini_magick to your Gemfile:

https://guides.rubyonrails.org/active_storage_overview.html#transforming-images

Sweet! MiniMagick is a wrapper around the infamous ImageMagick library. Basically just about anything you can do with ImageMagick directly you should be able to do from a Ruby API provided by MiniMagick… and according to the Rails docs anything minimagick can do we should be able to pass through the variant…  

We will get to that soon.  For now let’s talk about how you’d watermark an image using ONLY ImageMagick.

ImageMagick has a ton of utilities and according to ImageMagick’s docs watermarking is done using the composite command along with the -watermark option.  The ImageMagick docs even give a pretty straight forward example:

composite -watermark 30% -gravity south \
            wmark_image.png  logo.jpg    wmark_watermark.jpg

convert happens to be one of the supported tools by minimagick so we should be able to get this done… right?

Unfortunately after looking at the source code for Active Support it appears that this is straight up impossible. ActiveStorage::Variation#transform does use a MiniMagick::Image object but it also contains a hardcoded a call to mogrify.

mogrify is another ImageMagick command that is useful for a ton of different things like resizing, cropping etc but it does not really offer first class support for watermarking images.  It’s not impossible (!!) but it’s just not as fun.

OK, since we are stuck using mogrify how are we going to get this done? Luckily mogrify supports an option called draw. According to the docsdraw is designed to:

…annotate or decorate an image with one or more graphic primitives. The primitives include shapes, text, transformations, and pixel operations.

https://imagemagick.org/script/command-line-options.php#draw

One of the shape primitives is an image ! 

How would this look if we were using mogrify directly from the command line? Here’s an example that will plop a watermark directly in the center of your image:

mogrify -gravity center \
        -draw 'image Over 0,0 0,0 "watermark.png"' \
        unwatermarked_image.jpg

And finally, how would we use this to watermark an image in our app?  Like this:

image_tag @post.image.variant(
  combine_options: {
    gravity: 'center',
    draw: 'image Over 0,0 0,0 "'+Post::WATERMARK_PATH.to_s+'"'
  }
)

That’s basically it.  I’ve published source code for a demo app that does exactly this on GitHub — check it out here.