Generate thumbnails in Elixir
We are going to use excelent library exmagick which is a wrapper around graphicsmagick - in order to use it we have to install dependencies. On MacOs we can use:
$ brew install libtool
$ brew install graphicsmagick
Our simple solution consist of two modules:
- ExImage.ImageSize - it's main responsibility is to calculate size for scaled image
- ExImage - it's responsilbe for generating resized image
Main reason to split this two functionalities into separate modules is better testability. When calculating size for thumbnails we have to consider few cases which is described visually. Green is image that we want to scale and rectangle with blue border is thumbnail we want to generate.
In order to calculate new sizes we have to get ratio for width and height and from those we pick width ration if it is smaller or equal to height ration otherwise we use height ratio. With calculated ration we can get new size which is old size multiplied by ratio.
defmodule ExImage.ImageSize do
@enforce_keys [:width, :height]
defstruct [:width, :height]
@type t :: %__MODULE__{}
@spec scale(t, t) :: t
def scale(image_size, preferred_size) do
ratio_width = ratio(image_size.width, preferred_size.width)
ratio_height = ratio(image_size.height, preferred_size.height)
ratio = if ratio_width <= ratio_height, do: ratio_width, else: ratio_height
%__MODULE__{width: trunc(image_size.width * ratio), height: trunc(image_size.height * ratio)}
end
defp ratio(original, preferred), do: preferred / original
end
To fully test it we have to cover each case descibed visually.
defmodule ExImage.ImageSizeTest do
use ExUnit.Case
alias ExImage.ImageSize
describe "scale/2" do
test "width and height is equal" do
original = %ImageSize{width: 400, height: 400}
preferred = %ImageSize{width: 400, height: 400}
scaled = ImageSize.scale(original, preferred)
assert %ImageSize{width: 400, height: 400} == scaled
end
test "height is equal and width is greater" do
original = %ImageSize{width: 800, height: 200}
preferred = %ImageSize{width: 200, height: 200}
scaled = ImageSize.scale(original, preferred)
assert %ImageSize{width: 200, height: 50} == scaled
end
test "height is smaller and width is greater" do
original = %ImageSize{width: 800, height: 200}
preferred = %ImageSize{width: 400, height: 400}
scaled = ImageSize.scale(original, preferred)
assert %ImageSize{width: 400, height: 100} == scaled
end
test "height is greater and width is equal" do
original = %ImageSize{width: 400, height: 800}
preferred = %ImageSize{width: 400, height: 400}
scaled = ImageSize.scale(original, preferred)
assert %ImageSize{width: 200, height: 400} == scaled
end
test "height is greater and width is greater" do
original = %ImageSize{width: 600, height: 800}
preferred = %ImageSize{width: 400, height: 400}
scaled = ImageSize.scale(original, preferred)
assert %ImageSize{width: 300, height: 400} == scaled
end
test "height is greater and width is smaller" do
original = %ImageSize{width: 200, height: 800}
preferred = %ImageSize{width: 400, height: 400}
scaled = ImageSize.scale(original, preferred)
assert %ImageSize{width: 100, height: 400} == scaled
end
test "height is equal and width is smaller" do
original = %ImageSize{width: 200, height: 400}
preferred = %ImageSize{width: 400, height: 400}
scaled = ImageSize.scale(original, preferred)
assert %ImageSize{width: 200, height: 400} == scaled
end
test "height is smaller and width is equal" do
original = %ImageSize{width: 400, height: 200}
preferred = %ImageSize{width: 400, height: 100}
scaled = ImageSize.scale(original, preferred)
assert %ImageSize{width: 200, height: 100} == scaled
end
test "height is smaller and width is smaller" do
original = %ImageSize{width: 400, height: 200}
preferred = %ImageSize{width: 200, height: 100}
scaled = ImageSize.scale(original, preferred)
assert %ImageSize{width: 200, height: 100} == scaled
end
end
end
Generating thumbnails is quite easy with help of exmagick. We have to load image, get it's size, calculate new size using ExImage.ImageSize.scale(image_size, preffered_size), generate new image with new size.
defmodule ExImage do
alias ExImage.ImageSize
require Logger
@spec thumbnail(binary(), ImageSize.t()) :: {:ok, Path.t()} | {:error, String.t()}
def thumbnail(image, preffered_size) do
with {:ok, handler} <- ExMagick.init(),
{:ok, loaded_image} <- ExMagick.image_load(handler, {:blob, image}),
{:ok, image_size} <- ExMagick.size(loaded_image),
thumbnail_size <- ImageSize.scale(image_size, preffered_size),
{:ok, thumbnail} <-
ExMagick.thumb(loaded_image, thumbnail_size.width, thumbnail_size.height),
{:ok, resized_image} <- ExMagick.image_dump(thumbnail) do
{:ok, resized_image}
else
{:error, message} = error ->
Logger.error(message)
error
end
catch
error ->
Logger.error(error)
{:error, error.message}
end
end
To check if we resized image with correct ratio we can use ExMagic again and assert size of generated thumbnail.
defmodule ExImageTest do
use ExUnit.Case
alias ExImage.ImageSize
describe "thumbnail/2" do
test "generates thumbnail" do
image = File.read!("test/fixtures/image.jpeg")
{:ok, thumbnail} = ExImage.thumbnail(image, %ImageSize{width: 400, height: 400})
with {:ok, handler} <- ExMagick.init(),
{:ok, loaded_image} <- ExMagick.image_load(handler, {:blob, thumbnail}),
{:ok, image_size} <- ExMagick.size(loaded_image) do
assert image_size == %{width: 400, height: 266}
end
end
test "returns error for incorrect file" do
{:error, message} = ExImage.thumbnail("image", %ImageSize{width: 400, height: 400})
assert message == "Unable to deduce image format"
end
end
end
You can view full code at: https://github.com/elpikel/ex_image