Imagine an app for live streaming sports games (ESPN/Netflix/Twitch app).

On the homepage of the app you can see a list of sports games.

Each game has a preview image (thumbnail).

The thumnail should be composed of:

  • a stadium background (different stadiums for different sports)
  • home_team and guest_team logos
  • game starts_at text
  • game id

Right now the thumbnail is not one solid image.

It is a composite of images overlaying each other with HTML+CSS:


Now we want to generate an actual image thumbnail for each game and store it in the database.

This way we can utilize the generated image anywhere outside of our app (share on social, print posters, etc.)

To automatically generate and image we will use Rmagick.

To store and attach the image to a record, we will use ActiveStorage.

Rmagick is a Ruby library for interacting with Imagemagick - a powerful open source library for scripting images.

πŸš€ Final image that we will create: Final image

πŸ“„ Images used:


stadium image

Team 1 logo:

team 1 image

Team 2 logo:

team 1 image

⚠️ Before continuing, find a way to install rmagick on your machine. ⚠️

The below might not be enough:

bundle add rmagick

1. Generate new image

For this, we can use the method new.

πŸ“– rmagick#new docs

rails c
require 'RMagick'
include Magick
FileUtils.mkdir_p 'app/assets/images/generator'

# width, height
image =, 64) do |img|
  img.background_color = 'red'

image ='canvas:#ff5abb') do |img|
  img.size = '700x700'





2. Overlay image above image

For this, we will use the method composite.

πŸ“– rmagick#composite (image overlay) docs

rails c
require 'RMagick'
include Magick

# read existing image
image_path = Rails.root.join('app/assets/images/boys_basketball.png')
image =
width = image.columns
height = image.rows
# image.write("app/assets/images/generator/image-basic.png")

# create gradient of the same size as above image
gradient ='gradient:rgba(0,0,0,0.7)-rgba(0,0,0,0.0)') do |img|
  img.size = "#{width}x#{height}"
# gradient.write("app/assets/images/generator/gradient.png")

# overlay gradient over image
image = image.composite(gradient, 0, 0, OverCompositeOp)
# image.write("app/assets/images/generator/image-with-gradient.png")

# read another image, overlay image2 over image
image2_path = Rails.root.join('app/assets/images/Kashmere-Fighting-Rams.png')
image2 =
image = image.composite(image2, CenterGravity, -200, 0, OverCompositeOp)



notice that the top is darker than the bottom;

notice the avatar 200px to the left from the center;


So, for composite you specify:

  • main image
  • image that will be added
  • gravity
  • move left/right by X pixels from gravity
  • move down/up by Y pixels from gravity
  • composite operator

All available gravity options:


3. Overlay text above image

For this, we will use the methods annotate with Draw:

rails c
require 'RMagick'
include Magick

# create a canvas to write on
image =, 256) do |img|
  img.background_color = '#FFDD00'

text =
text.fill = '#0057B7'
text.pointsize = 48
text.gravity = CenterGravity
# 50px up Y axis = "-pt-5 
text.annotate(image, 0, 0, 0, -50, 'Glory to Ukraine')
text.gravity = SouthEastGravity
text.pointsize = 16
# 5px padding X (left) and Y (top) = "pl-5 pt-5"
text.annotate(image, 0, 0, 5, 5, 'by @yarotheslav')




Consider that the padding is calculate from gravity center πŸ™€

annotate also allows to assign text params in a block:

input_text = 'Glory to Ukraine'
# 0, 0, 0, 50 = width, height, x, y
text.annotate(image, 0, 0, 0, 50, input_text) do |txt|
  txt.pointsize = 48
  # txt.font_weight = BoldWeight
  txt.font_weight = NormalWeight
  txt.fill = '#ff5abb'
  txt.gravity = CenterGravity

4. Scale (resize) image


rails c
require 'RMagick'
include Magick

image_path = Rails.root.join('app/assets/images/boys_basketball.png')
image =
# => PNG 3335x2500 3335x2500+0+0 DirectClass 8-bit 825kb
smaller_image = image.scale(0.5)
# => PNG 3335x2500=>1668x1250 1668x1250+0+0 DirectClass 8-bit
larger_image = image.scale(1.75)
# => PNG 3335x2500=>5836x4375 5836x4375+0+0 DirectClass 8-bit
px_scaled_image = image.scale(300, 100)
# => PNG 3335x2500=>300x200 300x200+0+0 DirectClass 8-bit


disproportionally scaled image:


proportionally scaled image with calculation:

image_path = Rails.root.join('app/assets/images/boys_basketball.png')
image =

# calculation
width = image.columns
height = image.rows
new_width = 100
new_height = height*new_width/width
# => 74

proportionally_scaled_image = image.scale(new_width, new_height)

Actually, you don’t need to do the above calculation if you use resize_to_fit.

Just specify one aspect ration based on which you want to scale the other one:

image.resize_to_fit(0, 75)
# => PNG 3335x2500=>100x75
image.resize_to_fit(100, 0)
# => PNG 3335x2500=>100x75


5. Crop image

rmagicl#crop docs

#crop example

rails c
require 'RMagick'
include Magick

image_path = Rails.root.join('app/assets/images/boys_basketball.png')
image =

# initial image was too big. scale it to something standard
image = image.resize_to_fit(1668, 0)
# central crop
central_focus_crop = image.crop(CenterGravity, 800, 418, true)

The true in the end is very important, as it forces further manipulations to be based on the new size.

The image was very big initially, so we scaled it down.

Next, we focused on the center of the image and cropped 800px width and 418px height (the ideal twitter size).

This kind of crop can help you standartize cropping images while always keeping focus on the center:


6. Final. The real world case:

bin/rails g scaffold game starts_at:datetime sport home_team visiting_team
bin/rails active_storage:install
bin/rails db:migrate
# app/services/generate_thumbnail_service.rb
# event = Event.create(title: 'Madison Square Garden', description: 'VS')
class GenerateThumbnailService
  require 'RMagick'
  include Magick

  attr_reader :event

  def initialize(event)
    @event = event


  def call
    filename = create_image(event)
    # find & attach generated tmp image
    image_file_io ="app/assets/images/generator/#{filename}.png")
    event.thumbnail.attach(io: image_file_io, filename:, content_type: 'image/png')
    # delete tmp file


  def create_image(event)
    image_path = Rails.root.join('app/assets/images/boys_basketball.png')
    image =
    image = image.resize_to_fit(1000, 0)
    image = image.crop(CenterGravity, 800, 418, true)

    hero1_path = Rails.root.join('app/assets/images/Kashmere-Fighting-Rams.png')
    hero1 =
    image = image.composite(hero1, CenterGravity, -200, 0, OverCompositeOp)

    hero2_path = Rails.root.join('app/assets/images/Bellaire-Cardinals.png')
    hero2 =
    image = image.composite(hero2, CenterGravity, 200, 0, OverCompositeOp)

    text =

    input_text = event.title.truncate(50)
    text.annotate(image, 0, 0, 0, 10, input_text) do |txt|
      txt.pointsize = 48
      txt.font_weight = BoldWeight
      txt.fill = '#ff5abb'
      txt.gravity = NorthGravity

    input_text = event.description
    text.annotate(image, 0, 0, 0, 0, input_text) do |txt|
      txt.pointsize = 32
      txt.font_weight = NormalWeight
      txt.fill = 'white'
      txt.gravity = CenterGravity

    input_text = event.created_at.to_s
    text.annotate(image, 0, 0, 5, 5, input_text) do |txt|
      txt.pointsize = 32
      txt.font_weight = NormalWeight
      txt.fill = 'white'
      txt.gravity = SouthWestGravity

    input_text =
    text.annotate(image, 0, 0, 5, 5, input_text) do |txt|
      txt.pointsize = 32
      txt.font_weight = NormalWeight
      txt.fill = 'white'
      txt.gravity = NorthWestGravity

    filename = [event.model_name.human,].join.downcase
    FileUtils.mkdir_p 'app/assets/images/generator'

When an event is created, generate and attach the thumbnail:

# app/models/event.rb
class Event < ApplicationRecord
  has_one_attached :thumbnail, dependent: :destroy

  after_create do

Display the generated image in a view:

# app/views/events/_event.html.erb
<% if event.thumbnail.attached? %>
  <%= image_tag event.thumbnail %>
  <%#= event.thumbnail.blob.key %>
<% end %>



Other rmagick topics worth learning (that are not covered here):

  • transperency (pixel overlay strategies)
  • black & white
  • write generated images directly to AWS S3

Useful resources: