lib/thumbelina.ex

defmodule Thumbelina do
  @moduledoc """
    Image manipulation api.
  """
  alias Thumbelina.Image
  alias Thumbelina.Internal

  @type result :: {:ok, Image.t()} | {:error, String.t()}

  @spec open(String.t()) :: result()
  def open(path) do
    ext = Path.extname(path)

    with {:ok, binary} <- File.read(path),
         %Image{} = image <- Image.new(ext, binary) do
      {:ok, image}
    else
      err -> err
    end
  end

  @spec open_all!(String.t()) :: [Enumerable.t()]
  def open_all!(path) do
    path
    |> File.ls!()
    |> Stream.map(fn file -> Thumbelina.open(path <> "/" <> file) end)
  end

  @doc """
    If dealing with a large amount of files on disk and you wouldn't like
    to use `File.open/1` due to the number of open file descriptors and would like control
    over how many bytes are read into memory at a time. Defaults to 2 ** 16 bytes or ~65kb.
  """
  @spec stream_directory!(String.t(), pos_integer()) :: [Enumerable.t()]
  def stream_directory!(path, bytes \\ 65536) do
    path
    |> File.ls!()
    |> Stream.map(fn file -> File.stream!(path <> "/" <> file, [], bytes) end)
    |> Stream.map(fn file -> Stream.into(file, <<>>) end)
  end

  @spec resize(Image.t(), pos_integer(), pos_integer()) :: result()
  def resize(%Image{} = image, width, height) do
    Internal.resize(image.bytes, image.extension, width, height)
  end

  @spec crop(Image.t(), pos_integer(), pos_integer()) :: result()
  def crop(%Image{} = image, width, height) do
    Internal.crop(image.bytes, image.extension, width, height)
  end

  @spec thumbnail(Image.t(), pos_integer(), pos_integer()) :: result()
  def thumbnail(%Image{} = image, new_width, new_height) do
    Internal.thumbnail(image.bytes, image.extension, new_width, new_height)
  end

  @spec flip(Image.t(), :vertical | :horizontal) :: result()
  def flip(%Image{} = image, direction) do
    case direction do
      :vertical -> Internal.flip_vertical(image.bytes, image.extension)
      :horizontal -> Internal.flip_horizontal(image.bytes, image.extension)
      _ -> {:error, "Invalid direction for flip"}
    end
  end

  @spec rotate(Image.t(), pos_integer()) :: result()
  def rotate(%Image{} = image, angle) when angle > 0 and angle <= 360 do
    case angle do
      angle when angle in [90, 180, 270] ->
        Internal.rotate(image.bytes, image.extension, angle)

      angle when is_float(angle) or is_integer(angle) ->
        {angle, _} = Integer.parse("#{angle}")
        Internal.rotate(image.bytes, image.extension, angle)
    end
  end

  def rotate(%Image{}, _), do: {:error, "invalid rotation angle. Must be in range 1..360"}

  @spec blur(Image.t(), float()) :: result()
  def blur(%Image{} = image, sigma) do
    case sigma do
      sigma when is_integer(sigma) ->
        {sigma, _} = Float.parse("#{sigma}")
        Internal.blur(image.bytes, image.extension, sigma)

      sigma when is_float(sigma) ->
        Internal.blur(image.bytes, image.extension, sigma)

      _ ->
        {:error, "gausian blur must be a float"}
    end
  end

  @spec brighten(Image.t(), pos_integer()) :: result()
  def brighten(%Image{} = image, brightness) do
    case brightness do
      brightness when is_float(brightness) ->
        {brightness, _} = Integer.parse("#{brightness}")
        Internal.brighten(image.bytes, image.extension, brightness)

      brightness when is_integer(brightness) ->
        Internal.brighten(image.bytes, image.extension, brightness)

      _ ->
        {:error, "brightness must be an integer"}
    end
  end

  @spec greyscale(Image.t()) :: result()
  def greyscale(%Image{} = image), do: Internal.greyscale(image.bytes, image.extension)
end