defmodule Image.Options.Meme do
@moduledoc """
Options and option validation for `Image.meme/2`.
"""
alias Image.Color
@typedoc "Valid font weights"
@type font_weight :: :ultralight | :light | :normal | :bold | :ultrabold | :heavy
@typedoc "Valid type transforms"
@type text_transform :: :capitalize | :upcase | :downcase | :none
@typedoc "Options applicable to Image.meme/3"
@type meme_options ::
[
{:text, String.t()}
| {:font, String.t()}
| {:weight, font_weight()}
| {:color, Color.t()}
| {:outline_color, Color.t()}
| {:justify, boolean()}
| {:transform, text_transform()}
| {:width, pos_integer()}
]
| map()
@doc """
Validate the options for `Image.meme/3`.
See `t:Image.Options.Meme.meme_options/0`.
"""
def validate_options(image, options) when is_list(options) do
options = Keyword.merge(default_options(image), options)
case Enum.reduce_while(options, options, &validate_option(&1, &2)) do
{:error, value} ->
{:error, value}
options ->
options =
cond do
options[:font] == "Impact" && options[:fontfile] == :default ->
Keyword.put(options, :fontfile, font_file("Impact"))
options[:fontfile] == :default ->
Keyword.delete(options, :fontfile)
true ->
options
end
{:ok, Map.new(options)}
end
end
def validate_options(%{} = options) do
{:ok, options}
end
defp validate_option({:font, font}, options) when is_binary(font) do
{:cont, options}
end
defp validate_option({:font_file, font_file}, options) when is_binary(font_file) do
font_file = font_file(font_file)
if File.exists?(font_file) do
options =
options
|> Keyword.delete(:font_file)
|> Keyword.put(:fontfile, font_file)
{:cont, options}
else
{:halt, {:error, no_such_font_file(font_file)}}
end
end
defp validate_option({:font_file, :default}, options) do
options =
options
|> Keyword.delete(:font_file)
|> Keyword.put(:fontfile, :default)
{:cont, options}
end
defp validate_option({:margin, margin}, options) when is_integer(margin) and margin > 0 do
{:cont, options}
end
defp validate_option({:text, text}, options) when is_binary(text) do
{:cont, options}
end
defp validate_option({:weight, weight}, options)
when weight in [:ultralight, :light, :normal, :bold, :ultrabold, :heavy] do
{:cont, options}
end
defp validate_option({:transform, transform}, options)
when transform in [:upcase, :downcase, :capitalize, :none] do
{:cont, options}
end
defp validate_option({:justify, justify}, options) when is_boolean(justify) do
{:cont, options}
end
defp validate_option({key, size}, options)
when key in [:headline_size, :text_size] and is_integer(size) and size > 0 do
{:cont, options}
end
defp validate_option({key, color} = option, options) when key in [:color, :outline_color] do
case Color.rgb_color(color) do
{:ok, hex: _hex, rgb: color} -> {:cont, Keyword.put(options, key, color)}
{:ok, color} -> {:cont, Keyword.put(options, key, color)}
_other -> {:halt, invalid_option(option)}
end
end
defp validate_option(option, _options) do
{:halt, {:error, invalid_option(option)}}
end
defp invalid_option(option) do
"Invalid option or option value: #{inspect(option)}"
end
def no_such_font_file(file) do
"Font file #{inspect(file)} could not be found"
end
defp default_options(image) do
height = Image.height(image)
[
text: "",
font: "Impact",
font_file: :default,
weight: :bold,
color: :white,
outline_color: :black,
justify: false,
transform: :upcase,
headline_size: default_headline_size(height),
text_size: default_text_size(height),
margin: default_margin(image)
]
end
def default_margin(image) do
div(Image.width(image), 20)
end
defp default_headline_size(height) do
height_in_points(height) * 10
end
defp default_text_size(height) do
height_in_points(height) * 6
end
defp height_in_points(height) do
div(height, 72)
end
defp font_file("Impact") do
Path.join(to_string(:code.priv_dir(:image)), "fonts/unicode.impact.ttf")
end
defp font_file(name) when is_binary(name) do
name
end
end