# SPDX-FileCopyrightText: 2023 Isaak Tsalicoglou <isaak@waseigo.com>
# SPDX-License-Identifier: Apache-2.0
defmodule Identicon do
@moduledoc """
Defines the `%Identicon{}` struct used by the functions of the main
module to gradually process and generate an identicon.
"""
@moduledoc since: "0.1.0"
defstruct text: nil,
size: 5,
rgb: nil,
grid: nil,
svg: nil,
bg_color: nil,
opacity: 1.0
end
defmodule IdenticonSvg do
require EEx
@moduledoc """
Main module of `IdenticonSvg` that contains all functions of the library.
"""
@moduledoc since: "0.1.0"
@doc """
Generate the SVG code of the identicon for the specified `text`.
Without specifying any optional arguments this function generates a 5x5 identicon
with a transparent background and colored grid squares with full opacity.
Optionally, specify any combination of the following arguments:
* `size`: the number of grid squares of the identicon's side; integer, 4 to 10; 5 by default.
* `bg_color`: the color of the background grid squares; string, hex code (e.g., `#eee`); `nil` by default.
* `opacity`: the opacity of the entire identicon (all grid squares); float, 0.0 to 1.0; 1.0 by default.
Setting `bg_color` to `nil` (default value) generates only the foreground (colored) squares,
with the default (1.0) or requested `opacity`.
The color of the grid squares is always equal to the three first bytes of the
hash of `text`, regardless of which hashing function is used automatically.
A different hashing function is used automatically for each identicon `size`,
so that the utilization of bits is maximized for the given size: MD5 for sizes 4 and 5, RIPEMD-160 for 6, and SHA3 for 7 to 10 with 224, 256, 384 and 512 bits, respectively.
## Examples
5x5 identicon with transparent background:
```elixir
generate("banana")
```
![5x5 identicon for "banana", at full opacity, with transparent background](assets/banana_5x5_nil_1p0.svg)
6x6 identicon with transparent background:
```elixir
generate("pineapple", 6)
```
![6x6 identicon for "pineapple", at full opacity, with transparent background](assets/pineapple_6x6_nil_1p0.svg)
7x7 identicon with light gray (`#d3d3d3`) background:
```elixir
generate("refrigerator", 7, "#d3d3d3")
```
![7x7 identicon for "refrigerator", at full opacity, with light gray background](assets/refrigerator_7x7_d3d3d3_1p0.svg)
9x9 identicon with transparent background and 50% opacity:
```elixir
generate("2023-03-14", 9, nil, 0.5)
```
![9x9 identicon for "2023-03-14", at 50% opacity, with transparent background](assets/2023-03-14_9x9_nil_0p5.svg)
10x10 identicon with yellow (`#ff0`) background and 80% opacity:
```elixir
generate("banana", 10, "#ff0", 0.8)
```
![10x10 identicon for "banana", at 80% opacity, with yellow background](assets/banana_10x10_ff0_0p8.svg)
"""
def generate(text, size \\ 5, bg_color \\ nil, opacity \\ 1.0)
when is_bitstring(text) and size in 4..10 and
(is_bitstring(bg_color) or is_nil(bg_color)) and is_float(opacity) do
%Identicon{text: text, size: size, bg_color: bg_color, opacity: opacity}
|> hash_input()
|> extract_color()
|> square_grid()
|> mark_present_squares()
|> generate_coordinates()
|> color_all_squares()
|> output_svg()
end
defp appropriate_hash(size) when size in 4..10 do
hashes = %{
4 => :md5,
5 => :md5,
6 => :ripemd160,
7 => :sha3_224,
8 => :sha3_256,
9 => :sha3_384,
10 => :sha3_512
}
hashes[size]
end
defp hash_input(%Identicon{text: text, size: size} = input) do
grid =
appropriate_hash(size)
|> :crypto.hash(text)
|> :binary.bin_to_list()
%{input | grid: grid}
end
defp extract_color(%Identicon{grid: grid} = input) do
rgb =
grid
|> Enum.chunk_every(3)
|> hd()
|> Enum.map(&integer_to_hex/1)
|> List.to_string()
|> String.downcase()
%{input | rgb: "#" <> rgb}
end
defp integer_to_hex(value) when is_integer(value) and value in 0..255 do
Integer.to_string(value, 16)
|> String.pad_leading(2, "0")
end
defp square_grid(%Identicon{grid: grid, size: size} = input) do
odd = rem(size, 2)
chunks = Integer.floor_div(size, 2) + odd
grid =
grid
|> Enum.chunk_every(chunks)
|> Enum.slice(0, size)
|> Enum.map(&mirror_row(&1, odd))
|> List.flatten()
%{input | grid: grid}
end
defp mirror_row(row, odd) when odd in 0..1 do
mirror =
row
|> Enum.slice(0, length(row) - odd)
|> Enum.reverse()
row ++ mirror
end
defp mark_present_squares(%Identicon{grid: grid} = input) do
grid =
grid
|> Enum.map(fn x -> 1 - rem(x, 2) end)
%{input | grid: grid}
end
defp generate_coordinates(%Identicon{grid: grid} = input) do
grid =
grid
|> Enum.with_index()
%{input | grid: grid}
end
defp color_all_squares(
%Identicon{
grid: grid,
size: size,
rgb: fg_color,
bg_color: bg_color,
opacity: opacity
} = input
) do
svg =
grid
|> Enum.map(&square_to_rect(&1, fg_color, bg_color, opacity, size))
%{input | svg: svg}
end
EEx.function_from_string(
:defp,
:svg_rectangle,
~s( <rect width="20" height="20" x="<%= x_coord %>" y="<%= y_coord %>" style="stroke: <%= color %>; stroke-width: 0; stroke-opacity: <%= opacity %>; fill: <%= color %>; fill-opacity: <%= opacity %>;"/>\n),
[:x_coord, :y_coord, :color, :opacity]
)
EEx.function_from_string(
:defp,
:svg_preamble,
~s(<svg version="1.1" width="20mm" height="20mm" viewBox="0 0 <%= length %> <%= length %>" preserveAspectRatio="xMidYMid meet" shape-rendering="crispEdges" xmlns="http://www.w3.org/2000/svg">\n),
[:length]
)
defp square_to_rect(
{presence, index},
fg_color,
bg_color,
opacity,
divisor
) do
%{x: x_coord, y: y_coord} = index_to_coords(index, divisor)
case {presence, bg_color} do
{0, nil} ->
""
{0, _} ->
svg_rectangle(x_coord, y_coord, bg_color, opacity)
{1, _} ->
svg_rectangle(x_coord, y_coord, fg_color, opacity)
end
end
defp index_to_coords(index, divisor) when is_integer(divisor) do
%{
x: rem(index, divisor) * 20,
y: div(index, divisor) * 20
}
end
defp output_svg(%Identicon{svg: svg, size: size}) do
pre = svg_preamble(size * 20)
post = "</svg>"
pre <> List.to_string(svg) <> post
end
end