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 . ///