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
530 changes: 350 additions & 180 deletions lib/image.ex

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions lib/image/options/mapim.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
defmodule Image.Options.Mapim do
@moduledoc """
Options shared by transformations implemented with `Vix.Vips.Operation.mapim/3`.

"""

alias Image.BackgroundColor
alias Vix.Vips.Image, as: Vimage
alias Vix.Vips.Interpolate

@extend_modes [background: :VIPS_EXTEND_BACKGROUND, copy: :VIPS_EXTEND_COPY]

@typedoc """
The interpolators that may be selected with the `:interpolate`
option (descriptions from `vips -l interpolate`):

* `:nearest` - nearest-neighbour interpolation
* `:bilinear` - bilinear interpolation
* `:bicubic` - bicubic interpolation (Catmull-Rom)
* `:lbb` - reduced halo bicubic
* `:nohalo` - edge sharpening resampler with halo reduction
* `:vsqbs` - B-Splines with antialiasing smoothing

`:bilinear` is the default except for `Image.distort/4`, which
retains its existing `:bicubic` default.

"""
@type interpolate ::
:nearest
| :bilinear
| :bicubic
| :lbb
| :nohalo
| :vsqbs

@typedoc "An interpolation option for a mapim-based transformation."
@type interpolate_option :: {:interpolate, interpolate()}

@typedoc "A background fill option for a mapim-based transformation."
@type background_option :: {:background, BackgroundColor.spec() | nil}

@typedoc """
How the interpolator synthesizes the one-pixel fringe just beyond
the content edge when resampling boundary pixels.

* `:background` (the default) blends the fringe toward the
`:background` color. This is correct whenever the transform
exposes canvas.
* `:copy` clamps to the nearest content pixel. Use it when the
content fills the whole canvas, where the default would leave a
faint border along the outermost row and column.

"""
@type extend_mode :: :background | :copy

@typedoc "An interpolation-boundary option for a mapim-based transformation."
@type extend_option :: {:extend_mode, extend_mode()}

@typedoc "Interpolation and background options for transformations that expose canvas."
@type background_options :: [interpolate_option() | background_option()]

@typedoc "Interpolation options for transformations with fixed boundary handling."
@type interpolate_options :: [interpolate_option()]

@typedoc "Interpolation, background and boundary options for a mapim-based transformation."
@type t :: [interpolate_option() | background_option() | extend_option()]

@typep option_name :: :interpolate | :background | :extend_mode

@option_names [:interpolate, :background, :extend_mode]

# The libvips nickname for each interpolator is identical to the
# public atom, so resolution is a simple `Atom.to_string/1`.
@valid_interpolators ~w(nearest bilinear bicubic lbb nohalo vsqbs)a

@doc """
Validates the allowed options for a mapim-based transformation.

All mapim options are allowed by default.
"""
@spec validate_options(Vimage.t(), Keyword.t(), [option_name()]) ::
{:ok, Keyword.t()} | {:error, Image.error()}
def validate_options(image, options, allowed_options \\ @option_names) when is_list(options) do
options =
Keyword.merge(
default_options(allowed_options),
maybe_drop_nil_background(options, allowed_options)
)

case Enum.reduce_while(options, options, &validate_option(&1, image, &2, allowed_options)) do
{:error, value} -> {:error, value}
options -> {:ok, options}
end
end

defp validate_option(
{:interpolate, interpolate} = option,
_image,
options,
allowed_options
)
when interpolate in @valid_interpolators do
if :interpolate in allowed_options do
case Interpolate.new(Atom.to_string(interpolate)) do
{:ok, interpolator} ->
{:cont, Keyword.put(options, :interpolate, interpolator)}

{:error, reason} ->
{:halt, {:error, reason}}
end
else
{:halt, {:error, invalid_option(option)}}
end
end

defp validate_option({:background, background} = option, image, options, allowed_options) do
if :background in allowed_options do
case BackgroundColor.resolve(image, background) do
{:ok, pixel} -> {:cont, Keyword.put(options, :background, pixel)}
{:error, reason} -> {:halt, {:error, reason}}
end
else
{:halt, {:error, invalid_option(option)}}
end
end

defp validate_option({:extend_mode, extend_mode} = option, _image, options, allowed_options) do
if :extend_mode in allowed_options and Keyword.has_key?(@extend_modes, extend_mode) do
options =
options
|> Keyword.delete(:extend_mode)
|> Keyword.put(:extend, Keyword.fetch!(@extend_modes, extend_mode))

{:cont, options}
else
{:halt, {:error, invalid_option(option)}}
end
end

defp validate_option(option, _image, _options, _allowed_options) do
{:halt, {:error, invalid_option(option)}}
end

defp maybe_drop_nil_background(options, allowed_options) do
if :background in allowed_options do
Enum.reject(options, &match?({:background, nil}, &1))
else
options
end
end

defp default_options(allowed_options) do
Keyword.take([interpolate: :bilinear, extend_mode: :background], allowed_options)
end

defp invalid_option(option) do
%Image.Error{
reason: :invalid_option,
value: option,
message: "Invalid option or option value: #{inspect(option)}"
}
end
end
82 changes: 0 additions & 82 deletions lib/image/options/warp_perspective.ex

This file was deleted.

27 changes: 22 additions & 5 deletions test/coverage_wave3_options_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Image.CoverageWave3.Options.Test do
defp rgb(size \\ 20), do: Image.new!(size, size, color: [10, 20, 30])
defp rgba(size \\ 20), do: Image.new!(size, size, color: [10, 20, 30, 255])

describe "Image.Options.WarpPerspective" do
describe "Image.Options.Mapim" do
test "an unknown option returns an error" do
from = [{10, 10}, {90, 12}, {88, 90}, {12, 88}]
to = [{0, 0}, {100, 0}, {100, 100}, {0, 100}]
Expand All @@ -23,13 +23,30 @@ defmodule Image.CoverageWave3.Options.Test do
Image.warp_perspective(rgb(100), from, to, background_color: :not_a_color)
end

test ":extend defaults to background and :extend_mode is renamed for libvips" do
assert {:ok, options} = Image.Options.WarpPerspective.validate_options(rgb(100), [])
test ":extend defaults to background" do
assert {:ok, options} = Image.Options.Mapim.validate_options(rgb(100), [])

assert Keyword.get(options, :extend) == :VIPS_EXTEND_BACKGROUND
refute Keyword.has_key?(options, :extend_mode)
end

test ":interpolate selects an interpolator from the public vocabulary" do
for interpolator <- [:nearest, :bilinear, :bicubic, :lbb, :nohalo, :vsqbs] do
assert {:ok, options} =
Image.Options.Mapim.validate_options(rgb(100), interpolate: interpolator)

assert %Vix.Vips.Interpolate{} = Keyword.get(options, :interpolate)
end
end

test "rejects an unknown :interpolate value" do
assert {:error, %Image.Error{reason: :invalid_option, value: {:interpolate, :unknown}}} =
Image.Options.Mapim.validate_options(rgb(100), interpolate: :unknown)
end

test ":extend_mode is renamed for libvips" do
assert {:ok, options} =
Image.Options.WarpPerspective.validate_options(rgb(100), extend_mode: :copy)
Image.Options.Mapim.validate_options(rgb(100), extend_mode: :copy)

assert Keyword.get(options, :extend) == :VIPS_EXTEND_COPY
refute Keyword.has_key?(options, :extend_mode)
Expand All @@ -38,7 +55,7 @@ defmodule Image.CoverageWave3.Options.Test do
test "only :background and :copy are valid extend modes" do
for extend_mode <- [:repeat, :mirror, :black, :white] do
assert {:error, %Image.Error{reason: :invalid_option}} =
Image.Options.WarpPerspective.validate_options(rgb(100), extend_mode: extend_mode)
Image.Options.Mapim.validate_options(rgb(100), extend_mode: extend_mode)
end
end
end
Expand Down
24 changes: 24 additions & 0 deletions test/distortion_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule Image.Distortion.Test do
use ExUnit.Case, async: true
import Image.TestSupport

@background {[10, 20, 30], alpha: 40}

test "Image.distort/3" do
image_file = "koala.gif"
validate_file = "koala_distorted.png"
Expand All @@ -15,4 +17,26 @@ defmodule Image.Distortion.Test do
# {:ok, _image} = Image.write(distorted, validate_path)
assert_images_equal(distorted, validate_path)
end

test "Image.distort/4 fills coordinates outside the source with background" do
image = rgba_image()

assert {:ok, background} =
Image.distort(image, [{0, 0}], [{50, 0}], background: @background)

assert Image.get_pixel!(background, 0, 50) == [10, 20, 30, 40]
end

test "Image.distort/4 can copy the interpolation fringe" do
image = rgba_image()

assert {:ok, copied} =
Image.distort(image, [{0, 0}], [{1, 0}], extend_mode: :copy)

assert Image.get_pixel!(copied, 0, 50) == [255, 0, 0, 255]
end

defp rgba_image do
Image.new!(100, 100, color: [255, 0, 0, 255])
end
end
Loading
Loading