DIYTG - Do It Yourself Thumbnail Generation

an ai generated image featuring a cloud of potatoes and photos above a laptop.

I found myself needing a quick cheap origin server for a CDN that’s going to host and dynamically resize ~300gb of book covers. The server needs to take the width of the desired thumbnail and the isbn of the book, and return an image in the response.

An hours work in Elixir and I’ve got something I’m not afraid to throw in production. How cool is that?

All the code required

To generate the thumbnails on the fly we need a fast library to do it for us. Enter Vix, a great elixir extension that uses NIF to call into libvips for great performance™.

The first thing we do, lets generate yet another phoenix project.

$ mix phx.new thubz

Great project name as usual!

Then we add our one dependency

# mix.exs
defp deps do
    [
      ...
      {:vix, "0.26.0"}
    ]
end

Then we hijack the default page router

# router.ex
scope "/", ThubzWeb do
  pipe_through :browser

  get "/thumbs/:width/:isbn", PageController, :home
end

And finally we hijack the controller.

# page_controller.ex
defmodule ThubzWeb.PageController do
  use ThubzWeb, :controller
  alias Vix.Vips

  @root_path "/ftp_sync/images/Complete/"
  def home(conn, %{"isbn" => isbn, "width" => width}) do
    case path_from_isbn(isbn) do
      {:ok, path} ->
        width = String.to_integer(width)

        # From https://www.libvips.org/API/current/libvips-resample.html#vips-thumbnail
        # The output image will fit within a square of size width x width.
        # You can specify a separate height with the height option.
        # Set either width or height to a very large number to ignore that dimension.
        very_large_number = 4000

        {:ok, data} =
          path
          |> Vips.Operation.thumbnail!(width, height: very_large_number, size: :VIPS_SIZE_DOWN)
          |> Vips.Image.write_to_buffer(".jpg")

        conn
        |> put_resp_content_type("image/jpeg")
        |> send_resp(:ok, data)

      _ ->
        conn |> send_resp(404, "Not found")
    end
  end

  defp path_from_isbn(isbn) do
    # The images are stored in subfolders based on the first 8 digits of the ISBN
    sub_folder = isbn |> String.slice(0..7)

    # And they're all jpegs
    path = (Path.join([@root_path, sub_folder, isbn]) <> ".jpg") |> dbg()

    # But a lot of those files don't exist
    if File.exists?(path) do
      {:ok, path}
    else
      {:error, :not_found}
    end
  end
end

Just setting the width for some reason set the hight, so that threw me off for a second. The libvips docs suggested setting height to a very large number, and that did fix it.

And that’s it!