diff --git a/lib/image_plug/param_parser/twicpics.ex b/lib/image_plug/param_parser/twicpics.ex index 1116ef8..b50a29a 100644 --- a/lib/image_plug/param_parser/twicpics.ex +++ b/lib/image_plug/param_parser/twicpics.ex @@ -14,6 +14,7 @@ defmodule ImagePlug.ParamParser.Twicpics do "contain" => {ImagePlug.Transform.Contain, Twicpics.Transform.ContainParser}, "contain-min" => {ImagePlug.Transform.Contain, Twicpics.Transform.ContainMinParser}, "contain-max" => {ImagePlug.Transform.Contain, Twicpics.Transform.ContainMaxParser}, + "inside" => {ImagePlug.Transform.Contain, Twicpics.Transform.InsideParser}, "cover" => {ImagePlug.Transform.Cover, Twicpics.Transform.CoverParser}, "cover-min" => {ImagePlug.Transform.Cover, Twicpics.Transform.CoverMinParser}, "cover-max" => {ImagePlug.Transform.Cover, Twicpics.Transform.CoverMaxParser}, diff --git a/lib/image_plug/param_parser/twicpics/transform/contain_max_parser.ex b/lib/image_plug/param_parser/twicpics/transform/contain_max_parser.ex index 964bc8a..9588999 100644 --- a/lib/image_plug/param_parser/twicpics/transform/contain_max_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/contain_max_parser.ex @@ -13,13 +13,20 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.ContainMaxParser do ## Examples iex> ImagePlug.ParamParser.Twicpics.Transform.ContainMaxParser.parse("250x25.5") - {:ok, %ImagePlug.Transform.Contain.ContainParams{width: {:pixels, 250}, height: {:pixels, 25.5}, constraint: :max}} + {:ok, %ImagePlug.Transform.Contain.ContainParams{type: :dimensions, width: {:pixels, 250}, height: {:pixels, 25.5}, constraint: :max, letterbox: false}} """ def parse(input, pos_offset \\ 0) do case SizeParser.parse(input, pos_offset) do {:ok, %{width: width, height: height}} -> - {:ok, %ContainParams{width: width, height: height, constraint: :max}} + {:ok, + %ContainParams{ + type: :dimensions, + width: width, + height: height, + constraint: :max, + letterbox: false + }} {:error, _reason} = error -> Utils.update_error_input(error, input) diff --git a/lib/image_plug/param_parser/twicpics/transform/contain_min_parser.ex b/lib/image_plug/param_parser/twicpics/transform/contain_min_parser.ex index 7ace043..7bb40b0 100644 --- a/lib/image_plug/param_parser/twicpics/transform/contain_min_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/contain_min_parser.ex @@ -13,13 +13,20 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.ContainMinParser do ## Examples iex> ImagePlug.ParamParser.Twicpics.Transform.ContainMinParser.parse("250x25.5") - {:ok, %ImagePlug.Transform.Contain.ContainParams{width: {:pixels, 250}, height: {:pixels, 25.5}, constraint: :min}} + {:ok, %ImagePlug.Transform.Contain.ContainParams{type: :dimensions, width: {:pixels, 250}, height: {:pixels, 25.5}, constraint: :min, letterbox: false}} """ def parse(input, pos_offset \\ 0) do case SizeParser.parse(input, pos_offset) do {:ok, %{width: width, height: height}} -> - {:ok, %ContainParams{width: width, height: height, constraint: :min}} + {:ok, + %ContainParams{ + type: :dimensions, + width: width, + height: height, + constraint: :min, + letterbox: false + }} {:error, _reason} = error -> Utils.update_error_input(error, input) diff --git a/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex b/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex index 0c23935..92d6576 100644 --- a/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex @@ -13,13 +13,20 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.ContainParser do ## Examples iex> ImagePlug.ParamParser.Twicpics.Transform.ContainParser.parse("250x25.5p") - {:ok, %ImagePlug.Transform.Contain.ContainParams{width: {:pixels, 250}, height: {:percent, 25.5}, constraint: :none}} + {:ok, %ImagePlug.Transform.Contain.ContainParams{type: :dimensions, width: {:pixels, 250}, height: {:percent, 25.5}, constraint: :none, letterbox: false}} """ def parse(input, pos_offset \\ 0) do case SizeParser.parse(input, pos_offset) do {:ok, %{width: width, height: height}} -> - {:ok, %ContainParams{width: width, height: height, constraint: :none}} + {:ok, + %ContainParams{ + type: :dimensions, + width: width, + height: height, + constraint: :none, + letterbox: false + }} {:error, _reason} = error -> Utils.update_error_input(error, input) diff --git a/lib/image_plug/param_parser/twicpics/transform/cover_max_parser.ex b/lib/image_plug/param_parser/twicpics/transform/cover_max_parser.ex index c22d612..3f22470 100644 --- a/lib/image_plug/param_parser/twicpics/transform/cover_max_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/cover_max_parser.ex @@ -13,7 +13,7 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.CoverMaxParser do ## Examples iex> ImagePlug.ParamParser.Twicpics.Transform.CoverMaxParser.parse("250x25.5") - {:ok, %ImagePlug.Transform.Cover.CoverParams{width: {:pixels, 250}, height: {:pixels, 25.5}, constraint: :max}} + {:ok, %ImagePlug.Transform.Cover.CoverParams{type: :dimensions, width: {:pixels, 250}, height: {:pixels, 25.5}, constraint: :max}} """ def parse(input, pos_offset \\ 0) do diff --git a/lib/image_plug/param_parser/twicpics/transform/cover_min_parser.ex b/lib/image_plug/param_parser/twicpics/transform/cover_min_parser.ex index 4621d5a..2b26ade 100644 --- a/lib/image_plug/param_parser/twicpics/transform/cover_min_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/cover_min_parser.ex @@ -13,7 +13,7 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.CoverMinParser do ## Examples iex> ImagePlug.ParamParser.Twicpics.Transform.CoverMinParser.parse("250x25.5") - {:ok, %ImagePlug.Transform.Cover.CoverParams{width: {:pixels, 250}, height: {:pixels, 25.5}, constraint: :min}} + {:ok, %ImagePlug.Transform.Cover.CoverParams{type: :dimensions, width: {:pixels, 250}, height: {:pixels, 25.5}, constraint: :min}} """ def parse(input, pos_offset \\ 0) do diff --git a/lib/image_plug/param_parser/twicpics/transform/inside_parser.ex b/lib/image_plug/param_parser/twicpics/transform/inside_parser.ex new file mode 100644 index 0000000..b4e2b52 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics/transform/inside_parser.ex @@ -0,0 +1,55 @@ +defmodule ImagePlug.ParamParser.Twicpics.Transform.InsideParser do + alias ImagePlug.ParamParser.Twicpics.SizeParser + alias ImagePlug.ParamParser.Twicpics.RatioParser + alias ImagePlug.ParamParser.Twicpics.Utils + alias ImagePlug.Transform.Contain.ContainParams + + @doc """ + Parses a string into a `ImagePlug.Transform.Contain.ContainParams` struct. + + Syntax: + * `inside=` + * `inside=` + + ## Examples + + iex> ImagePlug.ParamParser.Twicpics.Transform.InsideParser.parse("250x25.5p") + {:ok, %ImagePlug.Transform.Contain.ContainParams{type: :dimensions, width: {:pixels, 250}, height: {:percent, 25.5}, constraint: :none, letterbox: true}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.InsideParser.parse("1.5:2") + {:ok, %ImagePlug.Transform.Contain.ContainParams{type: :ratio, ratio: {1.5, 2}, letterbox: true}} + """ + + def parse(input, pos_offset \\ 0) do + if String.contains?(input, ":"), + do: parse_ratio(input, pos_offset), + else: parse_size(input, pos_offset) + end + + defp parse_ratio(input, pos_offset) do + case RatioParser.parse(input, pos_offset) do + {:ok, %{width: width, height: height}} -> + {:ok, %ContainParams{type: :ratio, ratio: {width, height}, letterbox: true}} + + {:error, _reason} = error -> + Utils.update_error_input(error, input) + end + end + + defp parse_size(input, pos_offset) do + case SizeParser.parse(input, pos_offset) do + {:ok, %{width: width, height: height}} -> + {:ok, + %ContainParams{ + type: :dimensions, + width: width, + height: height, + constraint: :none, + letterbox: true + }} + + {:error, _reason} = error -> + Utils.update_error_input(error, input) + end + end +end diff --git a/lib/image_plug/transform/contain.ex b/lib/image_plug/transform/contain.ex index 10a8792..4ea2089 100644 --- a/lib/image_plug/transform/contain.ex +++ b/lib/image_plug/transform/contain.ex @@ -8,32 +8,78 @@ defmodule ImagePlug.Transform.Contain do alias ImagePlug.TransformState defmodule ContainParams do - defstruct [:width, :height, :constraint] + defstruct [:type, :ratio, :width, :height, :constraint, :letterbox] @type t :: %__MODULE__{ - width: ImagePlug.imgp_length(), - height: ImagePlug.imgp_length() | :auto, - constraint: :regular | :min | :max + type: :ratio, + ratio: ImagePlug.imgp_ratio(), + letterbox: boolean() } | %__MODULE__{ + type: :dimensions, + width: ImagePlug.imgp_length(), + height: ImagePlug.imgp_length() | :auto, + constraint: :regular | :min | :max, + letterbox: boolean() + } + | %__MODULE__{ + type: :dimensions, width: ImagePlug.imgp_length() | :auto, height: ImagePlug.imgp_length(), - constraint: :regular | :min | :max + constraint: :regular | :min | :max, + letterbox: boolean() } end @impl ImagePlug.Transform def execute(%TransformState{} = state, %ContainParams{ + type: :ratio, + ratio: {ratio_width, ratio_height}, + # Note: Not letterboxing doesn't make sense with this implementation, + # as the transformation would just return the same image + letterbox: letterbox + }) do + # compute target width and height based on the ratio + image_width = image_width(state) + image_height = image_height(state) + + target_ratio = ratio_width / ratio_height + original_ratio = image_width / image_height + + {target_width, target_height} = + if original_ratio > target_ratio do + # wider image: scale height to match ratio + {image_width, round(image_width / target_ratio)} + else + # taller image: scale width to match ratio + {round(image_height * target_ratio), image_height} + end + + execute(state, %ContainParams{ + type: :dimensions, + width: target_width, + height: target_height, + constraint: :none, + letterbox: letterbox + }) + end + + @impl ImagePlug.Transform + def execute(%TransformState{} = state, %ContainParams{ + type: :dimensions, width: width, height: height, - constraint: constraint + constraint: constraint, + letterbox: letterbox }) do {target_width, target_height} = resolve_auto_size(state, width, height) {resize_width, resize_height} = fit_inside(state, target_width, target_height) - case maybe_scale(state, resize_width, resize_height, constraint) do - {:ok, scaled_image} -> state |> set_image(scaled_image) |> reset_focus() + with {:ok, state} <- maybe_scale(state, resize_width, resize_height, constraint), + {:ok, state} <- maybe_add_letterbox(state, letterbox, target_width, target_height) do + state |> reset_focus() + else {:error, error} -> add_error(state, {__MODULE__, error}) end end @@ -52,13 +98,13 @@ defmodule ImagePlug.Transform.Contain do def maybe_scale(%TransformState{} = state, width, height, :min) do if width > image_width(state) or height > image_height(state), do: do_scale(state, width, height), - else: {:ok, state.image} + else: {:ok, state} end def maybe_scale(%TransformState{} = state, width, height, :max) do if width < image_width(state) or height < image_height(state), do: do_scale(state, width, height), - else: {:ok, state.image} + else: {:ok, state} end def maybe_scale(%TransformState{} = state, width, height, _constraint), @@ -67,6 +113,20 @@ defmodule ImagePlug.Transform.Contain do def do_scale(%TransformState{} = state, width, height) do width_scale = width / image_width(state) height_scale = height / image_height(state) - Image.resize(state.image, width_scale, vertical_scale: height_scale) + + case Image.resize(state.image, width_scale, vertical_scale: height_scale) do + {:ok, resized_image} -> {:ok, set_image(state, resized_image)} + {:error, _reason} = error -> error + end + end + + defp maybe_add_letterbox(state, letterbox?, width, height) + defp maybe_add_letterbox(%TransformState{} = state, false, width, height), do: {:ok, state} + + defp maybe_add_letterbox(%TransformState{} = state, true, width, height) do + case Image.embed(state.image, width, height, background_color: :white) do + {:ok, letterboxed_image} -> {:ok, set_image(state, letterboxed_image)} + {:error, _reason} = error -> error + end end end diff --git a/test/param_parser/twicpics_parser_test.exs b/test/param_parser/twicpics_parser_test.exs index 8367d7a..0795035 100644 --- a/test/param_parser/twicpics_parser_test.exs +++ b/test/param_parser/twicpics_parser_test.exs @@ -155,9 +155,11 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do assert {:ok, %Contain.ContainParams{ + type: :dimensions, width: to_result(width), height: to_result(height), - constraint: :none + constraint: :none, + letterbox: false }} == parsed end