How I built a URL to image generator over the weekend

joemasilotti

Joe Masilotti

Posted on August 31, 2020

How I built a URL to image generator over the weekend

I've grown really tired of manually creating social images for every single blog post. They take way too long to create and online tools always end up looking too generic. How many stock photos can I scroll through before they all start to look the same?

So I built Mugshot Bot. An automated, zero effort social image generator. You pass it a URL and it generates a perfectly sized, unique, beautiful social image.

Here's what they look like! The color and background pattern are randomized from a hand-tuned selection. The title and subtitle come directly from the HTML.

Example Mugshot Bot image

Overall approach

My goal is to design in HTML and CSS and then convert it to a PNG. This worked pretty well with some wkhtmlto* magic but there were a few hoops I had to jump through. Here's what I did.

Fetch the content

All of the content comes directly from the URL's HTML. So the first step is to fetch the website and parse the DOM. I'm using HTTParty and Nokogiri and then looking for specific markup.

body = HTTParty.get(@url).body
html = Nokogiri.parse(body)
title = html.at_css("meta[property='og:title']")
  .attr("content")
description = html.at_css("meta[property='og:description']")
  .attr("content")

Render and style the HTML

Now that we have the copy we can drop it into some HTML. In Rails we can render an arbitrary view and pass in some variables via ApplicationController#render.

mugshot = Mugshot.new(title: title, description: description)
rendered_html = ApplicationController.render(
  "mugshots/show",
  assigns: { title: title, description: description },
  formats: [:html],
)

The rendered HTML uses the default layout so we have all of the CSS and fonts normally added in <head>.

Convert to an image

Where the magic happens: wkhtmlto*. Or, as it is usually known, wkhtmltopdf. This library is bundled with a lesser known tool wkhtmltoimage that does exactly what we need.

If you have the library installed you can call directly into it with Open3. This works a bit better than backticks because you can handle stderr.

result, error = Open3.capture3(
  "wkhtmltoimage jpeg - -",
  stdin_data: rendered_html
)

The two dashes (- -) at the end of the command tell the tool to render from stdin and render to stdout. Open3 will write stdout to result and stderr to error.

Render from the controller

result is the actual image, as data. We can render this directly from the controller. Ideally, this would be uploaded to S3 and/or put behind a CDN.

def show
  # ...
  send_data(result, type: "image/jpeg", disposition: "inline")
end

What a weekend!

Thanks for reading, I hope you enjoyed how I built a little side project over the weekend.

If you give Mugshot Bot a try please let me know what you think in the comments! I'm open to feature requests, too.

💖 💪 🙅 🚩
joemasilotti
Joe Masilotti

Posted on August 31, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related