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