From c5b19b2681ffcee08da0673fc49967fe2e79641b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Mon, 16 Dec 2024 22:30:46 +0100 Subject: [PATCH] wip background transform --- lib/image_plug/param_parser/twicpics.ex | 1 + .../param_parser/twicpics/hex_color_parser.ex | 60 +++++++ .../param_parser/twicpics/hsl_parser.ex | 161 ++++++++++++++++++ .../param_parser/twicpics/ratio_parser.ex | 3 +- .../param_parser/twicpics/rgb_parser.ex | 153 +++++++++++++++++ .../twicpics/transform/background_parser.ex | 64 +++++++ lib/image_plug/transform/background.ex | 19 +++ lib/image_plug/transform/contain.ex | 6 +- lib/image_plug/transform_state.ex | 3 + lib/image_plug/utils.ex | 62 +++++++ test/param_parser/twicpics_parser_test.exs | 3 + 11 files changed, 532 insertions(+), 3 deletions(-) create mode 100644 lib/image_plug/param_parser/twicpics/hex_color_parser.ex create mode 100644 lib/image_plug/param_parser/twicpics/hsl_parser.ex create mode 100644 lib/image_plug/param_parser/twicpics/rgb_parser.ex create mode 100644 lib/image_plug/param_parser/twicpics/transform/background_parser.ex create mode 100644 lib/image_plug/transform/background.ex diff --git a/lib/image_plug/param_parser/twicpics.ex b/lib/image_plug/param_parser/twicpics.ex index b50a29a..4f89cd6 100644 --- a/lib/image_plug/param_parser/twicpics.ex +++ b/lib/image_plug/param_parser/twicpics.ex @@ -18,6 +18,7 @@ defmodule ImagePlug.ParamParser.Twicpics do "cover" => {ImagePlug.Transform.Cover, Twicpics.Transform.CoverParser}, "cover-min" => {ImagePlug.Transform.Cover, Twicpics.Transform.CoverMinParser}, "cover-max" => {ImagePlug.Transform.Cover, Twicpics.Transform.CoverMaxParser}, + "background" => {ImagePlug.Transform.Background, Twicpics.Transform.BackgroundParser}, "output" => {ImagePlug.Transform.Output, Twicpics.Transform.OutputParser} } diff --git a/lib/image_plug/param_parser/twicpics/hex_color_parser.ex b/lib/image_plug/param_parser/twicpics/hex_color_parser.ex new file mode 100644 index 0000000..e4a5cd8 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics/hex_color_parser.ex @@ -0,0 +1,60 @@ +defmodule ImagePlug.ParamParser.Twicpics.HexColorParser do + @doc """ + Parses a hexadecimal color string and returns RGB(A) values as a tuple. + + ## Examples + + iex> ImagePlug.ParamParser.Twicpics.HexColorParser.parse_hex("76E") + {:ok, {:rgb, 119, 102, 238}} + + iex> ImagePlug.ParamParser.Twicpics.HexColorParser.parse_hex("76E8") + {:ok, {:rgba, 119, 102, 238, 136}} + + iex> ImagePlug.ParamParser.Twicpics.HexColorParser.parse_hex("A68040") + {:ok, {:rgb, 166, 128, 64}} + + iex> ImagePlug.ParamParser.Twicpics.HexColorParser.parse_hex("A6804080") + {:ok, {:rgba, 166, 128, 64, 128}} + + iex> ImagePlug.ParamParser.Twicpics.HexColorParser.parse_hex("invalid") + {:error, :invalid_hex_format} + + """ + def parse(hex, pos) when is_binary(hex) do + case hex |> String.trim() |> String.trim_leading("#") |> String.upcase() do + # Match 3-character shorthand (RGB) + <> -> + {:ok, {:rgb, expand_hex(red), expand_hex(green), expand_hex(blue)}} + + # Match 4-character shorthand (RGBA) + <> -> + {:ok, {:rgba, expand_hex(red), expand_hex(green), expand_hex(blue), expand_hex(alpha)}} + + # Match 6-character full (RRGGBB) + <> -> + {:ok, {:rgb, hex_to_int(red), hex_to_int(green), hex_to_int(blue)}} + + # Match 8-character full (RRGGBBAA) + <> -> + {:ok, {:rgba, hex_to_int(red), hex_to_int(green), hex_to_int(blue), hex_to_int(alpha)}} + + # Invalid format + _ -> + {:error, {:invalid_hex_format, pos: pos}} + end + end + + # Helper to expand shorthand hex (e.g., "A" -> "AA") and convert to integer + defp expand_hex(char) do + char + # Duplicate the single char to make it "AA" + |> String.duplicate(2) + |> hex_to_int() + end + + # Helper to convert hex to integer + defp hex_to_int(hex) do + {int, _} = Integer.parse(hex, 16) + int + end +end diff --git a/lib/image_plug/param_parser/twicpics/hsl_parser.ex b/lib/image_plug/param_parser/twicpics/hsl_parser.ex new file mode 100644 index 0000000..5e53cf9 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics/hsl_parser.ex @@ -0,0 +1,161 @@ +defmodule ImagePlug.ParamParser.Twicpics.HSLParser do + @moduledoc """ + Parses HSL and HSLA color strings. + + Supported formats: + - "hsl(H S L[ / A])" or "hsla(H S L[ / A])" + - "hsl(H,S,L)" or "hsla(H,S,L,A)" (legacy) + + H = 'none' or a float (e.g., 180, 180.5deg) + S = 'none' or a percentage between 0% and 100% + L = 'none' or a percentage between 0% and 100% + A = 'none' or a float between 0 (0%) and 1 (100%) + """ + + @modern_regex ~r/ + \A + (?:hsl|hsla) # Match 'hsl' or 'hsla' + \(\s* # Match opening parenthesis + (?none|\d+(\.\d+)?(deg)?) # Capture H: 'none' or float (optional 'deg') + \s+ # Require whitespace + (?none|\d+(\.\d+)?%?) # Capture S: 'none' or percentage + \s+ # Require whitespace + (?none|\d+(\.\d+)?%?) # Capture L: 'none' or percentage + (?: # Optional alpha with \/ + \s*\/\s* # Require '\/' with optional spaces + (?none|\d+(\.\d+)?%?) # Capture A: 'none', float (0–1), or percentage + )? + \s*\) # Match closing parenthesis + \z # End of string + /xi + + @legacy_regex ~r/ + \A + (?:hsl|hsla) # Match 'hsl' or 'hsla' + \(\s* # Match opening parenthesis + (?none|\d+(\.\d+)?(deg)?) # Capture H: 'none' or float (optional 'deg') + ,\s* # Require comma + (?none|\d+(\.\d+)?%?) # Capture S: 'none' or percentage + ,\s* # Require comma + (?none|\d+(\.\d+)?%?) # Capture L: 'none' or percentage + (?: # Optional alpha with comma + ,\s* # Require comma + (?none|\d+(\.\d+)?%?) # Capture A: 'none', float (0–1), or percentage + )? + \s*\) # Match closing parenthesis + \z # End of string + /xi + + @doc """ + Parses an HSL/HSLA string and returns a map with the parsed components. + + ## Examples + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsl(180 50% 50%)") + {:ok, {:hsl, 180.0, 0.5, 0.5}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsl(none 0% 100%)") + {:ok, {:hsl, 0.0, 0.0, 1.0}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsl(180, 50%, 50%)") + {:ok, {:hsl, 180.0, 0.5, 0.5}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsla(160.5, 50%, 50%, 0.5)") + {:ok, {:hsla, 160.5, 0.5, 0.5, 0.5}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsla(160.5, 50%, 50%, 50%)") + {:ok, {:hsla, 160.5, 0.5, 0.5, 0.5}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsl(180 50% 50% / 0.5)") + {:ok, {:hsla, 180.0, 0.5, 0.5, 0.5}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsl(180 40 50 / 0.6)") + {:ok, {:hsla, 180.0, 0.4, 0.5, 0.6}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsl(180 50% 50% / 40%)") + {:ok, {:hsla, 180.0, 0.5, 0.5, 0.4}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsl(none none none / none)") + {:ok, {:hsl, 0.0, 0.0, 0.0}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsla(none, none, none, none)") + {:ok, {:hsl, 0.0, 0.0, 0.0}} + + iex> ImagePlug.ParamParser.Twicpics.HSLParser.parse("hsl(none, none, none)") + {:ok, {:hsl, 0.0, 0.0, 0.0}} + + """ + def parse(input, pos_offset \\ 0) when is_binary(input) do + cond do + String.match?(input, @modern_regex) -> + parse_with_regex(input, @modern_regex, pos_offset) + + String.match?(input, @legacy_regex) -> + parse_with_regex(input, @legacy_regex, pos_offset) + + true -> + {:error, {:invalid_hsl_or_hsla_format, pos: pos_offset}} + end + end + + defp parse_with_regex(input, regex, pos) do + case Regex.named_captures(regex, input) |> IO.inspect(label: input) do + %{"h" => h, "s" => s, "l" => l, "a" => a} -> + with {:ok, h} <- parse_value(h, :hue, pos), + {:ok, s} <- parse_value(s, :percentage, pos), + {:ok, l} <- parse_value(l, :percentage, pos), + {:ok, a} <- parse_value(a, :alpha, pos) do + if a == nil do + {:ok, {:hsl, h, s, l}} + else + {:ok, {:hsla, h, s, l, a}} + end + + else + {:error, _reason} = error -> error + end + + %{"h" => h, "s" => s, "l" => l} -> + with {:ok, h} <- parse_value(h, :hue, pos), + {:ok, s} <- parse_value(s, :percentage, pos), + {:ok, l} <- parse_value(l, :percentage, pos) do + {:ok, {:hsla, h, s, l}} + else + {:error, _reason} = error -> error + end + + _ -> + {:error, {:invalid_hsl_or_hsla_format, pos: pos}} + end + end + + defp parse_value("none", :hue, _pos), do: {:ok, 0.0} + + defp parse_value(value, :hue, pos) do + case Float.parse(value) do + {num, _} -> {:ok, num} + :error -> {:error, {:invalid_hue, pos: pos}} + end + end + + defp parse_value("none", :percentage, _pos), do: {:ok, 0.0} + + defp parse_value(value, :percentage, pos) do + case Float.parse(value) do + {num, _} when num >= 0 and num <= 100 -> {:ok, num / 100} + _ -> {:error, {:invalid_percentage, pos: pos}} + end + end + + defp parse_value(nil, :alpha, _pos), do: {:ok, nil} + defp parse_value("", :alpha, _pos), do: {:ok, nil} + defp parse_value("none", :alpha, _pos), do: {:ok, nil} + + defp parse_value(value, :alpha, pos) do + case Float.parse(value) do + {num, "%"} when num >= 0 and num <= 100 -> {:ok, num / 100} + {num, _} when num >= 0 and num <= 1 -> {:ok, num} + _ -> {:error, {:invalid_alpha, pos: pos}} + end + end +end diff --git a/lib/image_plug/param_parser/twicpics/ratio_parser.ex b/lib/image_plug/param_parser/twicpics/ratio_parser.ex index c5d221b..5409103 100644 --- a/lib/image_plug/param_parser/twicpics/ratio_parser.ex +++ b/lib/image_plug/param_parser/twicpics/ratio_parser.ex @@ -38,7 +38,8 @@ defmodule ImagePlug.ParamParser.Twicpics.RatioParser do {:error, {:positive_number_required, pos: pos_offset, found: number}} end - {:error, _reason} = error -> error + {:error, _reason} = error -> + error end end end diff --git a/lib/image_plug/param_parser/twicpics/rgb_parser.ex b/lib/image_plug/param_parser/twicpics/rgb_parser.ex new file mode 100644 index 0000000..bc38192 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics/rgb_parser.ex @@ -0,0 +1,153 @@ +defmodule ImagePlug.ParamParser.Twicpics.RGBParser do + @moduledoc """ + Parses RGB and RGBA color strings. + + Supported formats: + - "rgb(H S L[ / A])" or "rgba(H S L[ / A])" + - "rgb(H,S,L)" or "rgba(H,S,L,A)" (legacy) + + H = 'none' or a float (e.g., 180, 180.5deg) + S = 'none' or a percentage between 0% and 100% + L = 'none' or a percentage between 0% and 100% + A = 'none' or a float between 0 (0%) and 1 (100%) + """ + + @modern_regex ~r/ + \A + (?:rgb|rgba) # Match 'rgb' or 'rgba' + \(\s* # Match opening parenthesis + (?none|\d+(\.\d+)?%?) # Capture H: 'none' or float (optional 'deg') + \s+ # Require whitespace + (?none|\d+(\.\d+)?%?) # Capture S: 'none' or percentage + \s+ # Require whitespace + (?none|\d+(\.\d+)?%?) # Capture L: 'none' or percentage + (?: # Optional alpha with \/ + \s*\/\s* # Require '\/' with optional spaces + (?none|\d+(\.\d+)?%?) # Capture A: 'none', float (0–1), or percentage + )? + \s*\) # Match closing parenthesis + \z # End of string + /xi + + @legacy_regex ~r/ + \A + (?:rgb|rgba) # Match 'rgb' or 'rgba' + \(\s* # Match opening parenthesis + (?none|\d+(\.\d+)?%?) # Capture H: 'none' or float (optional 'deg') + ,\s* # Require comma + (?none|\d+(\.\d+)?%?) # Capture S: 'none' or percentage + ,\s* # Require comma + (?none|\d+(\.\d+)?%?) # Capture L: 'none' or percentage + (?: # Optional alpha with comma + ,\s* # Require comma + (?none|\d+(\.\d+)?%?) # Capture A: 'none', float (0–1), or percentage + )? + \s*\) # Match closing parenthesis + \z # End of string + /xi + + @doc """ + Parses an RGB/RGBA string and returns a map with the parsed components. + + ## Examples + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgb(50% 50% 50%)") + {:ok, {:rgb, 128, 128, 128}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgb(none 0% 100%)") + {:ok, {:rgb, 0, 0, 255}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgb(180, 50%, 50%)") + {:ok, {:rgb, 180, 128, 128}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgba(160.5, 50%, 50%, 0.5)") + {:ok, {:rgba, 161, 128, 128, 0.5}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgba(160.5, 50%, 50%, 50%)") + {:ok, {:rgba, 161, 128, 128, 0.5}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgb(180 50% 50% / 0.5)") + {:ok, {:rgba, 180, 128, 128, 0.5}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgb(180 40 50 / 0.6)") + {:ok, {:rgba, 180, 40, 50, 0.6}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgb(180 50% 50% / 40%)") + {:ok, {:rgba, 180, 128, 128, 0.4}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgb(none none none / none)") + {:ok, {:rgb, 0, 0, 0}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgba(none, none, none, none)") + {:ok, {:rgb, 0, 0, 0}} + + iex> ImagePlug.ParamParser.Twicpics.RGBParser.parse("rgb(none, none, none)") + {:ok, {:rgb, 0, 0, 0}} + + """ + def parse(input, pos_offset \\ 0) when is_binary(input) do + cond do + String.match?(input, @modern_regex) -> + parse_with_regex(input, @modern_regex, pos_offset) + + String.match?(input, @legacy_regex) -> + parse_with_regex(input, @legacy_regex, pos_offset) + + true -> + {:error, {:invalid_rgb_or_rgba_format, pos: pos_offset}} + end + end + + defp parse_with_regex(input, regex, pos) do + case Regex.named_captures(regex, input) |> IO.inspect(label: input) do + %{"r" => r, "g" => g, "b" => b, "a" => a} -> + with {:ok, r} <- parse_value(r, :color, pos), + {:ok, g} <- parse_value(g, :color, pos), + {:ok, b} <- parse_value(b, :color, pos), + {:ok, a} <- parse_value(a, :alpha, pos) do + if a == nil do + {:ok, {:rgb, r, g, b}} + else + {:ok, {:rgba, r, g, b, a}} + end + + else + {:error, _reason} = error -> error + end + + %{"r" => r, "g" => g, "b" => b} -> + with {:ok, r} <- parse_value(r, :color, pos), + {:ok, g} <- parse_value(g, :color, pos), + {:ok, b} <- parse_value(b, :color, pos) do + {:ok, {:rgb, r, g, b}} + else + {:error, _reason} = error -> error + end + + _ -> + {:error, {:invalid_rgb_or_rgba_format, pos: pos}} + end + end + + defp parse_value("none", :color, _pos), do: {:ok, 0} + + defp parse_value(value, :color, pos) do + case Float.parse(value) do + {num, "%"} when num >= 0 and num <= 100 -> {:ok, round(255 * (num / 100))} + {num, _} when num >= 0 and num <= 255 -> {:ok, round(num)} + _ -> {:error, {:invalid_color_value, pos: pos}} + end + end + + defp parse_value(nil, :alpha, _pos), do: {:ok, nil} + defp parse_value("", :alpha, _pos), do: {:ok, nil} + defp parse_value("none", :alpha, _pos), do: {:ok, nil} + + defp parse_value(value, :alpha, pos) do + case Float.parse(value) do + {num, "%"} when num >= 0 and num <= 100 -> {:ok, num / 100} + {num, _} when num >= 0 and num <= 1 -> {:ok, num} + _ -> {:error, {:invalid_alpha, pos: pos}} + end + end +end diff --git a/lib/image_plug/param_parser/twicpics/transform/background_parser.ex b/lib/image_plug/param_parser/twicpics/transform/background_parser.ex new file mode 100644 index 0000000..3573724 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics/transform/background_parser.ex @@ -0,0 +1,64 @@ +defmodule ImagePlug.ParamParser.Twicpics.Transform.BackgroundParser do + alias ImagePlug.ParamParser.Twicpics.CoordinatesParser + alias ImagePlug.ParamParser.Twicpics.Utils + alias ImagePlug.ParamParser.Twicpics.HSLParser + alias ImagePlug.ParamParser.Twicpics.RGBParser + alias ImagePlug.ParamParser.Twicpics.HexColorParser + + alias ImagePlug.Transform.Background.BackgroundParams + + @delimiter "+" + @css_colors Image.Color.color_map() + + @doc """ + Parses a string into a `ImagePlug.Transform.Background.BackgroundParams` struct. + """ + def parse(input, pos_offset \\ 0) do + # TODO: use reduce_while to fail on first error + items = + split_with_offset(input) + |> Enum.map(fn {item, local_offset} -> + to_color(maybe_split_alpha(item), pos_offset + local_offset) + end) + + IO.inspect(items) + + {:ok, %BackgroundParams{backgrounds: []}} + end + + defp split_with_offset(string) do + string + |> String.split(@delimiter) + |> Enum.reduce({[], 0}, fn part, {acc, offset} -> + updated_acc = acc ++ [{part, offset}] + new_offset = offset + String.length(part) + String.length(@delimiter) + {updated_acc, new_offset} + end) + # Extract the result list + |> elem(0) + end + + def maybe_split_alpha(item) do + # todo: only split if last part is "\.\d+" + case String.split(item, ".", parts: 2) do + [color, alpha] -> {color, alpha} + [color] -> {color, 100} + end + end + + def to_color({item, alpha}, _pos_offset) when is_map_key(@css_colors, item) do + css_color = Map.get(@css_colors, item) + [r, g, b] = Keyword.get(css_color, :rgb) + {:ok, {:rgb, r, g, b}} + end + + def to_color({item, alpha}, pos) do + cond do + Regex.match?(~r/^hsla?\(/i, item) -> HSLParser.parse(item, pos) + Regex.match?(~r/^rgba?\(/i, item) -> RGBParser.parse(item, pos) + Regex.match?(~r/^#?[a-f0-9]{3,8}$/i, item) -> HexColorParser.parse(item, pos) + item == "blur" -> {:ok, {:blur, 50.0}} + true -> {:error, {:invalid_background, pos: pos}} + end + end +end diff --git a/lib/image_plug/transform/background.ex b/lib/image_plug/transform/background.ex new file mode 100644 index 0000000..8c15f61 --- /dev/null +++ b/lib/image_plug/transform/background.ex @@ -0,0 +1,19 @@ +defmodule ImagePlug.Transform.Background do + @behaviour ImagePlug.Transform + + alias ImagePlug.TransformState + + defmodule BackgroundParams do + @doc """ + The parsed parameters used by `ImagePlug.Transform.Background`. + """ + defstruct [:backgrounds] + + @type t :: %__MODULE__{backgrounds: list(any())} + end + + @impl ImagePlug.Transform + def execute(%TransformState{} = state, %BackgroundParams{backgrounds: backgrounds}) do + state |> IO.inspect(label: :state) + end +end diff --git a/lib/image_plug/transform/contain.ex b/lib/image_plug/transform/contain.ex index 4ea2089..8775925 100644 --- a/lib/image_plug/transform/contain.ex +++ b/lib/image_plug/transform/contain.ex @@ -124,8 +124,10 @@ defmodule ImagePlug.Transform.Contain do 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)} + with {:ok, background_image} <- compose_background(state, width, height), + {:ok, letterboxed_image} <- Image.compose(background_image, state.image) do + {:ok, set_image(state, letterboxed_image)} + else {:error, _reason} = error -> error end end diff --git a/lib/image_plug/transform_state.ex b/lib/image_plug/transform_state.ex index 3ae26e0..88823c1 100644 --- a/lib/image_plug/transform_state.ex +++ b/lib/image_plug/transform_state.ex @@ -2,11 +2,13 @@ defmodule ImagePlug.TransformState do @default_focus {:anchor, :center, :center} defstruct image: nil, + background: [], focus: @default_focus, errors: [], output: :auto, debug: true + @type background() :: :blur | {:rgba, integer(), integer(), integer(), integer()} @type file_format() :: :avif | :webp | :jpeg | :png @type preview_format() :: :blurhash @type output_format() :: :auto | file_format() | preview_format() @@ -23,6 +25,7 @@ defmodule ImagePlug.TransformState do @type t :: %__MODULE__{ image: Vix.Vips.Image.t(), + background: list(background()), focus: {:coordinate, integer(), integer()} | focus_anchor(), errors: keyword(String.t()) | keyword(atom()), output: output_format() diff --git a/lib/image_plug/utils.ex b/lib/image_plug/utils.ex index 92b6a88..d02c1ae 100644 --- a/lib/image_plug/utils.ex +++ b/lib/image_plug/utils.ex @@ -9,6 +9,11 @@ defmodule ImagePlug.Utils do def to_pixels(_length, num) when is_integer(num), do: num def to_pixels(_length, num) when is_float(num), do: round(num) def to_pixels(_length, {:pixels, num}), do: round(num) + + # todo: remove + def to_pixels(length, {:scale, numerator, denominator}), + do: round(length * numerator / denominator) + def to_pixels(length, {:scale, factor}), do: round(length * factor) def to_pixels(length, {:percent, percent}), do: round(percent / 100 * length) @@ -73,4 +78,61 @@ defmodule ImagePlug.Utils do TransformState.set_image(state, image_with_debug_dot) end + + def compose_background(%TransformState{background: []} = state, width, height) do + Image.new(width, height, color: :white) + end + + def compose_background(%TransformState{} = state, width, height) do + handle_result = fn + {:ok, image}, acc -> {:cont, {:ok, [image | acc]}} + {:error, _reason} = error, _acc -> {:halt, error} + end + + background_images = + Enum.reduce_while(state.background, {:ok, []}, fn + {:rgb, r, g, b}, {:ok, acc} -> + Image.new(width, height, color: Image.Color.rgb_color!([r, g, b])) + |> handle_result.(acc) + + {:rgba, r, g, b, a}, {:ok, acc} -> + Image.new(width, height, color: Image.Color.rgba_color!([r, g, b, a])) + |> handle_result.(acc) + + {:blur, sigma}, {:ok, acc} -> + with {:ok, blurred_image} <- Image.blur(state.image, sigma: sigma), + {:ok, cropped_image} <- Image.thumbnail(blurred_image, width, fit: :contain) do + Image.write!(blurred_image, "/Users/hlindset/Downloads/blurred.jpg") + Image.write!(cropped_image, "/Users/hlindset/Downloads/cropped.jpg") + + {:ok, cropped_image} + else + {:error, _reason} = error -> error + end + |> handle_result.(acc) + end) + + compose_images = fn base_image, background_images -> + IO.inspect(base_image, label: :base_image) + IO.inspect(background_images, label: :background_images) + + case background_images do + {:ok, image} -> + Enum.reduce_while(image, {:ok, base_image}, fn background_image, {:ok, acc_image} -> + case Image.compose(acc_image, background_image) do + {:ok, composed} -> {:cont, {:ok, composed}} + {:error, _reason} = error -> {:halt, error} + end + end) + + {:error, _reason} = error -> + error + end + end + + with {:ok, base_image} <- Image.new(width, height, color: :transparent), + {:ok, [composed_bg]} <- compose_images.(base_image, background_images) do + {:ok, composed_bg} + end + end end diff --git a/test/param_parser/twicpics_parser_test.exs b/test/param_parser/twicpics_parser_test.exs index 0795035..e215253 100644 --- a/test/param_parser/twicpics_parser_test.exs +++ b/test/param_parser/twicpics_parser_test.exs @@ -20,6 +20,9 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do doctest ImagePlug.ParamParser.Twicpics.Transform.CoverMinParser doctest ImagePlug.ParamParser.Twicpics.Transform.CoverMaxParser doctest ImagePlug.ParamParser.Twicpics.Transform.OutputParser + doctest ImagePlug.ParamParser.Twicpics.HSLParser + doctest ImagePlug.ParamParser.Twicpics.RGBParser + doctest ImagePlug.ParamParser.Twicpics.HexColorParser defp length_str({:pixels, unit}), do: "#{unit}" defp length_str({:scale, unit_a, unit_b}), do: "(#{unit_a}/#{unit_b})s"