lib/image/options/write.ex

defmodule Image.Options.Write do
  @moduledoc """
  Options and option validation for `Image.write/3`.

  """

  # Map the keyword option to the
  # Vix option.

  alias Image.Color
  import Color, only: [is_inbuilt_profile: 1, is_color: 1]

  @typedoc """
  Options for writing an image to a file with
  `Image.write/2`.

  """
  @type image_write_options :: [
          jpeg_write_options()
          | png_write_options()
          | tiff_write_options()
          | webp_write_options()
        ]

  @type jpeg_write_options :: [
          {:quality, 1..100},
          {:strip_metadata, boolean()},
          {:icc_profile, Path.t()},
          {:background, Image.pixel()}
        ]

  @type png_write_options :: [
          {:quality, 1..100},
          {:strip_metadata, boolean()},
          {:icc_profile, Path.t()},
          {:background, Image.pixel()}
        ]

  @type tiff_write_options :: [
          {:quality, 1..100},
          {:icc_profile, Path.t()},
          {:background, Image.pixel()}
        ]

  @type heif_write_options :: [
          {:quality, 1..100},
          {:background, Image.pixel()},
          {:compression, compression()}
        ]

  @type webp_write_options :: [
          {:quality, 1..100},
          {:icc_profile, Path.t()},
          {:background, Image.pixel()},
          {:strip_metadata, boolean()}
        ]

  @typedoc """
  Allowble compression types for heif images.

  """
  @type compression :: :hevc | :avc | :jpeg | :av1

  def validate_options(options) do
    case Enum.reduce_while(options, options, &validate_option(&1, &2)) do
      {:error, value} ->
        {:error, value}

      options ->
        {:ok, options}
    end
  end

  defp validate_option({:quality, quality}, options)
       when is_integer(quality) and quality in 1..100 do
    options =
      options
      |> Keyword.delete(:quality)
      |> Keyword.put(:Q, quality)

    {:cont, options}
  end

  defp validate_option({:strip_metadata, strip?}, options) when is_boolean(strip?) do
    options =
      options
      |> Keyword.delete(:strip_metadata)
      |> Keyword.put(:strip, strip?)

    {:cont, options}
  end

  defp validate_option({:progressive, progressive?}, options) when is_boolean(progressive?) do
    options =
      options
      |> Keyword.delete(:progressive)
      |> Keyword.put(:interlace, progressive?)

    {:cont, options}
  end

  defp validate_option({:icc_profile, profile}, options)
       when is_inbuilt_profile(profile) or is_binary(profile) do
    options =
      options
      |> Keyword.delete(:icc_profile)
      |> Keyword.put(:profile, to_string(profile))

    if Color.known_icc_profile?(profile) do
      {:cont, options}
    else
      {:halt, {:error, "The color profile #{inspect(profile)} is not known"}}
    end
  end

  defp validate_option({:background, background}, options) when is_color(background) do
    {:cont, options}
  end

  defp validate_option(option, _options) do
    {:halt, {:error, invalid_option(option)}}
  end

  defp invalid_option(option) do
    "Invalid option or option value: #{inspect(option)}"
  end
end