All Google queries for watermarking an image with ActiveStorage currently point to this great, thorough result from 2018. ActiveStorage has changed a lot in the past three years, and change_options is no longer the going syntax as of 2021.

It can be much simpler to generate primitive watermarks with ActiveStorage 6.1. The basic concept remains the same: you pass imagemagick command line options to the variant method of your attachment. Given the wealth of options that imagemagick provides, it's a very flexible way to make a lot of possible things happen to an image.

Given a model called Feature that includes a has_one_attached :screenshot, this incantation will emboss the words "" in the lower-right corner of an image:

feature.screenshot.variant(gravity: "Southeast", pointsize: "30", fill: "#999", draw: "text 0,0 ''").processed.url

It uses two key options: "text" and "gravity" from imagemagick.

So an image like this:

Has this variant generated:

This option is best combined with the Rails' much appreciated new option to store which variants have been previously generated in the database, and reuse those instead of regenerating (+ reuploading) a new image on every call to the variant method.

# In config/application.rb
# Store in the local DB the URL to an already-generated variant and use that when available
config.active_storage.track_variants = true

linkExtra credit: To avoid ending up with an N+1 when loading variants

Since all this ActiveStorage variant business is relatively new as of mid-2021, work is still ongoing to ensure that variants can be preloaded to avoid an N+1 query situation when showing many images on a page. This PR looks set to offer new methods that can be utilized to eager-load an attachment and its variants. In the meantime, the following patch can be added to config/initializers to allow the possibility of using variant records when they have been preloaded:

require "active_storage/variant_with_record"

# Patch in
module EagerLoadVariant
  def record
    @record ||= if blob.variant_records.loaded?
      blob.variant_records.detect { |v| v.variation_digest == variation.digest }
      blob.variant_records.find_by(variation_digest: variation.digest)

raise("Has the PR for this been merged yet?") unless Rails.version =~ /6\.1/

Of course, if you are looking to show the public version of these watermarked images, the path to ensuring the resource is eager-loaded won't be mistaken for simple:

Feature.includes(screenshot_attachment: { blob: { variant_records: { image_attachment: :blob }}}

The outer screenshot_attachment is the name of the attachment added to Feature. Variant records are recorded by blob, so we must look up both of those. The actual key used by the variant image is nestled even further down, in the variant_record's own has_one_attached :image association, which holds yet another blob 😅. It looks like the incoming PR doesn't simplify this possible variant lookup, which makes me wonder if there is supposed to be an easier way to load the public key used by rails_public_blob_url. Feel free to drop a line in the comments if you discover a better way to ensure no N+1 looking up the public URL of a generated variant.