Let's Build a Proof of Concept Dynamic Image Generator with Elixir & Phoenix
Tags: phoenix, elixir, imagemagick
Published: 2021-01-23 17:23:08

## 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](https://www.phoenixframework.org/) 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:

![Example quote generation](https://dev-to-uploads.s3.amazonaws.com/i/hu2yw0w9jwyp3v8vha4f.gif)

## 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 ](https://hexdocs.pm/phoenix/installation.html) 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.

```bash
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:

```elixir
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:

```elixir
# 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:

```elixir
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.

```elixir
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](https://imagemagick.org/index.php) via [`System.cmd/2`](https://hexdocs.pm/elixir/System.html#cmd/3). Installing ImageMagick on macOS is just a case of `brew install imagemagick`, for other OSs check out their [downloads page](https://imagemagick.org/script/download.php).

`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.

```elixir
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:

1. 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.
2. Similarly, the value of `attribution` is passed to the second label.
3. `png:-` tells ImageMagick to output the generated image data as a PNG to STDOUT. This is what's collected in `output`.

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:

```elixir
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!

![Example quote generation](https://dev-to-uploads.s3.amazonaws.com/i/hu2yw0w9jwyp3v8vha4f.gif)

Thanks for reading! You'll find the source code [here](https://github.com/elliotekj/image-gen-proof-of-concept) if you need it and if you have any questions, feel free to ask below 💬