Let's Build a Proof of Concept Dynamic Image Generator with Elixir & Phoenix
Elliot Jackson
Posted on January 23, 2021
The problem
GIFs are great but sometimes it could be nice to have a little bit of dynamism on a site without having a GIF changing in a loop and distracting the user or just without adding the file size of a GIF to the page load.
The solution
What we're going to build is a small Phoenix web app that has a single endpoint, /quote.png
, that will return an image of a random quote and attribution from an array of user defined quotes. Demo:
The how
The first question is how we'll get the list of quotes and attributions from the user. In a "real" app this would probably be backed by a database and a nice UI for adding quotes. Remember that this is only a proof of concept though so we'll go with the simplest method I could think of: putting them in the query string.
The syntax for an array in a query string is key[]=value&key[]=value2
so if we create an array of quotes and an array of attributions we'll end up with something like this (q
for quotes and a
for attributions to help keep a long URL a bit shorter):
/quote.png?q[]=It%27s%20awesome&a[]=Mum&q[]=Pretty%20fun&a[]=Bob&q[]=Great%20stuff&a[]=Alice&q[]=Average%20at%20best&a[]=Theo
Now we know how the data is going to be passed into our endpoint, let's start doing something with it!
💡 First you'll need to install Phoenix. The project maintains a very good installation page so I won't repeat its contents. You can skip the node.js and Postgres sections if you don't already have them installed, we won't be needing them.
Once you have Phoenix installed, create a new project and install the dependencies when prompted. We won't need a DB or Webpack so we can skip those with flags.
mix phx.new image_gen_poc --no-ecto --no-webpack
Open the project in your editor of choice and let's get to it!
We'll start be defining the endpoint. Go to image_gen_poc/lib/image_gen_poc_web/router.ex
and update line 19 to the following:
get "/quote.png", PageController, :index
We can see above that the new /quote.png
endpoint is handled by the index
function in the PageController
so that's where we'll go next.
First things first we need to extract our quotes and attributions from the URL which can be done very easily with Elixir's pattern matching:
# image_gen_poc/lib/image_gen_poc_web/controllers/page_controller.ex
defmodule ImageGenPocWeb.PageController do
use ImageGenPocWeb, :controller
def index(conn, %{"q" => quotes, "a" => attributions}) do
# ...
end
end
The array of quotes (q
) is assigned to quotes
and the array of attributions (a
) is assigned to attributions
. Before we go any further let's make sure this is all working so far. We'll require Logger
then update our index
function as follows:
defmodule ImageGenPocWeb.PageController do
use ImageGenPocWeb, :controller
require Logger
def index(conn, %{"q" => quotes, "a" => attributions}) do
Logger.info(quotes)
Logger.info(attributions)
render(conn, "index.html")
end
end
cd
into your project directory and start the server with mix phx.server
. Now go to your browser and paste the following URL:
http://localhost:4000/quote.png?q[]=It%27s%20awesome&a[]=Mum&q[]=Pretty%20fun&a[]=Bob&q[]=Great%20stuff&a[]=Alice&q[]=Average%20at%20best&a[]=Theo
Coming back to your terminal you should see something like this:
[info] GET /quote.png
[debug] Processing with ImageGenPocWeb.PageController.index/2
Parameters: %{"a" => ["Mum", "Bob", "Alice", "Theo"], "q" => ["It's awesome", "Pretty fun", "Great stuff", "Average at best"]}
Pipelines: [:browser]
[info] It's awesomePretty funGreat stuffAverage at best
[info] MumBobAliceTheo
[info] Sent 200 in 1ms
It's working, great!
Next we need to get a quote and its matching attribution.
def index(conn, %{"q" => quotes, "a" => attributions}) do
random_index = Enum.random(0..(length(quotes) - 1))
quote = Enum.at(quotes, random_index)
attribution = Enum.at(attributions, random_index)
Logger.info(quote)
Logger.info(attribution)
render(conn, "index.html")
end
If we were only dealing with one list we could have just used Enum.random/1
directly on it but as we need to get a value from the same random index in 2 sperate lists we're just using it to generate an index. We can then use Enum.at/2
to get a value from the same random index in 2 different lists.
Going back to your browser and refreshing the page should now output something like this in your terminal:
[info] GET /quote.png
[debug] Processing with ImageGenPocWeb.PageController.index/2
Parameters: %{"a" => ["Mum", "Bob", "Alice", "Theo"], "q" => ["It's awesome", "Pretty fun", "Great stuff", "Average at best"]}
Pipelines: [:browser]
[info] Great stuff
[info] Alice
[info] Sent 200 in 3ms
The quote and attribution match so the next job is to generate the image! We're going to hand the work involved in this off to ImageMagick via System.cmd/2
. Installing ImageMagick on macOS is just a case of brew install imagemagick
, for other OSs check out their downloads page.
System.cmd/2
returns a tuple containing the collected result of the command and the exit status code (0
means the command was successful) which we'll pattern match.
def index(conn, %{"q" => quotes, "a" => attributions}) do
random_index = Enum.random(0..(length(quotes) - 1))
quote = Enum.at(quotes, random_index)
attribution = Enum.at(attributions, random_index)
{output, 0} =
System.cmd("convert", [
"-background",
"#333333",
"-fill",
"white",
"-font",
"Helvetica",
"-pointsize",
"56",
"label:#{quote}", # [1]
"-fill",
"grey",
"-font",
"Helvetica-Oblique",
"-pointsize",
"32",
"label:- #{attribution}", #[2]
"-append",
"-background",
"#333333",
"-gravity",
"center",
"-extent",
"800x400",
"png:-" # [3]
])
render(conn, "index.html")
end
There's a lot of ImageMagick config going on there and we're going to ignore most of it as this isn't really an ImageMagick tutorial. I've numbered the lines that are of interest to us:
- The first label is passed the value of
quote
, the quote will be escaped automatically so we don't have to worry about it containing spaces or anything. - Similarly, the value of
attribution
is passed to the second label. -
png:-
tells ImageMagick to output the generated image data as a PNG to STDOUT. This is what's collected inoutput
.
We now have the image data in output
, all that's left to do is return it to the client. Replace render/2
with the following:
def index(conn, %{"q" => quotes, "a" => attributions}) do
# ...
conn
|> put_resp_content_type("image/png")
|> send_resp(200, output)
end
put_resp_content_type/2
assigns the appropriate Content-Type
header for the PNG we'll be returning, then we send the response back with a status code of 200 and the image data.
If you now go back to your browser and refresh it you should see a quotation image like the one at the start of this article generated. Keep refreshing and it should change randomly!
Thanks for reading! You'll find the source code here if you need it and if you have any questions, feel free to ask below 💬
Posted on January 23, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 23, 2021