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 theBlob
. You can pass any MiniMagick supported transformation to the method.To enable variants, add
https://guides.rubyonrails.org/active_storage_overview.html#transforming-imagesmini_magick
to yourGemfile
:
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 docs, draw
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.