lib/vix/operator.ex

defmodule Vix.Operator do
  @moduledoc """
  Provides implementation of basic math operators for Vix Image.

  Useful to improve the readability of complex Image processing
  operation pipelines.

  ```elixir
  def foo do
    use Vix.Operator

    black = Operation.black!(100, 100, bands: 3)
    white = Operation.invert!(black)

    white == white + black
    grey = black + 125  # same as [125]
    grey = black + [125] # same as [125, 125, 125], since the image contains 3 bands
    grey = black + [125, 125, 125]
  end
  ```
  """

  alias Vix.Vips.Image
  alias Vix.Vips.Operation

  import Kernel, except: [+: 2, -: 2, *: 2, /: 2]

  @doc false
  defmacro __using__(_opts) do
    quote do
      import Kernel, except: [+: 2, -: 2, *: 2, /: 2]
      import Vix.Operator
    end
  end

  @doc """
  Perform addition operation of an image and a number, an array or
  an another image.

  * if argument is a number or a list of only one number, then the same number is used
    for all image bands for the operation. Example `img + 255`, `img + [255]`
  * if array size matches image bands, then each array
    element is added to respective band. Example `red = Operation.black!(100, 100, bands: 3) + [125, 0, 0]`
  * if array size is more than image bands and image only contains one band
    then the output will be a multi band image with each array element mapping
    to one band
  * if argument is an image, then `Vix.Vips.Operation.add!/2` operation will
    be performed

  When none of the argument is an image then delegates to `Kernel.+/2`
  """
  @spec Image.t() + Image.t() :: Image.t()
  @spec Image.t() + number() :: Image.t()
  @spec number() + Image.t() :: Image.t()
  @spec Image.t() + [number()] :: Image.t()
  @spec [number()] + Image.t() :: Image.t()
  @spec number() + number() :: number()
  def a + b do
    add(a, b)
  end

  @doc """
  Perform multiplication operation of an image and a number, an array or
  an another image.

  * if argument is a number or a list of only one number, then the same number is used
    for all image bands for the operation. Example `img * 2`, `img + [2]`
  * if array size matches image bands, then each array
    element is added to respective band
  * if array size is more than image bands and image only contains one band
    then the output will be a multi band image with each array element mapping
    to one band
  * if argument is an image, then `Vix.Vips.Operation.multiply!/2` operation will
    be performed

  When none of the argument is an image then delegates to `Kernel.*/2`
  """
  @spec Image.t() * Image.t() :: Image.t()
  @spec Image.t() * number() :: Image.t()
  @spec number() * Image.t() :: Image.t()
  @spec Image.t() * [number()] :: Image.t()
  @spec [number()] * Image.t() :: Image.t()
  @spec number() * number() :: number()
  def a * b do
    mul(a, b)
  end

  @doc """
  Perform subtraction operation of an image and a number, an array or
  an another image.

  * if argument is a number or a list of only one number, then the same number is used
    for all image bands for the operation. Example `img - 125`, `img - [125]`
  * if array size matches image bands, then each array
    element is added to respective band
  * if array size is more than image bands and image only contains one band
    then the output will be a multi band image with each array element mapping
    to one band
  * if argument is an image, then `Vix.Vips.Operation.subtract!/2` operation will
    be performed

  When none of the argument is an image then delegates to `Kernel.-/2`
  """
  @spec Image.t() - Image.t() :: Image.t()
  @spec Image.t() - number() :: Image.t()
  @spec number() - Image.t() :: Image.t()
  @spec Image.t() - [number()] :: Image.t()
  @spec [number()] - Image.t() :: Image.t()
  @spec number() - number() :: number()
  def a - b do
    sub(a, b)
  end

  @doc """
  Perform division operation of an image and a number, an array or
  an another image.

  * if argument is a number or a list of only one number, then the same number is used
    for all image bands for the operation. Example `img / 2`, `img / [2]`
  * if array size matches image bands, then each array
    element is added to respective band
  * if array size is more than image bands and image only contains one band
    then the output will be a multi band image with each array element mapping
    to one band
  * if argument is an image, then `Vix.Vips.Operation.divide!/2` operation will
    be performed

  When none of the argument is an image then delegates to `Kernel.//2`
  """
  @spec Image.t() / Image.t() :: Image.t()
  @spec Image.t() / number() :: Image.t()
  @spec number() / Image.t() :: Image.t()
  @spec Image.t() / [number()] :: Image.t()
  @spec [number()] / Image.t() :: Image.t()
  @spec number() / number() :: number()
  def a / b do
    divide(a, b)
  end

  defmacrop when_number_list(arg, do: block) do
    quote do
      if Enum.all?(unquote(arg), &is_number/1) do
        unquote(block)
      else
        raise ArgumentError, "list elements must be a number, got: #{inspect(unquote(arg))}"
      end
    end
  end

  # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
  defp add(a, b) do
    cond do
      is_image(a) && is_image(b) ->
        Operation.add!(a, b)

      is_image(a) && is_number(b) ->
        add(a, [b])

      is_number(a) && is_image(b) ->
        add(b, [a])

      is_list(a) && is_image(b) ->
        add(b, a)

      is_image(a) && is_list(b) ->
        when_number_list b do
          Operation.linear!(a, [1], b)
        end

      true ->
        Kernel.+(a, b)
    end
  end

  # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
  defp mul(a, b) do
    cond do
      is_image(a) && is_image(b) ->
        Operation.multiply!(a, b)

      is_image(a) && is_number(b) ->
        mul(a, [b])

      is_number(a) && is_image(b) ->
        mul(b, [a])

      is_list(a) && is_image(b) ->
        mul(b, a)

      is_image(a) && is_list(b) ->
        when_number_list b do
          Operation.linear!(a, b, [0])
        end

      true ->
        Kernel.*(a, b)
    end
  end

  # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
  defp sub(a, b) do
    cond do
      is_image(a) && is_image(b) ->
        Operation.subtract!(a, b)

      is_image(a) && is_number(b) ->
        sub(a, [b])

      is_number(a) && is_image(b) ->
        sub([a], b)

      is_list(a) && is_image(b) ->
        when_number_list a do
          # a - b = (b * -1) + a
          Operation.linear!(b, [-1], a)
        end

      is_image(a) && is_list(b) ->
        when_number_list b do
          Operation.linear!(a, [1], Enum.map(b, &(-&1)))
        end

      true ->
        Kernel.-(a, b)
    end
  end

  # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
  defp divide(a, b) do
    cond do
      is_image(a) && is_image(b) ->
        Operation.divide!(a, b)

      is_image(a) && is_number(b) ->
        divide(a, [b])

      is_number(a) && is_image(b) ->
        divide([a], b)

      is_list(a) && is_image(b) ->
        when_number_list a do
          # a / b = (b^-1) * a = (1 / b) * a
          b
          |> Operation.math2_const!(:VIPS_OPERATION_MATH2_POW, [-1])
          |> Operation.linear!(a, [0])
        end

      is_image(a) && is_list(b) ->
        when_number_list b do
          Operation.linear!(a, Enum.map(b, &(1 / &1)), [0])
        end

      true ->
        Kernel./(a, b)
    end
  end

  defp is_image(%Image{}), do: true
  defp is_image(_), do: false
end