defmodule Avatarex do
@moduledoc """
Avatarex is is an elixir package for generating unique, reproducible avatars.
The package is inspired by [Robohash](https://github.com/e1ven/Robohash)
Two Avatar sets are natively `Avatarex.Sets.Birdy` and `Avatarex.Sets.Kitty`, but
additional sets can be created using `Avatarex.Set`.
Optional parameters:
otp_app: The parent application to be used to generate the sets path.
sets_path: The fully qualified sets path or path relative to the otp application's priv directory.
Will default to "myapp/priv/sets" if unset.
renders_path: The absolute path to the directory in which to render images.
Will default to "myapp/priv/renders" if unset or path doesn't exist.
Logs avatar generation, rendering, and writing of rendered avatar.
## Example Usage
defmodule MyApp.Avatar do
use Avatarex
alias Avatarex.Sets.{Birdy, Kitty}
for {name, module} <- [birdy: Birdy, kitty: Kitty] do
set name, module
end
end
"""
require Logger
@spec __before_compile__(env :: Macro.Env.t()) :: Macro.t
# credo:disable-for-next-line
defmacro __before_compile__(_env) do
quote unquote: false do
set_list = "#{inspect Keyword.keys(@sets)}"
@doc """
Generates a reproducible #{__MODULE__} for a given name and set
in #{set_list}.
Returns `%#{__MODULE__}{image: nil, name: ...}`.
## Examples
iex> #{__MODULE__}.generate("user_name", :kitty)
%#{__MODULE__}{image: nil, name: "bob", set: :kitty, renders_path: ...}
"""
@spec generate(Avatarex.set, Avatarex.set_module) :: Avatarex.t_unrendered
for {set, module} <- @sets do
def generate(name, unquote(set)) do
Avatarex.generate(name, unquote(module), unquote(set), @renders_path)
end
end
@doc """
Generates an unreproducible random #{__MODULE__} from a set in #{set_list}.
Returns `%#{__MODULE__}{image: nil, name: ...}`.
## Examples
#{__MODULE__}.random()
%#{__MODULE__}{image: nil, name: nil, set: :birdy, renders_path: ...}
"""
@spec random() :: Avatarex.t_unrendered
def random do
{set, module} = Enum.random(@sets)
Avatarex.random(module, set, @renders_path)
end
@doc """
Generates an unreproducible random #{__MODULE__} for a given set in #{set_list}.
Returns `%#{__MODULE__}{image: nil, name: ...}`.
## Examples
#{__MODULE__}.random(:kitty)
%#{__MODULE__}{image: nil, name: nil, set: :kitty, renders_path: ...}
"""
@spec random(Avatarex.set) :: Avatarex.t_unrendered
for {set, module} <- @sets do
def random(unquote(set)) do
Avatarex.random(unquote(module), unquote(set), @renders_path)
end
end
@doc """
Generates a random #{__MODULE__} for a set in #{set_list}
and renders a composite image.
Returns `%#{__MODULE__}{image: %Vix.Vips.Image{}, set: ...}`.
## Examples
#{__MODULE__}.generate(:kitty)
%#{__MODULE__}{image: %Vix.Vips.Image{}, name: 5, set: :kitty...}
"""
@spec render(Avatarex.set) :: Avatarex.t
for {set, module} <- @sets do
def render(unquote(set)) do
Avatarex.render(unquote(module), unquote(set), @renders_path)
end
end
@doc """
Renders a #{__MODULE__} to form a composite image.
Returns `%#{__MODULE__}{image: %Vix.Vips.Image{}, ...}`.
## Examples
#{__MODULE__}.render(%#{__MODULE__}{})
%#{__MODULE__}{image: %Vix.Vips.Image{}, ...}
"""
@spec render(Avatarex.t) :: Avatarex.t
def render(avatar) do
Avatarex.render(avatar)
end
@doc """
Generates a #{__MODULE__} for a given name and set in #{set_list}
and renders a composite image
Returns `%#{__MODULE__}{image: %Vix.Vips.Image{}, name: ...}`.
## Examples
#{__MODULE__}.write("user_name, :kitty)
%#{__MODULE__}{image: %Vix.Vips.Image{}, name: "user_name", set: :kitty...}
"""
@spec render(Avatarex.name, Avatarex.set) :: Avatarex.t
for {set, module} <- @sets do
def render(name, unquote(set)) do
Avatarex.render(name, unquote(module), unquote(set), @renders_path)
end
end
@doc """
Writes a #{__MODULE__} to initialized renders path.
Returns `%#{__MODULE__}{image: %Vix.Vips.Image{}, name: ...}`.
## Examples
iex> #{__MODULE__}.write(%#{__MODULE__}{})
%#{__MODULE__}{image: %Vix.Vips.Image{}, name: "user_name", set: :kitty...}
"""
@spec write(Avatarex.t) :: Avatarex.t
def write(avatar) do
Avatarex.write(avatar)
end
end
end
@spec __using__([opts: String.t]) :: Macro.t
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@before_compile Avatarex
@app (case opts[:otp_app] do
nil -> Mix.Project.get().project()[:app]
app -> app
end)
@sets_path (case opts[:sets_path] do
nil -> "sets"
path -> path
end)
@sets_path (if File.exists?(@sets_path) do
@sets_path
else
@app |> :code.priv_dir |> Path.join(@sets_path)
end)
@doc """
Returns a full path to the default directory for this avatar's sets.
## Examples
sets_path("kitty")
"../my_app/priv/sets/")
alias MyApp.Avatar.Set.Robot
sets_path(Robot)
".../my_app/priv/sets/")
"""
@spec sets_path() :: sets_path :: String.t
def sets_path(), do: @sets_path
@renders_path (case opts[:renders_path] do
nil -> "renders"
path -> path
end)
@renders_path (if File.exists?(@renders_path) do
@renders_path
else
@app |> :code.priv_dir |> Path.join(@renders_path)
end)
unless File.exists?(@renders_path), do: File.mkdir_p(@renders_path)
Module.register_attribute(__MODULE__, :sets, accumulate: true)
import Avatarex, only: [set: 2]
end
end
@doc """
Macro that adds the provided {set, module} to the available #{__MODULE__} sets.
## Examples
use Avatarex, renders_path:...
set {:kitty, Avatarex.Sets.Kitty}
"""
@spec set(Avatarex.set, Avatarex.set_module) :: Macro.t
defmacro set(set, module) do
quote bind_quoted: [module: module, set: set] do
@sets {set, module}
end
end
defstruct [:image, :name, :set, :renders_path, images: []]
@typedoc "This type"
@type t(image) :: %__MODULE__{image: image, name: String.t | pos_integer(),
set: atom, renders_path: String.t, images: [{Avatarex.Set.layer, Avatarex.Set.image_path}]}
@typedoc "An Avatarex type"
@type t :: t(Vix.Vips.Image.t | nil)
@typedoc "An unrendered Avatarex type"
@type t_unrendered :: t(nil)
@typedoc "A rendered Avatarex type"
@type t_rendered :: t(Vix.Vips.Image.t)
@typedoc "A render path"
@type renders_path :: String.t
@typedoc "A name used for avatar and subsequent rendered file name"
@type name :: String.t | pos_integer
@typedoc "An Avatarex set atom"
@type set :: atom
@typedoc "A module which conforms to Avatar Set behaviour"
@type set_module :: module
@typedoc "A list of Avatarex Set layer and Avatarex Set image _path tuples"
@type images :: [{Avatarex.Set.layer, Avatarex.Set.image_path}]
@typep log_action :: :generate | :render | :write
@doc """
Generates a reproducible #{__MODULE__} for a given name, from the module and set specified
for a set and render path.
Returns `%#{__MODULE__}{image: ...}`.
## Examples
alias Avatarex.Sets.Kitty
#{__MODULE__}.generate("user_name", Kitty, :kitty, ".../priv/renders/...")
%#{__MODULE__}{image: nil, name: "user_name", set: :kitty...}
"""
@spec generate(Avatarex.name, Avatarex.set_module, Avatarex.set, Avatarex.renders_path) :: Avatarex.t_unrendered
def generate(name, module, set, renders_path)
when is_atom(module) and is_binary(name) and is_atom(set) and is_binary(renders_path) do
hash = :crypto.hash(:sha512, name)
layers = module.get_layers()
hash_parts = div(128, Enum.count(layers)) # Future potential for + 1 based on background
block_size = div(512, hash_parts)
layers
|> Enum.zip(for <<block::size(block_size) <- hash>>, do: block)
|> Enum.map(fn {key, value} ->
index = rem(value, module.get_image_count(key))
{key, module.get_image_path_by_index(key, index)}
end)
|> construct(set, name, renders_path)
end
@doc """
Generates a random #{__MODULE__} to render path for a given module, set, and render path.
Returns `%#{__MODULE__}{image: %Vix.Vips.Image{}, name: ...}`.
## Examples
alias Avatarex.Sets.Kitty
#{__MODULE__}.random(Kitty, :kitty, "priv/render")
%#{__MODULE__}{image: %Vix.Vips.Image{}, name: "user_name", set: :kitty...}
"""
@spec random(Avatarex.set_module, Avatarex.set, Avatarex.renders_path) :: Avatarex.t_unrendered
def random(module, set, renders_path)
when is_atom(module) and is_atom(set) and is_binary(renders_path) do
module.get_layers()
|> Enum.reduce([], fn key, acc ->
module.get_images_paths(key)
|> Enum.random()
|> then(&[{key, &1} | acc])
end)
|> construct(set, :rand.uniform(24), renders_path)
end
@spec construct(Avatarex.images, Avatarex.set, Avatarex.name, Avatarex.renders_path) :: Avatarex.t_unrendered
defp construct(images, set, name, renders_path) do
%__MODULE__{images: images, set: set, name: name, renders_path: renders_path}
|> log(:generate)
end
@doc """
Renders a #{__MODULE__} with a composite image.
Returns `%#{__MODULE__}{image: ...}`.
## Examples
#{__MODULE__}.render(%#{__MODULE__}{images: []})
%#{__MODULE__}{image: %Vix.Vips.Image{}, name: "user_name", set: :kitty...}
"""
@spec render(Avatarex.t) :: Avatarex.t_rendered
def render(%__MODULE__{images: images} = avatar) do
images
|> Keyword.values()
|> Enum.reduce(nil, fn
image, nil -> Image.open!(image)
image, composite -> Image.compose!(composite, Image.open!(image))
end)
|> then(&%__MODULE__{avatar | image: &1})
|> log(:render)
end
@doc """
Generates an unreproducible random #{__MODULE__} for a set and render path
and renders the composite image.
Returns `%#{__MODULE__}{image: ...}`.
## Examples
#{__MODULE__}.render(%#{__MODULE__}{images: _})
%#{__MODULE__}{image: %Vix.Vips.Image{}, name: "user_name", set: :kitty...}
"""
@spec render(Avatarex.set_module, Avatarex.set, Avatarex.renders_path) :: Avatarex.t_rendered
def render(module, set, renders_path)
when is_atom(module) and is_atom(set) and is_binary(renders_path) do
module |> random(set, renders_path) |> render()
end
@doc """
Generates a reproducible random #{__MODULE__} for a given name and set and
renders the composite image.
Returns `%#{__MODULE__}{image: ...}`.
## Examples
#{__MODULE__}.render("user_name", Avatarex.Sets.Kitty, :kitty, "/renders/...")
%#{__MODULE__}{image: %Vix.Vips.Image{}, name: "user_name", set: :kitty...}
"""
@spec render(Avatarex.name, Avatarex.set_module, Avatarex.set, Avatarex.renders_path) :: Avatarex.t_rendered
def render(name, module, set, renders_path)
when is_atom(module) and is_atom(set) and is_binary(name) and is_binary(renders_path) do
name |> generate(module, set, renders_path) |> render()
end
@doc """
Renders a #{__MODULE__} composite image and writes the image at the designated
renders path with the form "{name}_{set}.png". Spaces in {name} are replaced with
underscores.
Returns `%#{__MODULE__}{image: %Vix.Vips.Image{}}`.
## Examples
#{__MODULE__}.render("user_name", Avatarex.Sets.Kitty, :kitty, "/renders/...")
%#{__MODULE__}{image: %Vix.Vips.Image{}, name: "user_name", set: :kitty...}
"""
@spec write(avatar :: Avatarex.t_unrendered) :: Avatarex.t_rendered
def write(%__MODULE__{image: nil} = avatar) do
avatar |> render() |> write()
end
@spec write(avatar :: Avatarex.t_rendered) :: Avatarex.t_rendered
def write(%__MODULE__{image: image, set: set, name: name, renders_path: renders_path} = avatar) do
"#{name}"
|> String.replace(~r/[^a-zA-Z0-9-]/, "_")
|> then(&"#{&1}_#{set}.png")
|> then(&Path.join(renders_path, &1))
|> then(&Image.write!(image, &1))
|> then(&%{avatar | image: &1})
|> log(:write)
end
@spec log(Avatarex.t, action :: Avatarex.log_action) :: Avatarex.t
defp log(avatar, action) do
case action do
:generate -> "Generating a #{avatar.set} avatar"
:render -> "Rendering #{avatar.set} avatar named #{avatar.name}"
:write -> "Writing image '#{avatar.name}_#{avatar.set}.png' to renders path"
end
|> Logger.info()
avatar
end
end