lib/avatador.ex

defmodule Avatador do
  @moduledoc """
  Avatador
  """
  @moduledoc since: "0.1.0"

  alias Avatador.Helpers
  alias Avatador.Helpers.{Color, List}

  defstruct(
    format: "SVG",
    background: "rgba(0,0,0,1)",
    background_rgba: %{r: 0, g: 0, b: 0, a: 1},
    color: "rgba(255,255,255,1)",
    color_rgba: %{r: 255, g: 255, b: 255, a: 1},
    name: "",
    is_rounded: false,
    rounded: 0.0,
    width: 500,
    height: 500,
    font_size: 250,
    font_family: "Montserrat",
    caps: 1,
    bold: true,
    text_style: "font-size:250.0px;line-height:1;color:rgba\(255,255,255,1\);font-family:Montserrat,sans-serif;",
    hash: nil,
    hash_integer: nil,
    hash_list: nil,
    grid: nil,
    pixels: nil
  )

  @doc """
  Create an avatar from a `Map` of options.
  Returns a `~s()` SVG.

  ## Examples

      # An avatar with background of "#962ED" and name "John Doe"
      iex> Avatador.avatar(%{background: "96C2ED", name: "John Doe"})

      # An avatar with a random background, name "John Doe", and width of "50px"
      iex> Avatador.avatar(%{name: "John Doe", width: "50"})

  ## Arguments

  - `:background` The background color of the avatar, without the hash (#) e.g. `000000`.
  - `:color` The text color of the avatar, without the hash (#) e.g. `FFFFFF`.
  - `:name` The name/username/email of the entity you want the avatar's initials to represent.
  - `:is_rounded` True or false, should avater be rounded? If `true` and no `rounded` value it assumes the width/height (fully rounded).
  - `:rounded` The px value of rounding, leave blank or `0` for no rounding.
  - `:width` The height of the avatar in pixels. If not provided assumes width=height or default value.
  - `:height` The width of the avatar in pixels. If not provided assumes width=height or default value.
  - `:font_size` The font size in pixels.
  - `:font_family` The name of the font family.
  - `:caps` Capitalization of the initials. 1 for uppercase, 2 for lowercase, 3 for leaving as provided.
  - `:bold` True or false, should text be bold?

  ## Defaults

      %{
        background: "#000000",
        color: "#FFFFFF",
        name: "",
        is_rounded: false,
        rounded: 0.0,
        width: 500.0,
        height: 500.0,
        font_size: 250.0,
        font_family: "Montserrat",
        caps: 1,
        bold: true,
      }

  """
  @doc since: "0.1.0"
  def avatar(assigns \\ %{}) do
    assigns
    |> sanatize_inputs
    |> create_hash_from_name
    |> verify_color
    |> verify_background
    |> verify_name_create_initials
    |> verify_size
    |> verify_rounded
    |> create_text_style
    |> render_avatar_svg
  end

  @doc """
  Create an identicon from a `Map` of options.
  Returns a `~s()` for SVG or a base64 encoded PNG.

  ## Examples

      # An identicon with background of "#962ED" and name "John Doe"
      iex> Avatador.identicon(%{background: "96C2ED", name: "John Doe"})

      # An identicon in PNG with a random background, name "John Doe", and width of "50px"
      iex> Avatador.identicon(%{format: "PNG", background: "96C2ED", name: "John Doe", width: "50"})

  ## Arguments

  - `:format` SVG or PNG output.
  - `:background` The background color of the avatar, without the hash (#) e.g. `000000`.
  - `:color` The text color of the avatar, without the hash (#) e.g. `FFFFFF`.
  - `:name` The name/username/email of the entity you want the avatar's initials to represent.
  - `:is_rounded` True or false, should avater be rounded? If `true` and no `rounded` value it assumes the width/height (fully rounded).
  - `:rounded` The px value of rounding, leave blank or `0` for no rounding.
  - `:width` The height of the avatar in pixels. If not provided assumes width=height or default value.
  - `:height` The width of the avatar in pixels. If not provided assumes width=height or default value.

  ## Defaults

      %{
        format: "SVG",
        background: "#000000",
        color: "#FFFFFF",
        name: "",
        is_rounded: false,
        rounded: 0.0,
        width: 500.0,
        height: 500.0,
      }

  """
  @doc since: "0.1.0"
  def identicon(assigns \\ %{}) do
    format = sanatize_format_input(Map.get(assigns, :format))
    case format do
      "PNG" ->
        assigns
        |> sanatize_inputs
        |> create_hash_from_name
        |> verify_color
        |> verify_background
        |> verify_size
        |> verify_rounded
        |> build_grid
        |> remove_odd_bytes
        |> calculate_pixels
        |> render_identicon_png

      _ ->
        assigns
        |> sanatize_inputs
        |> create_hash_from_name
        |> verify_color
        |> verify_background
        |> verify_size
        |> verify_rounded
        |> build_grid
        |> remove_odd_bytes
        |> calculate_pixels
        |> render_identicon_svg
    end
  end

  defp sanatize_format_input(format), do: format |> Helpers.to_string() |> String.upcase()

  defp sanatize_inputs(assigns) do
    # Floats
    rounded = Map.get(assigns, :rounded) |> Helpers.to_float()
    width = Map.get(assigns, :width) |> Helpers.to_float()
    height = Map.get(assigns, :height) |> Helpers.to_float()
    font_size = Map.get(assigns, :font_size) |> Helpers.to_float()

    # Integers
    caps = Map.get(assigns, :caps) |> Helpers.to_integer()

    # Booleans
    bold =Map.get(assigns, :bold) |>  Helpers.to_boolean()
    is_rounded = Map.get(assigns, :is_rounded) |> Helpers.to_boolean()

    # Strings
    background = Map.get(assigns, :background) |> Helpers.to_string()
    color = Map.get(assigns, :color) |> Helpers.to_string()
    name = Map.get(assigns, :name) |> Helpers.to_string()
    font_family = Map.get(assigns, :font_family) |> Helpers.to_string()

    %Avatador{
      background: "#" <> background,
      color: "#" <> color,
      name: name,
      is_rounded: is_rounded,
      rounded: rounded,
      width: width,
      height: height,
      font_size: font_size,
      font_family: font_family,
      caps: caps,
      bold: bold,
    }
  end

  defp create_hash_from_name(%Avatador{name: name} = avatador) do
    hash = Helpers.hash_value(name)
    hash_integer = Helpers.hash_to_integer(hash)
    hash_list  = Helpers.hash_to_list(hash)
    %Avatador{avatador | hash: hash, hash_integer: hash_integer, hash_list: hash_list}
  end

  defp verify_color(%Avatador{color: color} = avatador) do
    rgba = if Color.is_valid_color_hex?(color) do
      Color.hex_to_rgba(color)
    else
      # Default white
      %{r: 255, g: 255, b: 255, a: 1}
    end

    %Avatador{avatador | color: "rgba(#{rgba.r},#{rgba.g},#{rgba.b},#{rgba.a})", color_rgba: rgba}
  end

  defp verify_background(%Avatador{background: background} = avatador) do
    rgba = if Color.is_valid_color_hex?(background) do
      Color.hex_to_rgba(background)
    else
      # Default random
      [r, g, b | _] = avatador.hash_list
      %{r: r, g: g, b: b, a: 1}
    end

    %Avatador{avatador | background: "rgba(#{rgba.r},#{rgba.g},#{rgba.b},#{rgba.a})", background_rgba: rgba}
  end

  defp verify_name_create_initials(%Avatador{name: name} = avatador) do
    if String.length(name) >= 1 do
      initials =
        name
        |> Helpers.generate_initials()
        |> Helpers.maybe_capitalize(avatador.caps)

      %Avatador{avatador | name: initials}
    else
      # Default empty string
      %Avatador{avatador | name: ""}
    end
  end

  defp verify_size(%Avatador{width: 0.0, height: 0.0} = avatador), do: %Avatador{avatador | height: 500.0, width: 500.0}
  defp verify_size(%Avatador{width: 0.0, height: height} = avatador), do: %Avatador{avatador | height: height, width: height}
  defp verify_size(%Avatador{width: width, height: 0.0} = avatador), do: %Avatador{avatador | height: width, width: width}
  defp verify_size(%Avatador{} = avatador), do: avatador

  defp verify_rounded(%Avatador{is_rounded: true, rounded: rounded} = avatador) when rounded > 0.0, do: avatador
  defp verify_rounded(%Avatador{rounded: rounded} = avatador) when rounded > 0.0, do: %Avatador{avatador | is_rounded: true}
  defp verify_rounded(%Avatador{is_rounded: true} = avatador), do: %Avatador{avatador | rounded: max(avatador.width, avatador.height)}
  defp verify_rounded(%Avatador{} = avatador), do: %Avatador{avatador | rounded: 0.0, is_rounded: false}

  ###
  ##
  ## Avatar Specific
  ##
  ###

  defp create_text_style(%Avatador{bold: bold, font_size: font_size, font_family: font_family} = avatador) do
    width = avatador.width
    height = avatador.height
    color = avatador.color

    font_size_style = if font_size == 0.0, do: "font-size:#{min(width, height) / 2}px;", else: "font-size:#{font_size}px;"
    font_line_height_style = "line-height:1;"
    font_color_style = "color:#{color};"
    font_bolt_style = if bold == true, do: "font-weight:700;", else: ""
    font_family_style = if String.length(font_family) == 0, do: "font-family:Montserrat,sans-serif;", else: "font-family:#{font_family},sans-serif;"

    text_style = "#{font_size_style}#{font_line_height_style}#{font_color_style}#{font_bolt_style}#{font_family_style}"
    %Avatador{avatador | text_style: text_style}
  end

  defp render_avatar_svg(%Avatador{name: name, background: background, width: width, height: height, rounded: rounded, color: color, text_style: text_style}) do
    ~s(<svg width="#{width}px" height="#{height}px" viewBox="0 0 #{width} #{height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0"><rect x="0" y="0" width="#{width}" height="#{height}" rx="#{rounded}" style="fill:#{background}" /><text x="50%" y="50%" dy=".1em" fill="#{color}" text-anchor="middle" dominant-baseline="middle" style="#{text_style}">#{name}</text></svg>)
  end

  ###
  ##
  ## Identicon Specific
  ## Modified from: https://github.com/rbishop/identicon
  ###

  # We remove the head of the hexadecimal hash list because we only need fifteen
  # bytes to generate the left side and center of the grid
  defp build_grid(%Avatador{hash_list: [_ | hash_list]} = avatador) do
    grid = List.integer_list_to_grid(hash_list)
    %Avatador{avatador | grid: grid}
  end

  defp remove_odd_bytes(%Avatador{grid: grid} = avatador) do
    grid = Enum.filter grid, fn({code, _index}) ->
      rem(code, 2) == 0
    end

    %Avatador{avatador | grid: grid}
  end

  defp calculate_pixels(%Avatador{grid: grid, width: width, height: height} = avatador) do
    width = trunc(width / 5)
    height = trunc(height / 5)

    pixels = Enum.map(grid, fn({_code, index}) ->
      horizontal = rem(index, 5) * width
      vertical = div(index, 5) * height

      top_left = {horizontal, vertical}
      bottom_right = {horizontal + width, vertical + height}

      {top_left, bottom_right}
    end)

    %Avatador{avatador | pixels: pixels}
  end

  defp render_identicon_svg(%Avatador{pixels: pixels, background: background, width: width, height: height, rounded: rounded}) do
    # svg_background = ~s(<rect fill="#{avatador.background}" x="0" y="0" width="#{avatador.width}" height="#{avatador.height}" />)
    svg_blocks = Enum.reduce(pixels, "", fn({{x1, y1} = _start, {x2, y2} = _stop}, svg_blocks) ->
      svg_blocks <> ~s(<rect fill="#{background}" x="#{x1}" y="#{y1}" width="#{x2 - x1}" height="#{y2 - y1}"/>)
    end)

    ~s(<svg style="border-radius:#{rounded}px;" width="#{width}px" height="#{height}px" viewBox="0 0 #{width} #{height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0">#{svg_blocks}</svg>)
  end

  # :edg is an optional module
  # if not available just generate SVG
  if Code.ensure_loaded?(:egd) do
    defp render_identicon_png(%Avatador{background_rgba: background_rgba, pixels: pixels, width: width, height: height}) do
      # Create image object. `trunc` to convert floats to integers.
      image = :egd.create(trunc(width), trunc(height))

      # Set color object
      %{r: r, g: g, b: b, a: _a} = background_rgba
      fill = :egd.color({r, g, b})

      # Create a rectangle for each "pixel"
      Enum.each(pixels, fn({start, stop}) ->
        :egd.filledRectangle(image, start, stop, fill)
      end)

      # Render image to base64
      :egd.render(image, :png) |> Base.encode64
    end
  else
    defp render_identicon_png(%Avatador{} = avatador) do
      render_identicon_svg(avatador)
    end
  end

end