Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 41 additions & 35 deletions lib/image.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2373,26 +2373,24 @@ defmodule Image do

@doc subject: "Mask", since: "0.13.0"

@spec chroma_mask(image :: Vimage.t(), options :: ChromaKey.chroma_key_options() | map()) ::
@spec chroma_mask(image :: Vimage.t(), options :: ChromaKey.chroma_key_options()) ::
{:ok, Vimage.t()} | {:error, error()}

def chroma_mask(image, options \\ [])

def chroma_mask(%Vimage{} = image, options) when is_list(options) do
def chroma_mask(%Vimage{} = image, options \\ []) do
with {:ok, options} <- Options.ChromaKey.validate_options(image, options) do
chroma_mask(image, options)
do_chroma_mask(image, options)
end
end

def chroma_mask(%Vimage{} = image, %{color: color, threshold: threshold}) do
defp do_chroma_mask(%Vimage{} = image, %{color: color, threshold: threshold}) do
alias Image.Math

# The mask is computed from the color bands only so any alpha
# band is split off and the color is truncated to match.
{color_image, _alpha} = split_alpha(image)

with {:ok, color} <- maybe_calculate_color(image, color) do
color = color |> List.wrap() |> Enum.take(bands(color_image))
color = Enum.take(color, bands(color_image))

color_image
|> Math.subtract!(color)
Expand All @@ -2403,7 +2401,7 @@ defmodule Image do
end
end

def chroma_mask(%Vimage{} = image, %{greater_than: greater_than, less_than: less_than}) do
defp do_chroma_mask(%Vimage{} = image, %{greater_than: greater_than, less_than: less_than}) do
alias Image.Math

with {:ok, greater} <- Math.greater_than(image, greater_than),
Expand Down Expand Up @@ -2553,7 +2551,7 @@ defmodule Image do

def chroma_key(%Vimage{} = image, options \\ []) do
with {:ok, options} <- Options.ChromaKey.validate_options(image, options),
{:ok, mask} <- chroma_mask(image, options),
{:ok, mask} <- do_chroma_mask(image, options),
{:ok, flattened} <- flatten(image) do
Operation.bandjoin([flattened, mask])
end
Expand Down Expand Up @@ -2674,10 +2672,15 @@ defmodule Image do

def blur(%Vimage{} = image, options \\ []) do
with {:ok, options} <- Options.Blur.validate_options(options) do
Operation.gaussblur(image, options.sigma, "min-ampl": options.min_amplitude)
do_blur(image, options)
end
end

# Takes validated options so `feather/2` can blur without validating twice.
defp do_blur(%Vimage{} = image, options) do
Operation.gaussblur(image, options.sigma, "min-ampl": options.min_amplitude)
end

@doc """
Applies a gaussian blur to an image.

Expand Down Expand Up @@ -3002,37 +3005,40 @@ defmodule Image do

def feather(%Vimage{} = image, options \\ []) do
with {:ok, options} <- Options.Blur.validate_options(options) do
margin = round(options.sigma * 2)
do_feather(image, options)
end
end

cond do
has_alpha?(image) ->
{image, alpha} = split_alpha(image)
defp do_feather(%Vimage{} = image, options) do
margin = round(options.sigma * 2)

with {:ok, feathered} <- feather(alpha, options) do
Operation.bandjoin([image, feathered])
end
cond do
has_alpha?(image) ->
{image, alpha} = split_alpha(image)

with {:ok, feathered} <- do_feather(alpha, options) do
Operation.bandjoin([image, feathered])
end

bands(image) == 1 and (width(image) <= 2 * margin or height(image) <= 2 * margin) ->
message =
"Image of size {#{width(image)}, #{height(image)}} is too small to feather " <>
"with sigma #{inspect(options.sigma)}. The image must be larger than " <>
"{#{2 * margin}, #{2 * margin}}"
bands(image) == 1 and (width(image) <= 2 * margin or height(image) <= 2 * margin) ->
message =
"Image of size {#{width(image)}, #{height(image)}} is too small to feather " <>
"with sigma #{inspect(options.sigma)}. The image must be larger than " <>
"{#{2 * margin}, #{2 * margin}}"

{:error, %Image.Error{message: message, reason: message}}
{:error, %Image.Error{message: message, reason: message}}

bands(image) == 1 ->
crop!(image, margin, margin, width(image) - 2 * margin, height(image) - 2 * margin)
|> Operation.embed!(margin, margin, width(image), height(image))
|> blur!(options)
|> wrap(:ok)
bands(image) == 1 ->
crop!(image, margin, margin, width(image) - 2 * margin, height(image) - 2 * margin)
|> Operation.embed!(margin, margin, width(image), height(image))
|> do_blur(options)

true ->
{:error,
%Image.Error{
message: "Image has no alpha band and is not a single band image",
reason: "Image has no alpha band and is not a single band image"
}}
end
true ->
{:error,
%Image.Error{
message: "Image has no alpha band and is not a single band image",
reason: "Image has no alpha band and is not a single band image"
}}
end
end

Expand Down
109 changes: 20 additions & 89 deletions lib/image/draw.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ defmodule Image.Draw do
alias Vix.Vips.Image, as: Vimage
alias Vix.Vips.MutableImage
alias Image.Vips.MutableOperation
alias Image.Pixel
alias Image.Options

import Image, only: :macros
Expand Down Expand Up @@ -104,24 +103,19 @@ defmodule Image.Draw do
def point(%Vimage{} = image, left, top, options)
when is_integer(left) and is_integer(top) and left >= 0 and top >= 0 do
with {:ok, options} <- Options.Draw.validate_options(image, :point, options) do
color = maybe_add_alpha(image, options.color)

Vimage.mutate(image, fn mut_img ->
MutableOperation.draw_rect(mut_img, color, left, top, 1, 1)
MutableOperation.draw_rect(mut_img, options.color, left, top, 1, 1)
end)
end
end

def point(%MutableImage{} = image, left, top, options) when is_point(left, top) do
with {:ok, options} <- Options.Draw.validate_options(image, :point, options) do
color = maybe_add_alpha(image, options.color)

case MutableOperation.draw_rect(image, color, left, top, 1, 1) do
case MutableOperation.draw_rect(image, options.color, left, top, 1, 1) do
:ok -> {:ok, image}
other -> other
end
end
|> maybe_wrap()
end

@doc """
Expand Down Expand Up @@ -272,10 +266,8 @@ defmodule Image.Draw do
when is_image(image_type) and is_box(left, top, width, height) do
with {:ok, options} <- Options.Draw.validate_options(image, :rect, options) do
%{stroke_width: stroke_width, fill: fill} = options
color = maybe_add_alpha(image, options.color)
rect(image, left, top, width, height, color, stroke_width, fill)
rect(image, left, top, width, height, options.color, stroke_width, fill)
end
|> maybe_wrap()
end

# If the stroke width is 1 then use the underlying Vips call.
Expand Down Expand Up @@ -477,10 +469,8 @@ defmodule Image.Draw do
when is_image(image_type) and is_circle(cx, cy, radius) do
with {:ok, options} <- Options.Draw.validate_options(image, :circle, options) do
%{stroke_width: stroke_width, fill: fill, color: color} = options
color = maybe_add_alpha(image, color)
circle(image, cx, cy, radius, color, stroke_width, fill)
end
|> maybe_wrap()
end

# When drawing a circle with a stroke_wiodth of > 1 then
Expand Down Expand Up @@ -661,21 +651,22 @@ defmodule Image.Draw do
is_integer(x2) and is_integer(y2) and x2 >= 0 and y2 >= 0 do
with {:ok, options} <- Options.Draw.validate_options(image, :line, options) do
Image.mutate(image, fn mut_img ->
line(mut_img, x1, y1, x2, y2, options)
do_line(mut_img, x1, y1, x2, y2, options)
end)
end
|> maybe_wrap()
end

def line(%MutableImage{} = image, x1, y1, x2, y2, options)
when is_integer(x1) and is_integer(y1) and x1 >= 0 and y1 >= 0 and
is_integer(x2) and is_integer(y2) and x2 >= 0 and y2 >= 0 do
with {:ok, options} <- Options.Draw.validate_options(image, :line, options) do
color = maybe_add_alpha(image, options.color)
:ok = MutableOperation.draw_line(image, color, x1, y1, x2, y2)
{:ok, image}
do_line(image, x1, y1, x2, y2, options)
end
|> maybe_wrap()
end

defp do_line(%MutableImage{} = image, x1, y1, x2, y2, options) do
:ok = MutableOperation.draw_line(image, options.color, x1, y1, x2, y2)
{:ok, image}
end

@doc """
Expand Down Expand Up @@ -823,19 +814,21 @@ defmodule Image.Draw do
# Image.mutate/2.
with {:ok, options} <- Options.Draw.validate_options(image, :image, options) do
Image.mutate(image, fn mut_img ->
image(mut_img, sub_image, top, left, options)
do_image(mut_img, sub_image, top, left, options)
end)
end
|> maybe_wrap()
end

def image(%MutableImage{} = image, %Vimage{} = sub_image, top, left, options)
when is_integer(top) and is_integer(left) and top >= 0 and left >= 0 do
with {:ok, options} <- Options.Draw.validate_options(image, :image, options) do
:ok = MutableOperation.draw_image(image, sub_image, top, left, Map.to_list(options))
{:ok, image}
do_image(image, sub_image, top, left, options)
end
|> maybe_wrap()
end

defp do_image(%MutableImage{} = image, %Vimage{} = sub_image, top, left, options) do
:ok = MutableOperation.draw_image(image, sub_image, top, left, Map.to_list(options))
{:ok, image}
end

@doc """
Expand Down Expand Up @@ -989,10 +982,8 @@ defmodule Image.Draw do
def flood(%image_type{} = image, left, top, options \\ [])
when is_image(image_type) and is_point(left, top) do
with {:ok, options} <- Options.Draw.validate_options(image, :flood, options) do
color = maybe_add_alpha(image, options.color)
flood(image, left, top, color, options.equal)
flood(image, left, top, options.color, options.equal)
end
|> maybe_wrap()
end

defp flood(%Vimage{} = image, left, top, color, equal) do
Expand Down Expand Up @@ -1119,23 +1110,18 @@ defmodule Image.Draw do
def mask(%Vimage{} = image, %Vimage{} = mask, x, y, options)
when is_integer(x) and is_integer(y) and x >= 0 and y >= 0 do
with {:ok, options} <- Options.Draw.validate_options(image, :mask, options) do
color = maybe_add_alpha(image, options.color)

Image.mutate(image, fn mut_img ->
MutableOperation.draw_mask(mut_img, color, mask, x, y)
MutableOperation.draw_mask(mut_img, options.color, mask, x, y)
end)
end
|> maybe_wrap()
end

def mask(%MutableImage{} = image, %Vimage{} = mask, x, y, options)
when is_integer(x) and is_integer(y) and x >= 0 and y >= 0 do
with {:ok, options} <- Options.Draw.validate_options(image, :mask, options) do
color = maybe_add_alpha(image, options.color)
:ok = MutableOperation.draw_mask(image, color, mask, x, y)
:ok = MutableOperation.draw_mask(image, options.color, mask, x, y)
{:ok, image}
end
|> maybe_wrap()
end

@doc """
Expand Down Expand Up @@ -1174,7 +1160,6 @@ defmodule Image.Draw do
MutableOperation.draw_smudge(mut_img, left, top, width, height)
end)
end
|> maybe_wrap()
end

def smudge(%MutableImage{} = image, left, top, width, height, options)
Expand All @@ -1184,59 +1169,5 @@ defmodule Image.Draw do
:ok = MutableOperation.draw_smudge(image, left, top, width, height)
{:ok, image}
end
|> maybe_wrap()
end

## Helpers

@spec maybe_add_alpha(Vimage.t() | MutableImage.t(), Pixel.t()) :: Pixel.t()

# Colors resolved by `Image.Pixel.to_pixel/2` already match the image's
# band count (including any alpha band), so this only adjusts colors that
# arrive unresolved (for example via map-shaped options) and are exactly
# one band short of, or one band over, the image's band count.
@doc false
def maybe_add_alpha(image, color) when is_list(color) do
bands = bands(image)
color_bands = length(color)

cond do
color_bands == bands - 1 && has_alpha?(image) ->
List.insert_at(color, -1, Pixel.max_opacity())

color_bands == bands + 1 && not has_alpha?(image) ->
List.delete_at(color, -1)

true ->
color
end
end

defp bands(%MutableImage{} = image) do
{:ok, {_width, _height, bands}} = MutableImage.shape(image)
bands
end

defp bands(%Vimage{} = image) do
Vimage.bands(image)
end

defp has_alpha?(%MutableImage{} = image) do
case MutableImage.has_alpha?(image) do
{:ok, true} -> true
{:ok, false} -> false
end
end

defp has_alpha?(%Vimage{} = image) do
Vimage.has_alpha?(image)
end

defp maybe_wrap({:ok, result}) do
{:ok, result}
end

defp maybe_wrap(error) do
error
end
end
2 changes: 1 addition & 1 deletion lib/image/options/affine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ defmodule Image.Options.Affine do
"""
@spec validate_options(Vimage.t(), Keyword.t()) ::
{:ok, Keyword.t()} | {:error, Image.error()}
def validate_options(image, options) do
def validate_options(image, options) when is_list(options) do
# A nil `:background` means "unset", i.e. it falls back to the default.
options = Enum.reject(options, &match?({:background, nil}, &1))
options = Keyword.merge(default_options(), options)
Expand Down
6 changes: 1 addition & 5 deletions lib/image/options/blur.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule Image.Options.Blur do
Options list for Image.blur/2

"""
@type blur_options :: [blur_option()] | map()
@type blur_options :: [blur_option()]

@default_blur_sigma 5

Expand All @@ -43,10 +43,6 @@ defmodule Image.Options.Blur do
end
end

def validate_options(%{} = options) do
{:ok, options}
end

defp validate_option({:sigma, sigma}, options) when is_number(sigma) and sigma > 0 do
{:cont, options}
end
Expand Down
2 changes: 1 addition & 1 deletion lib/image/options/blurhash.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule Image.Options.Blurhash do
Options list for Image.Blurhash.encode/2

"""
@type blurhash_options :: [blurhash_option()] | map()
@type blurhash_options :: [blurhash_option()]

@default_x_components 4
@default_y_components 3
Expand Down
Loading
Loading