Packages
image
0.63.0
0.72.0
0.71.0
0.70.0
0.69.0
0.68.0
0.67.0
0.67.0-dev
retired
0.66.0
0.65.0
0.64.0
0.63.0
0.62.1
0.62.0
0.61.1
0.61.0
0.60.0
0.59.3
0.59.2
0.59.1
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.2
0.55.1
retired
0.55.0
0.54.4
0.54.3
0.54.2
0.54.1
0.54.0
0.53.0
0.52.3
0.52.2
0.52.1
0.52.0
retired
0.51.0
0.50.0
0.49.0
0.48.1
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.2
0.43.1
0.43.0
0.42.0
0.41.0
0.40.0
0.39.3
0.39.2
0.39.1
0.39.0
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.0
0.36.2
0.36.1
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.31.0
0.30.0
0.29.0
0.28.2
0.28.1
0.28.0
0.27.0
0.26.0
0.25.1
0.25.0
0.24.1
0.24.0
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.0
0.20.0
retired
0.19.0
0.18.1
0.18.0
0.17.0
0.16.0
0.15.0
0.14.4
0.14.2
0.14.1
0.14.0
retired
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.10.0-rc.0
retired
0.9.0
retired
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
An approachable image processing library primarily based upon Vix and libvips that is NIF-based, fast, multi-threaded, pipelined and has a low memory footprint.
Current section
Files
Jump to
Current section
Files
lib/image/options/text.ex
defmodule Image.Options.Text do
@moduledoc """
Options for text drawing.
"""
alias Image.Color
alias Vix.Vips.Image, as: Vimage
@type t :: [
{:font, String.t()}
| {:font_size, pos_integer()}
| {:dpi, pos_integer()}
| {:text_fill_color, Color.t()}
| {:text_stroke_width, pos_integer()}
| {:font_weigtht, atom()}
| {:background_fill_color, Color.t()}
| {:background_stroke_color, Color.t()}
| {:background_stroke_width, pos_integer()}
| {:background_stroke_opacity, float()}
| {:background_fill_opacity, float()}
| {:padding, [non_neg_integer(), ...]}
| {:letter_spacing, :normal | pos_integer()}
| {:x, :center | :left | :right | pos_integer()}
| {:y, :middle | :top | :bottom | pos_integer()}
| {:width, pos_integer() | nil}
| {:height, pos_integer() | nil}
| {:fontfile, String.t() | nil}
| {:align, :left | :right | :center}
| {:justify, boolean()}
]
def default_options do
[
font: "Helvetica",
font_size: 50,
text_fill_color: :white,
text_stroke_width: 1,
font_weight: :normal,
background_fill_color: :none,
background_stroke_color: :none,
background_stroke_width: 1,
background_stroke_opacity: 0.7,
background_fill_opacity: 0.7,
padding: [0, 0],
letter_spacing: :normal,
x: :center,
y: :middle,
fontfile: nil,
align: :left,
justify: false
]
end
@doc """
Validate the options for `Image.Text.text/2`.
"""
def validate_options(options) do
options = merge(default_options(), options)
options =
case Enum.reduce_while(options, options, &validate_option(&1, &2)) do
{:error, value} ->
{:error, value}
options ->
{:ok, options}
end
case options do
{:ok, options} ->
options
|> Map.new()
|> ensure_background_color_if_transparent_text()
|> ensure_width_if_height_specified()
|> wrap(:ok)
other ->
other
end
end
# The default :y is :middle if :height is specified
# and :top if it is not. The default for :x is :center
# if :width is specified and :left if not.
defp merge(defaults, options) do
defaults =
if options[:height], do: defaults, else: Keyword.put(defaults, :y, :top)
defaults =
if options[:width], do: defaults, else: Keyword.put(defaults, :x, :left)
Keyword.merge(defaults, options)
end
defp validate_option({:x, x}, options) when is_integer(x) and x >= 0 do
{:cont, options}
end
defp validate_option({:y, y}, options) when is_integer(y) and y >= 0 do
{:cont, options}
end
defp validate_option({:x, x}, options) when x in [:left, :center, :right] do
{:cont, options}
end
defp validate_option({:y, y}, options) when y in [:top, :middle, :bottom] do
{:cont, options}
end
defp validate_option({:width, width}, options) when is_integer(width) and width >= 0 do
{:cont, options}
end
defp validate_option({:width, nil}, options) do
{:cont, options}
end
defp validate_option({:height, height}, options) when is_integer(height) and height >= 0 do
{:cont, options}
end
defp validate_option({:height, nil}, options) do
{:cont, options}
end
defp validate_option({:font, font}, options) when is_binary(font) do
{:cont, options}
end
defp validate_option({:font_weight, font_weight}, options)
when is_binary(font_weight) or is_atom(font_weight) do
{:cont, options}
end
defp validate_option({:font_weight, font_weight}, options)
when is_integer(font_weight) and font_weight in 1..1000 do
{:cont, options}
end
defp validate_option({:font_size, font_size}, options)
when is_integer(font_size) and font_size >= 0 do
{:cont, options}
end
defp validate_option({:dpi, dpi}, options)
when is_integer(dpi) and dpi >= 0 do
{:cont, options}
end
defp validate_option({:background_fill_color = option, color}, options) do
validate_color(option, color, options)
end
defp validate_option({:background_stroke_color = option, color}, options) do
validate_color(option, color, options)
end
defp validate_option({:text_fill_color = option, color}, options) do
validate_color(option, color, options)
end
defp validate_option({:background_fill_opacity = option, opacity}, options) do
validate_opacity(option, opacity, options)
end
defp validate_option({:background_stroke_opacity = option, opacity}, options) do
validate_opacity(option, opacity, options)
end
defp validate_option({:text_stroke_width = option, width}, options) do
validate_stroke_width(option, width, options)
end
defp validate_option({:background_stroke_width = option, width}, options) do
validate_stroke_width(option, width, options)
end
defp validate_option({:padding, [left, right]}, options)
when is_integer(left) and is_integer(right) and left >= 0 and right >= 0 do
{:cont, options}
end
defp validate_option({:padding = option, padding}, options)
when is_integer(padding) and padding >= 0 do
{:cont, Keyword.put(options, option, [padding, padding])}
end
# If padding is related to a base image we defer calculating it
# until we know the size of the text image.
defp validate_option({:padding, %Vimage{}}, options) do
{:cont, options}
end
defp validate_option({:letter_spacing, :normal}, options) do
{:cont, options}
end
defp validate_option({:letter_spacing = option, letter_spacing}, options)
when is_integer(letter_spacing) do
{:cont, Keyword.put(options, option, letter_spacing)}
end
defp validate_option({:font_file, nil}, options) do
options =
options
|> Keyword.delete(:font_file)
{:cont, options}
end
# Deprecate :fontfile in favour of :font_file for consistency
defp validate_option({:font_file, fontfile}, options) when is_binary(fontfile) do
options =
options
|> Keyword.delete(:font_file)
|> Keyword.put(:fontfile, fontfile)
{:cont, options}
end
defp validate_option({:fontfile, nil}, options) do
{:cont, options}
end
defp validate_option({:fontfile, font_file}, options) when is_binary(font_file) do
{:cont, options}
end
defp validate_option({:align, :right}, options) do
options = Keyword.put(options, :align, :VIPS_ALIGN_HIGH)
{:cont, options}
end
defp validate_option({:align, :left}, options) do
options = Keyword.put(options, :align, :VIPS_ALIGN_LOW)
{:cont, options}
end
defp validate_option({:align, :center}, options) do
options = Keyword.put(options, :align, :VIPS_ALIGN_CENTRE)
{:cont, options}
end
defp validate_option({:align, align}, options)
when align in [:VIPS_ALIGN_LOW, :VIPS_ALIGN_LOW, :VIPS_ALIGN_CENTRE] do
{:cont, options}
end
defp validate_option({:justify, justify}, options) do
justify = if justify, do: true, else: false
{:cont, Keyword.put(options, :justify, justify)}
end
defp validate_option(option, _options) do
{:halt, {:error, invalid_option(option)}}
end
@doc false
def validate_color(option, color, options) do
cond do
(is_binary(color) or is_atom(color)) && Map.get(Color.color_map(), Color.normalize(color)) ->
{:cont, options}
match?(<<"#", _rest::bytes-6>>, color) ->
{:cont, options}
match?([_r, _g, _b], color) ->
[r, g, b] = color
hex_color =
["#", convert_color(r), convert_color(g), convert_color(b)]
|> :erlang.iolist_to_binary()
{:cont, Keyword.put(options, option, hex_color)}
color in [:none, "none"] ->
{:cont, Keyword.put(options, option, :transparent)}
color in [:transparent, "transparent"] ->
{:cont, Keyword.put(options, option, :transparent)}
(is_binary(color) or is_atom(color)) && String.downcase(to_string(color)) in ["none", ""] ->
{:cont, Keyword.put(options, option, :none)}
true ->
{:halt, {:error, invalid_option(option, color)}}
end
end
@doc false
def validate_opacity(_option, opacity, options)
when is_float(opacity) and opacity >= 0.0 and opacity <= 1.0 do
{:cont, options}
end
def validate_opacity(option, opacity, _options) do
{:halt, {:error, invalid_option(option, opacity)}}
end
@doc false
def validate_stroke_width(_option, width, options) when is_integer(width) and width > 0 do
{:cont, options}
end
def validate_stroke_width(option, width, _options) do
{:halt, {:error, invalid_option(option, width)}}
end
@doc false
def invalid_option(option) do
"Invalid option or option value: #{inspect(option)}"
end
@doc false
def invalid_option(option, value) do
"Invalid option or option value: #{option}: #{inspect(value)}"
end
defp ensure_background_color_if_transparent_text(options) do
case options do
%{text_fill_color: :transparent, background_color: :none} ->
Map.put(options, :background_color, "black")
_other ->
options
end
end
defp ensure_width_if_height_specified(%{width: width, height: height} = options)
when is_integer(width) and is_integer(height) do
options
end
defp ensure_width_if_height_specified(%{width: nil, height: height})
when is_integer(height) do
{:error, ":width must be specified if :height is specified"}
end
defp ensure_width_if_height_specified(%{height: height}) when is_integer(height) do
{:error, ":width must be specified if :height is specified"}
end
defp ensure_width_if_height_specified(options) do
options
end
defp convert_color(c) do
c
|> round()
|> Integer.to_string(16)
|> String.pad_leading(2, "0")
end
@doc false
def wrap({:error, _} = error_return, _atom) do
error_return
end
def wrap(term, atom) do
{atom, term}
end
end