diff --git a/README.md b/README.md
index 036058e3a..caf0b3d74 100644
--- a/README.md
+++ b/README.md
@@ -52,8 +52,7 @@ git clone https://github.com/JimBobSquarePants/ImageProcessor
- [ ] CIE XYZ
- [x] CMYK
- [x] HSV
- - [ ] HSLA
- - [ ] RGBAW
+ - [x] HSL
- [x] YCbCr
- Basic shape primitives (Unfinished and could possible be updated by using Vector2, Vector3, etc)
- [x] Rectangle
diff --git a/src/ImageProcessor/Colors/Color.cs b/src/ImageProcessor/Colors/Color.cs
index ced40890e..00906ea48 100644
--- a/src/ImageProcessor/Colors/Color.cs
+++ b/src/ImageProcessor/Colors/Color.cs
@@ -228,125 +228,6 @@ namespace ImageProcessor
}
}
- ///
- /// Allows the implicit conversion of an instance of to a
- /// .
- ///
- /// The instance of to convert.
- ///
- /// An instance of .
- ///
- public static implicit operator Color(Bgra32 color)
- {
- return new Color(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
- }
-
- ///
- /// Allows the implicit conversion of an instance of to a
- /// .
- ///
- /// The instance of to convert.
- ///
- /// An instance of .
- ///
- public static implicit operator Color(Cmyk cmykColor)
- {
- float r = (1 - cmykColor.C) * (1 - cmykColor.K);
- float g = (1 - cmykColor.M) * (1 - cmykColor.K);
- float b = (1 - cmykColor.Y) * (1 - cmykColor.K);
- return new Color(r, g, b);
- }
-
- ///
- /// Allows the implicit conversion of an instance of to a
- /// .
- ///
- /// The instance of to convert.
- ///
- /// An instance of .
- ///
- public static implicit operator Color(YCbCr color)
- {
- float y = color.Y;
- float cb = color.Cb - 128;
- float cr = color.Cr - 128;
-
- float r = (float)(y + (1.402 * cr)) / 255f;
- float g = (float)(y - (0.34414 * cb) - (0.71414 * cr)) / 255f;
- float b = (float)(y + (1.772 * cb)) / 255f;
-
- return new Color(r, g, b);
- }
-
- ///
- /// Allows the implicit conversion of an instance of to a
- /// .
- ///
- /// The instance of to convert.
- ///
- /// An instance of .
- ///
- public static implicit operator Color(Hsv color)
- {
- float s = color.S;
- float v = color.V;
-
- if (Math.Abs(s) < Epsilon)
- {
- return new Color(v, v, v, 1);
- }
-
- float h = (Math.Abs(color.H - 360) < Epsilon) ? 0 : color.H / 60;
- int i = (int)Math.Truncate(h);
- float f = h - i;
-
- float p = v * (1.0f - s);
- float q = v * (1.0f - (s * f));
- float t = v * (1.0f - (s * (1.0f - f)));
-
- float r, g, b;
- switch (i)
- {
- case 0:
- r = v;
- g = t;
- b = p;
- break;
-
- case 1:
- r = q;
- g = v;
- b = p;
- break;
-
- case 2:
- r = p;
- g = v;
- b = t;
- break;
-
- case 3:
- r = p;
- g = q;
- b = v;
- break;
-
- case 4:
- r = t;
- g = p;
- b = v;
- break;
-
- default:
- r = v;
- g = p;
- b = q;
- break;
- }
-
- return new Color(r, g, b);
- }
-
///
/// Computes the product of multiplying a color by a given factor.
///
diff --git a/src/ImageProcessor/Colors/ColorDefinitions.cs b/src/ImageProcessor/Colors/ColorDefinitions.cs
index f3d4844d6..a8cf86947 100644
--- a/src/ImageProcessor/Colors/ColorDefinitions.cs
+++ b/src/ImageProcessor/Colors/ColorDefinitions.cs
@@ -5,6 +5,14 @@
namespace ImageProcessor
{
+ ///
+ /// Represents a four-component color using red, green, blue, and alpha data.
+ /// Each component is stored in premultiplied format multiplied by the alpha component.
+ ///
+ ///
+ /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
+ /// as it avoids the need to create new values for modification operations.
+ ///
public partial struct Color
{
///
diff --git a/src/ImageProcessor/Colors/ColorTransforms.cs b/src/ImageProcessor/Colors/ColorTransforms.cs
new file mode 100644
index 000000000..5c7dc99ef
--- /dev/null
+++ b/src/ImageProcessor/Colors/ColorTransforms.cs
@@ -0,0 +1,229 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessor
+{
+ using System;
+
+ ///
+ /// Represents a four-component color using red, green, blue, and alpha data.
+ /// Each component is stored in premultiplied format multiplied by the alpha component.
+ ///
+ ///
+ /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
+ /// as it avoids the need to create new values for modification operations.
+ ///
+ public partial struct Color
+ {
+ ///
+ /// Allows the implicit conversion of an instance of to a
+ /// .
+ ///
+ /// The instance of to convert.
+ ///
+ /// An instance of .
+ ///
+ public static implicit operator Color(Bgra32 color)
+ {
+ return new Color(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
+ }
+
+ ///
+ /// Allows the implicit conversion of an instance of to a
+ /// .
+ ///
+ /// The instance of to convert.
+ ///
+ /// An instance of .
+ ///
+ public static implicit operator Color(Cmyk cmykColor)
+ {
+ float r = (1 - cmykColor.C) * (1 - cmykColor.K);
+ float g = (1 - cmykColor.M) * (1 - cmykColor.K);
+ float b = (1 - cmykColor.Y) * (1 - cmykColor.K);
+ return new Color(r, g, b);
+ }
+
+ ///
+ /// Allows the implicit conversion of an instance of to a
+ /// .
+ ///
+ /// The instance of to convert.
+ ///
+ /// An instance of .
+ ///
+ public static implicit operator Color(YCbCr color)
+ {
+ float y = color.Y;
+ float cb = color.Cb - 128;
+ float cr = color.Cr - 128;
+
+ float r = (float)(y + (1.402 * cr)) / 255f;
+ float g = (float)(y - (0.34414 * cb) - (0.71414 * cr)) / 255f;
+ float b = (float)(y + (1.772 * cb)) / 255f;
+
+ return new Color(r, g, b);
+ }
+
+ ///
+ /// Allows the implicit conversion of an instance of to a
+ /// .
+ ///
+ /// The instance of to convert.
+ ///
+ /// An instance of .
+ ///
+ public static implicit operator Color(Hsv color)
+ {
+ float s = color.S;
+ float v = color.V;
+
+ if (Math.Abs(s) < Epsilon)
+ {
+ return new Color(v, v, v, 1);
+ }
+
+ float h = (Math.Abs(color.H - 360) < Epsilon) ? 0 : color.H / 60;
+ int i = (int)Math.Truncate(h);
+ float f = h - i;
+
+ float p = v * (1.0f - s);
+ float q = v * (1.0f - (s * f));
+ float t = v * (1.0f - (s * (1.0f - f)));
+
+ float r, g, b;
+ switch (i)
+ {
+ case 0:
+ r = v;
+ g = t;
+ b = p;
+ break;
+
+ case 1:
+ r = q;
+ g = v;
+ b = p;
+ break;
+
+ case 2:
+ r = p;
+ g = v;
+ b = t;
+ break;
+
+ case 3:
+ r = p;
+ g = q;
+ b = v;
+ break;
+
+ case 4:
+ r = t;
+ g = p;
+ b = v;
+ break;
+
+ default:
+ r = v;
+ g = p;
+ b = q;
+ break;
+ }
+
+ return new Color(r, g, b);
+ }
+
+ ///
+ /// Allows the implicit conversion of an instance of to a
+ /// .
+ ///
+ /// The instance of to convert.
+ ///
+ /// An instance of .
+ ///
+ public static implicit operator Color(Hsl color)
+ {
+ float rangedH = color.H / 360f;
+ float r = 0;
+ float g = 0;
+ float b = 0;
+ float s = color.S;
+ float l = color.L;
+
+ if (Math.Abs(l) > Epsilon)
+ {
+ if (Math.Abs(s) < Epsilon)
+ {
+ r = g = b = l;
+ }
+ else
+ {
+ float temp2 = (l < 0.5f) ? l * (1f + s) : l + s - (l * s);
+ float temp1 = (2f * l) - temp2;
+
+ r = GetColorComponent(temp1, temp2, rangedH + (1 / 3f));
+ g = GetColorComponent(temp1, temp2, rangedH);
+ b = GetColorComponent(temp1, temp2, rangedH - (1 / 3f));
+ }
+ }
+
+ return new Color(r, g, b);
+ }
+
+ ///
+ /// Gets the color component from the given values.
+ ///
+ /// The first value.
+ /// The second value.
+ /// The third value.
+ ///
+ /// The .
+ ///
+ private static float GetColorComponent(float first, float second, float third)
+ {
+ third = MoveIntoRange(third);
+ if (third < 1.0 / 6.0)
+ {
+ return first + ((second - first) * 6.0f * third);
+ }
+
+ if (third < 0.5)
+ {
+ return second;
+ }
+
+ if (third < 2.0 / 3.0)
+ {
+ return first + ((second - first) * ((2.0f / 3.0f) - third) * 6.0f);
+ }
+
+ return first;
+ }
+
+ ///
+ /// Moves the specific value within the acceptable range for
+ /// conversion.
+ /// Used for converting colors to this type.
+ ///
+ /// The value to shift.
+ ///
+ /// The .
+ ///
+ private static float MoveIntoRange(float value)
+ {
+ if (value < 0.0)
+ {
+ value += 1.0f;
+ }
+ else if (value > 1.0)
+ {
+ value -= 1.0f;
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/ImageProcessor/Colors/Formats/Bgra32.cs b/src/ImageProcessor/Colors/Colorspaces/Bgra32.cs
similarity index 100%
rename from src/ImageProcessor/Colors/Formats/Bgra32.cs
rename to src/ImageProcessor/Colors/Colorspaces/Bgra32.cs
diff --git a/src/ImageProcessor/Colors/Formats/Cmyk.cs b/src/ImageProcessor/Colors/Colorspaces/Cmyk.cs
similarity index 100%
rename from src/ImageProcessor/Colors/Formats/Cmyk.cs
rename to src/ImageProcessor/Colors/Colorspaces/Cmyk.cs
diff --git a/src/ImageProcessor/Colors/Colorspaces/Hsl.cs b/src/ImageProcessor/Colors/Colorspaces/Hsl.cs
new file mode 100644
index 000000000..091046d87
--- /dev/null
+++ b/src/ImageProcessor/Colors/Colorspaces/Hsl.cs
@@ -0,0 +1,207 @@
+//
+// Copyright (c) James Jackson-South and contributors.
+// Licensed under the Apache License, Version 2.0.
+//
+
+namespace ImageProcessor
+{
+ using System;
+ using System.ComponentModel;
+ using System.Numerics;
+
+ ///
+ /// Represents a Hsl (hue, saturation, lightness) color.
+ ///
+ public struct Hsl : IEquatable
+ {
+ ///
+ /// Represents a that has H, S, and L values set to zero.
+ ///
+ public static readonly Hsl Empty = default(Hsl);
+
+ ///
+ /// The epsilon for comparing floating point numbers.
+ ///
+ private const float Epsilon = 0.0001f;
+
+ ///
+ /// The backing vector for SIMD support.
+ ///
+ private Vector3 backingVector;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The h hue component.
+ /// The s saturation component.
+ /// The l value (lightness) component.
+ public Hsl(float h, float s, float l)
+ {
+ this.backingVector.X = h.Clamp(0, 360);
+ this.backingVector.Y = s.Clamp(0, 1);
+ this.backingVector.Z = l.Clamp(0, 1);
+ }
+
+ ///
+ /// Gets the hue component.
+ /// A value ranging between 0 and 360.
+ ///
+ public float H => this.backingVector.X;
+
+ ///
+ /// Gets the saturation component.
+ /// A value ranging between 0 and 1.
+ ///
+ public float S => this.backingVector.Y;
+
+ ///
+ /// Gets the lightness component.
+ /// A value ranging between 0 and 1.
+ ///
+ public float L => this.backingVector.Z;
+
+ ///
+ /// Gets a value indicating whether this is empty.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsEmpty => this.backingVector.Equals(default(Vector3));
+
+ ///
+ /// Allows the implicit conversion of an instance of to a
+ /// .
+ ///
+ /// The instance of to convert.
+ ///
+ /// An instance of .
+ ///
+ public static implicit operator Hsl(Color color)
+ {
+ color = Color.ToNonPremultiplied(color.Limited);
+ float r = color.R;
+ float g = color.G;
+ float b = color.B;
+
+ float max = Math.Max(r, Math.Max(g, b));
+ float min = Math.Min(r, Math.Min(g, b));
+ float chroma = max - min;
+ float h = 0;
+ float s = 0;
+ float l = (max + min) / 2;
+
+ if (Math.Abs(chroma) < Epsilon)
+ {
+ return new Hsl(0, s, l);
+ }
+
+ if (Math.Abs(r - max) < Epsilon)
+ {
+ h = (g - b) / chroma;
+ }
+ else if (Math.Abs(g - max) < Epsilon)
+ {
+ h = 2 + ((b - r) / chroma);
+ }
+ else if (Math.Abs(b - max) < Epsilon)
+ {
+ h = 4 + ((r - g) / chroma);
+ }
+
+ h *= 60;
+ if (h < 0.0)
+ {
+ h += 360;
+ }
+
+ if (l <= .5f)
+ {
+ s = chroma / (max + min);
+ }
+ else {
+ s = chroma / (2 - chroma);
+ }
+
+ return new Hsl(h, s, l);
+ }
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ ///
+ /// The on the left side of the operand.
+ ///
+ ///
+ /// The on the right side of the operand.
+ ///
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ public static bool operator ==(Hsl left, Hsl right)
+ {
+ return left.Equals(right);
+ }
+
+ ///
+ /// Compares two objects for inequality.
+ ///
+ ///
+ /// The on the left side of the operand.
+ ///
+ ///
+ /// The on the right side of the operand.
+ ///
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ public static bool operator !=(Hsl left, Hsl right)
+ {
+ return !left.Equals(right);
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ if (obj is Hsl)
+ {
+ Hsl color = (Hsl)obj;
+
+ return this.backingVector == color.backingVector;
+ }
+
+ return false;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return GetHashCode(this);
+ }
+
+ ///
+ public override string ToString()
+ {
+ if (this.IsEmpty)
+ {
+ return "Hsl [ Empty ]";
+ }
+
+ return $"Hsl [ H={this.H:#0.##}, S={this.S:#0.##}, L={this.L:#0.##} ]";
+ }
+
+ ///
+ public bool Equals(Hsl other)
+ {
+ return this.backingVector.Equals(other.backingVector);
+ }
+
+ ///
+ /// Returns the hash code for this instance.
+ ///
+ ///
+ /// The instance of to return the hash code for.
+ ///
+ ///
+ /// A 32-bit signed integer that is the hash code for this instance.
+ ///
+ private static int GetHashCode(Hsl color) => color.backingVector.GetHashCode();
+ }
+}
diff --git a/src/ImageProcessor/Colors/Formats/Hsv.cs b/src/ImageProcessor/Colors/Colorspaces/Hsv.cs
similarity index 97%
rename from src/ImageProcessor/Colors/Formats/Hsv.cs
rename to src/ImageProcessor/Colors/Colorspaces/Hsv.cs
index 428db9e2c..430f7a5e5 100644
--- a/src/ImageProcessor/Colors/Formats/Hsv.cs
+++ b/src/ImageProcessor/Colors/Colorspaces/Hsv.cs
@@ -93,11 +93,7 @@ namespace ImageProcessor
return new Hsv(0, s, v);
}
- if (Math.Abs(chroma) < Epsilon)
- {
- h = 0;
- }
- else if (Math.Abs(r - max) < Epsilon)
+ if (Math.Abs(r - max) < Epsilon)
{
h = (g - b) / chroma;
}
diff --git a/src/ImageProcessor/Colors/Formats/YCbCr.cs b/src/ImageProcessor/Colors/Colorspaces/YCbCr.cs
similarity index 100%
rename from src/ImageProcessor/Colors/Formats/YCbCr.cs
rename to src/ImageProcessor/Colors/Colorspaces/YCbCr.cs
diff --git a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs
index 0a3945622..6551b17c7 100644
--- a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs
+++ b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs
@@ -172,6 +172,87 @@ namespace ImageProcessor.Tests
}
}
+ ///
+ /// Tests the implicit conversion from to .
+ ///
+ [Fact]
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation",
+ Justification = "Reviewed. Suppression is OK here.")]
+ public void ColorToHsl()
+ {
+ // Black
+ Color b = new Color(0, 0, 0);
+ Hsl h = b;
+
+ Assert.Equal(0, h.H, 1);
+ Assert.Equal(0, h.S, 1);
+ Assert.Equal(0, h.L, 1);
+
+ // White
+ Color color = new Color(1, 1, 1);
+ Hsl hsl = color;
+
+ Assert.Equal(0f, hsl.H, 1);
+ Assert.Equal(0f, hsl.S, 1);
+ Assert.Equal(1f, hsl.L, 1);
+
+ // Dark moderate pink.
+ Color color2 = new Color(128 / 255f, 64 / 255f, 106 / 255f);
+ Hsl hsl2 = color2;
+
+ Assert.Equal(320.6f, hsl2.H, 1);
+ Assert.Equal(0.33f, hsl2.S, 1);
+ Assert.Equal(0.376f, hsl2.L, 2);
+
+ // Ochre.
+ Color color3 = new Color(204 / 255f, 119 / 255f, 34 / 255f);
+ Hsl hsl3 = color3;
+
+ Assert.Equal(30f, hsl3.H, 1);
+ Assert.Equal(0.714f, hsl3.S, 3);
+ Assert.Equal(0.467f, hsl3.L, 3);
+ }
+
+ ///
+ /// Tests the implicit conversion from to .
+ ///
+ [Fact]
+ public void HslToColor()
+ {
+ // Dark moderate pink.
+ Hsl hsl = new Hsl(320.6f, 0.33f, 0.376f);
+ Color color = hsl;
+
+ Assert.Equal(color.B, 106 / 255f, 1);
+ Assert.Equal(color.G, 64 / 255f, 1);
+ Assert.Equal(color.R, 128 / 255f, 1);
+
+ // Ochre
+ Hsl hsl2 = new Hsl(30, 0.714f, 0.467f);
+ Color color2 = hsl2;
+
+ Assert.Equal(color2.B, 34 / 255f, 1);
+ Assert.Equal(color2.G, 119 / 255f, 1);
+ Assert.Equal(color2.R, 204 / 255f, 1);
+
+ // White
+ Hsl hsl3 = new Hsl(0, 0, 1);
+ Color color3 = hsl3;
+
+ Assert.Equal(color3.B, 1, 1);
+ Assert.Equal(color3.G, 1, 1);
+ Assert.Equal(color3.R, 1, 1);
+
+ // Check others.
+ Random random = new Random(0);
+ for (int i = 0; i < 1000; i++)
+ {
+ Color color4 = new Color(random.Next(1), random.Next(1), random.Next(1));
+ Hsl hsl4 = color4;
+ Assert.Equal(color4, (Color)hsl4);
+ }
+ }
+
///
/// Tests the implicit conversion from to .
///