lib/tools/image/convert.ex

defmodule Dragon.Tools.Image.Convert do
  @moduledoc """
  Image handling. Very focused on only a few types of images, so it will break
  if things are given outside of what it expects.

  Requires "Image Magick" or one of it's relatives to be installed.
  """

  import Dragon.Tools.Cmd
  import Transmogrify.As

  @type2atom %{
    "jpg" => :jpg,
    "jpeg" => :jpg,
    "png" => :png,
    "webp" => :webp
  }

  # only those we care about
  @mimetypes %{
    :jpg => "image/jpeg",
    :png => "image/png",
    :webp => "image/webp"
  }
  @types Map.keys(@mimetypes)

  @resolutions [200, 480, 1440, 1920]
  # @social 1000

  # TODO: don't link to anything bigger than this
  # MAXWIDTH = 2000
  # @sizerex Regex.compile("-x[0-9]+(?=\.|-|$)")
  # SIZEREX = re.compile("-x[0-9]+(?=\.|-|$)")

  defp get_type(str) when is_binary(str) do
    down = String.downcase(str)
    with nil <- @type2atom[down], do: as_atom!(down)
  end

  # this can break, doesn't handle all cases; when it breaks we'll fix
  def identify(path) do
    with {:ok, line} <- run(["identify", path]) |> image_magick_error(path) do
      case Regex.run(~r/([A-Z]+) (\d+)x(\d+)/, line) do
        [_, type, width, height] ->
          {:ok, %{type: get_type(type), width: as_int!(width), height: as_int!(height)}}
      end
    end
  end

  def image_magick_error({:error, 1, str}, path) do
    if String.contains?(str, "unable to open image") do
      {:error, "unable to open image: #{path}"}
    else
      {:error, str}
    end
  end

  def image_magick_error(pass, _), do: pass

  def resize(src, dst, w) do
    with {:ok, _} <-
           run(["convert", src, "-resize", "#{w}", dst]) |> image_magick_error(src),
         do: :ok
  end

  def convert(src, dst) do
    with run(["convert", src, dst]) |> image_magick_error(src), do: :ok
  end

  def rename_file(path, target) do
    if path == target do
      :ok
    else
      if File.exists?(target) do
        {:error, "File conflict: #{path} -> #{target} but target already exists"}
      else
        File.rename(path, target)
      end
    end
  end

  def root_name(path), do: Path.rootname(path) |> String.replace(~r/-x[0-9]+(?=\.|-|$)/i, "")

  @default_opts %{resolutions: @resolutions, types: @types}
  def expand_variants(path, opts \\ []) do
    with {:ok, %{type: otype, width: maxw} = orig} <- identify(path) do
      opts = Map.merge(@default_opts, Map.new(opts)) |> Map.merge(orig)
      root = root_name(path)
      target = "#{root}-x#{maxw}.#{otype}"

      opts =
        Map.merge(opts, %{
          origin: target,
          # dirname: Path.dirname(target),
          root: Path.basename(root)
        })

      with :ok <- rename_file(path, target), do: expand_types(%{target => orig}, opts.types, opts)
    end
  end

  ##############################################################################
  def expand_types(acc, [t | rest], opts) do
    with {:ok, acc} <- expand_sizes(acc, t, opts.resolutions, opts),
         do: expand_types(acc, rest, opts)
  end

  def expand_types(acc, [], _), do: {:ok, acc}

  ##############################################################################
  def expand_sizes(acc, type, [width | rest], %{width: max} = opts) when width <= max do
    target = "#{opts.root}-x#{width}.#{type}"

    with {:ok, acc} <- expand_size(acc, target, width, opts),
         do: expand_sizes(acc, type, rest, opts)
  end

  def expand_sizes(acc, t, [_ | rest], o), do: expand_sizes(acc, t, rest, o)

  def expand_sizes(acc, _, [], _), do: {:ok, acc}

  # # #
  defp expand_size(acc, target, _, _) when is_map_key(acc, target), do: acc

  defp expand_size(acc, target, width, opts) do
    with :ok <- resize(opts.origin, target, width),
         {:ok, info} <- identify(target),
         do: {:ok, Map.put(acc, target, info)}
  end
end