DEV Community

Andrey Blinov
Andrey Blinov

Posted on

5

Open Graph image generation on the fly with Ruby

Prerequisites

At Wannadocs, we're generating Open Graph tags for each article to make it looks as attractive as possible when sharing on social media or messengers.

I didn't want to spend a lot of time on this feature, so I created a 1200x600px image, filled it with the color that a user specified as the primary color in the Knowledge Base theme, and added the title as white text.

Screenshot 2021-05-10 at 10.50.24 AM

It was operating like that for a whole year.

def create_og_image_preview(path, color: '#5168ec', message: )
unless File.exist?(path)
rgb = color.match(/#(..)(..)(..)/)[1..3].map{|x| x.hex}
im = Vips::Image.black(1200, 630).ifthenelse([0,0,0], rgb)
text = Vips::Image.text message, width: im.width, dpi: 400, font: 'sans bold'
text = text.gravity :centre, im.width, im.height
overlay = (text.new_from_image [255, 255, 255]).copy interpretation: :srgb
overlay = overlay.bandjoin text
im = im.composite overlay, :over
im.write_to_file path
end
end

A year later, I began to notice that large companies are moving in this direction. I was excited and wants to know how they do it.

For example:

dev.to

99a51d06-1010-4d35-b120-52a4fb635df6

github.com

caddy-geoip

I did a little investigation. Since the source code of the dev.to is open, I found out that Github and dev.to creating their previews in HTML and send them to a special service that renders previews through Headless Chromium. With the new knowledge, I decided to slightly improve our previews generating to keep it up with the smooth appearance and content.

Since we sell the self-hosted version of Wannadocs, we're not suited to using the third-party service here, so we decided to stay on "libvips" + Ruby.

Let's go!

Installation

First, you need to install all necessary packages on the server and build the "libvips" (Ansible playbook is doing this for me, so I tried to get just shell commands from there):

sudo apt-get install fonts-open-sans devscripts libfftw3-dev libmagickwand-dev libopenexr-dev liborc-0.4-0 gobject-introspection libgsf-1-dev libglib2.0-dev liborc-0.4-dev automake libtool swig gtk-doc-tools libpango1.0-dev -y
git clone https://github.com/libvips/libvips.git
cd libvips && ./autogen.sh && make && sudo make install
ldconfig -v

Here you can find the official manual on how to build it.

Pay attention to these packages:

fonts-open-sans – don't forget about fonts otherwise you will get that:

1_uP-3RRlrly7-K9_JLyuwFQ

And libpango1.0-dev – without this library Vips cannot properly write text.

Anatomy

Our previews are very similar to Github:

b707a690-5295-45ac-b0eb-bb7d5cf920d2

  • Gray text is a knowledge base name, in this case (Demo)
  • Big dark text is an article title
  • On the right side knowledge base logo
  • At the bottom 20px border is the primary color of the knowledge base

Code

I'm also a big fan of jQuery-style DSL with returning of the “self” object from a method and chaining.

# frozen_string_literal: true
require 'vips'
module Superdocs
module Opengraph
class Image
attr_reader :elements, :canvas, :borders, :canvas_color
def initialize(width, height, color: '#ffffff')
@elements = []
@canvas_color = color
@canvas = Vips::Image.black(width, height).ifthenelse([0, 0, 0], hex_to_rgb(color))
@borders = {}
end
def image(source, resize_with_ratio:)
im = source ? Vips::Image.new_from_buffer(source, '') : Vips::Image.black(1, 1).ifthenelse([0, 0, 0], hex_to_rgb(canvas_color))
if resize_with_ratio && source
width, height = resize_with_ratio
ratio = get_ratio(im, width, height, :min)
im = im.resize(ratio)
end
@elements << im
self
end
def text(message, width:, height:, dpi: 300, color: '#2f363d', font: 'Open Sans Bold')
im = Vips::Image.text(message, width: width, height: height, dpi: dpi, font: font)
im = im.new_from_image(hex_to_rgb(color)).copy(interpretation: :srgb).bandjoin(im)
@elements << im
self
end
def border_bottom(height, color: '#000000')
im = Vips::Image.black(canvas.width, height).ifthenelse([0, 0, 0], hex_to_rgb(color))
borders[:bottom] = im
self
end
def compose(filename)
result = yield(canvas, *elements)
@canvas = @canvas.composite(
elements + [borders[:bottom]], :over,
x: result[:x] + (borders[:bottom] ? [0] : []),
y: result[:y] + (borders[:bottom] ? [canvas.height - borders[:bottom].height] : [])
)
@canvas.write_to_file(filename)
end
private
def hex_to_rgb(input)
case input
when String
input.match(/#(..)(..)(..)/)[1..3].map(&:hex)
when Array
input
else
raise ArgumentError, "Unknown input #{input.inspect}"
end
end
# https://github.com/dariocravero/vips-process/blob/master/lib/vips-process/resize.rb#L109
def get_ratio(image, width, height, min_or_max = :min)
width_ratio = width.to_f / image.width
height_ratio = height.to_f / image.height
[width_ratio, height_ratio].send min_or_max
end
end
end
end
# logo = File.read('logo.png')
# Superdocs::Opengraph::Image.new(1200, 600)
# .image(logo, resize_with_ratio: [180, 180])
# .text('Demo', width: 900, height: 200, dpi: 200, color: '#959da5')
# .text('Custom CSS and Javascript', width: 800, height: 200, dpi: 300, color: '#2f363d')
# .border_bottom(20, color: '#4152bc')
# .compose(filename) do |canvas, logo, project_name, _note_title|
# {
# x: [canvas.width - logo.width - 100, 100, 100],
# y: [100, 100, project_name.height + 150]
# }
# end
# Github style OG image: https://cdn-images-1.medium.com/max/800/1*fDSZLzAtrs7xOasSULOISg.jpeg
view raw image.rb hosted with ❤ by GitHub

With DSL I'm trying to reduce the inconvenience of creating image layout with code and make this snippet more portable from project to project.

In Rails, I created a controller that serves up these images like this:

module Frontpage
class OpengraphController < ApplicationController
def image
note = project.notes.find(params[:id])
checksum = Digest::SHA1.hexdigest([note.title, project.primary_color, project.name, project.logo&.file&.id].join)
filename = "#{Rails.root}/public/uploads/og-image-#{checksum}.jpg"
unless File.exist?(filename)
Superdocs::Opengraph::Image.new(1200, 600)
.image(project.logo&.file&.read, resize_with_ratio: [180, 180])
.text(project.name, width: 900, height: 200, dpi: 200, color: project_name_color)
.text(note.title, width: 800, height: 200, dpi: 300, color: note_title_color)
.border_bottom(20, color: project.primary_color)
.compose(filename) do |canvas, logo, project_name, _note_title|
{
x: [canvas.width - logo.width - 100, 100, 100],
y: [100, 100, project_name.height + 150]
}
end
end
if stale?(etag: checksum)
send_data File.read(path), type: 'image/jpg', disposition: 'inline'
end
end
end
end

As a result, we get Open Graph images for all social networks without unnecessary dependencies on third-party services and very fast generation, unfortunately at the cost of the inconvenience of image layout.

Screenshot 2021-05-10 at 11.51.39 AM


Wannadocs - we make product knowledge bases for your customers, we have:

  • Cool editor with support for markdown and embedded elements (YouTube, CodePen, Gist, Figma, Airtable etc.)

  • Analytics by the article: views, likes, dislikes

  • Built-in full-text search

  • Templates and customization

  • SSL certificates in two clicks

  • Amazing OpenGraph images for your links 😍

Who has read to the end catch the promo code 10% discount: dz74cdq

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

If this post resonated with you, feel free to hit ❤️ or leave a quick comment to share your thoughts!

Okay