Skip to content
Open
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
1 change: 1 addition & 0 deletions lib/image_plug/param_parser/twicpics.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}

Expand Down
60 changes: 60 additions & 0 deletions lib/image_plug/param_parser/twicpics/hex_color_parser.ex
Original file line number Diff line number Diff line change
@@ -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)
<<red::binary-1, green::binary-1, blue::binary-1>> ->
{:ok, {:rgb, expand_hex(red), expand_hex(green), expand_hex(blue)}}

# Match 4-character shorthand (RGBA)
<<red::binary-1, green::binary-1, blue::binary-1, alpha::binary-1>> ->
{:ok, {:rgba, expand_hex(red), expand_hex(green), expand_hex(blue), expand_hex(alpha)}}

# Match 6-character full (RRGGBB)
<<red::binary-2, green::binary-2, blue::binary-2>> ->
{:ok, {:rgb, hex_to_int(red), hex_to_int(green), hex_to_int(blue)}}

# Match 8-character full (RRGGBBAA)
<<red::binary-2, green::binary-2, blue::binary-2, alpha::binary-2>> ->
{: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
161 changes: 161 additions & 0 deletions lib/image_plug/param_parser/twicpics/hsl_parser.ex
Original file line number Diff line number Diff line change
@@ -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
(?<h>none|\d+(\.\d+)?(deg)?) # Capture H: 'none' or float (optional 'deg')
\s+ # Require whitespace
(?<s>none|\d+(\.\d+)?%?) # Capture S: 'none' or percentage
\s+ # Require whitespace
(?<l>none|\d+(\.\d+)?%?) # Capture L: 'none' or percentage
(?: # Optional alpha with \/
\s*\/\s* # Require '\/' with optional spaces
(?<a>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
(?<h>none|\d+(\.\d+)?(deg)?) # Capture H: 'none' or float (optional 'deg')
,\s* # Require comma
(?<s>none|\d+(\.\d+)?%?) # Capture S: 'none' or percentage
,\s* # Require comma
(?<l>none|\d+(\.\d+)?%?) # Capture L: 'none' or percentage
(?: # Optional alpha with comma
,\s* # Require comma
(?<a>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
3 changes: 2 additions & 1 deletion lib/image_plug/param_parser/twicpics/ratio_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
153 changes: 153 additions & 0 deletions lib/image_plug/param_parser/twicpics/rgb_parser.ex
Original file line number Diff line number Diff line change
@@ -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
(?<r>none|\d+(\.\d+)?%?) # Capture H: 'none' or float (optional 'deg')
\s+ # Require whitespace
(?<g>none|\d+(\.\d+)?%?) # Capture S: 'none' or percentage
\s+ # Require whitespace
(?<b>none|\d+(\.\d+)?%?) # Capture L: 'none' or percentage
(?: # Optional alpha with \/
\s*\/\s* # Require '\/' with optional spaces
(?<a>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
(?<r>none|\d+(\.\d+)?%?) # Capture H: 'none' or float (optional 'deg')
,\s* # Require comma
(?<g>none|\d+(\.\d+)?%?) # Capture S: 'none' or percentage
,\s* # Require comma
(?<b>none|\d+(\.\d+)?%?) # Capture L: 'none' or percentage
(?: # Optional alpha with comma
,\s* # Require comma
(?<a>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
Loading
Loading