diff --git a/src/ImageProcessor/Colors/Bgra.cs b/src/ImageProcessor/Colors/Bgra.cs index db814cf6b..eb9bc7e26 100644 --- a/src/ImageProcessor/Colors/Bgra.cs +++ b/src/ImageProcessor/Colors/Bgra.cs @@ -273,6 +273,24 @@ namespace ImageProcessor return new Bgra(b, g, r, 255); } + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator Bgra(Cmyk cmykColor) + { + int red = Convert.ToInt32((1 - (cmykColor.C / 100)) * (1 - (cmykColor.K / 100)) * 255.0); + int green = Convert.ToInt32((1 - (cmykColor.M / 100)) * (1 - (cmykColor.K / 100)) * 255.0); + int blue = Convert.ToInt32((1 - (cmykColor.Y / 100)) * (1 - (cmykColor.K / 100)) * 255.0); + return new Bgra(blue.ToByte(), green.ToByte(), red.ToByte()); + } + /// /// Compares two objects. The result specifies whether the values /// of the , , , and diff --git a/src/ImageProcessor/Colors/Cmyk.cs b/src/ImageProcessor/Colors/Cmyk.cs new file mode 100644 index 000000000..42fd96ba0 --- /dev/null +++ b/src/ImageProcessor/Colors/Cmyk.cs @@ -0,0 +1,227 @@ +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessor +{ + using System; + using System.ComponentModel; + + /// + /// Represents an CMYK (cyan, magenta, yellow, keyline) color. + /// + public struct Cmyk : IEquatable + { + /// + /// Represents a that has C, M, Y, and K values set to zero. + /// + public static readonly Cmyk Empty = default(Cmyk); + + /// + /// Gets the cyan color component. + /// + /// A value ranging between 0 and 100. + public readonly float C; + + /// + /// Gets the magenta color component. + /// + /// A value ranging between 0 and 100. + public readonly float M; + + /// + /// Gets the yellow color component. + /// + /// A value ranging between 0 and 100. + public readonly float Y; + + /// + /// Gets the keyline black color component. + /// + /// A value ranging between 0 and 100. + public readonly float K; + + /// + /// The epsilon for comparing floating point numbers. + /// + private const float Epsilon = 0.0001f; + + /// + /// Initializes a new instance of the struct. + /// + /// The cyan component. + /// The magenta component. + /// The yellow component. + /// The keyline black component. + public Cmyk(float cyan, float magenta, float yellow, float keyline) + { + this.C = Clamp(cyan); + this.M = Clamp(magenta); + this.Y = Clamp(yellow); + this.K = Clamp(keyline); + } + + /// + /// Gets a value indicating whether this is empty. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsEmpty => Math.Abs(this.C) < Epsilon + && Math.Abs(this.M) < Epsilon + && Math.Abs(this.Y) < Epsilon + && Math.Abs(this.K) < Epsilon; + + /// + /// Allows the implicit conversion of an instance of to a + /// . + /// + /// + /// The instance of to convert. + /// + /// + /// An instance of . + /// + public static implicit operator Cmyk(Bgra color) + { + float c = (255f - color.R) / 255; + float m = (255f - color.G) / 255; + float y = (255f - color.B) / 255; + + float k = Math.Min(c, Math.Min(m, y)); + + if (Math.Abs(k - 1.0) <= Epsilon) + { + return new Cmyk(0, 0, 0, 100); + } + + c = ((c - k) / (1 - k)) * 100; + m = ((m - k) / (1 - k)) * 100; + y = ((y - k) / (1 - k)) * 100; + + return new Cmyk(c, m, y, k * 100); + } + + /// + /// Compares two objects. The result specifies whether the values + /// of the , , , and + /// properties of the two objects are equal. + /// + /// + /// 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 ==(Cmyk left, Cmyk right) + { + return left.Equals(right); + } + + /// + /// Compares two objects. The result specifies whether the values + /// of the , , , and + /// properties of the two objects are unequal. + /// + /// + /// 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 !=(Cmyk left, Cmyk right) + { + return !left.Equals(right); + } + + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// + /// true if and this instance are the same type and represent the same value; otherwise, false. + /// + /// Another object to compare to. + public override bool Equals(object obj) + { + if (obj is Cmyk) + { + Cmyk color = (Cmyk)obj; + + return Math.Abs(this.C - color.C) < Epsilon + && Math.Abs(this.M - color.M) < Epsilon + && Math.Abs(this.Y - color.Y) < Epsilon + && Math.Abs(this.K - color.K) < Epsilon; + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + public override int GetHashCode() + { + unchecked + { + int hashCode = this.C.GetHashCode(); + hashCode = (hashCode * 397) ^ this.M.GetHashCode(); + hashCode = (hashCode * 397) ^ this.Y.GetHashCode(); + hashCode = (hashCode * 397) ^ this.K.GetHashCode(); + return hashCode; + } + } + + /// + /// Returns the fully qualified type name of this instance. + /// + /// + /// A containing a fully qualified type name. + /// + public override string ToString() + { + if (this.IsEmpty) + { + return "Cmyk [Empty]"; + } + + return $"Cmyk [ C={this.C:#0.##}, M={this.M:#0.##}, Y={this.Y:#0.##}, K={this.K:#0.##}]"; + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// True if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(Cmyk other) + { + return Math.Abs(this.C - other.C) < Epsilon + && Math.Abs(this.M - other.M) < Epsilon + && Math.Abs(this.Y - other.Y) < Epsilon + && Math.Abs(this.K - other.Y) < Epsilon; + } + + /// + /// Checks the range of the given value to ensure that it remains within the acceptable boundaries. + /// + /// + /// The value to check. + /// + /// + /// The sanitized . + /// + private static float Clamp(float value) + { + return value.Clamp(0, 100); + } + } +} diff --git a/src/ImageProcessor/Colors/Hsv.cs b/src/ImageProcessor/Colors/Hsv.cs index 352556fae..94248d969 100644 --- a/src/ImageProcessor/Colors/Hsv.cs +++ b/src/ImageProcessor/Colors/Hsv.cs @@ -149,7 +149,7 @@ namespace ImageProcessor /// The on the right side of the operand. /// /// - /// True if the current left is equal to the parameter; otherwise, false. + /// True if the current left is unequal to the parameter; otherwise, false. /// public static bool operator !=(Hsv left, Hsv right) { diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index 1207c97ff..7843b88bf 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -40,6 +40,7 @@ + diff --git a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs index a5d584cc8..7b6de0e0d 100644 --- a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs +++ b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs @@ -160,12 +160,94 @@ namespace ImageProcessor.Tests // Check others. Random random = new Random(0); - for (var i = 0; i < 1000; i++) + for (int i = 0; i < 1000; i++) { Bgra bgra4 = new Bgra((byte)random.Next(255), (byte)random.Next(255), (byte)random.Next(255)); Hsv hsb4 = bgra4; Assert.Equal(bgra4, (Bgra)hsb4); } } + + /// + /// Tests the implicit conversion from to . + /// + [Fact] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", + Justification = "Reviewed. Suppression is OK here.")] + public void BgrToCmyk() + { + // White + Bgra color = new Bgra(255, 255, 255, 255); + Cmyk cmyk = color; + + Assert.Equal(0, cmyk.C); + Assert.Equal(0, cmyk.M); + Assert.Equal(0, cmyk.Y); + Assert.Equal(0, cmyk.K); + + // Black + Bgra color2 = new Bgra(0, 0, 0, 255); + Cmyk cmyk2 = color2; + Assert.Equal(0, cmyk2.C); + Assert.Equal(0, cmyk2.M); + Assert.Equal(0, cmyk2.Y); + Assert.Equal(100, cmyk2.K); + + // Grey + Bgra color3 = new Bgra(128, 128, 128, 255); + Cmyk cmyk3 = color3; + Assert.Equal(0, cmyk3.C); + Assert.Equal(0, cmyk3.M); + Assert.Equal(0, cmyk3.Y); + Assert.Equal(49.8, cmyk3.K, 1); // Checked with other tools. + + // Cyan + Bgra color4 = new Bgra(255, 255, 0, 255); + Cmyk cmyk4 = color4; + Assert.Equal(100, cmyk4.C); + Assert.Equal(0, cmyk4.M); + Assert.Equal(0, cmyk4.Y); + Assert.Equal(0, cmyk4.K); + } + + /// + /// Tests the implicit conversion from to . + /// + [Fact] + public void CmykToBgr() + { + // Dark moderate pink. + Cmyk cmyk = new Cmyk(49.8f, 74.9f, 58.4f, 0); + Bgra bgra = cmyk; + + Assert.Equal(bgra.B, 106); + Assert.Equal(bgra.G, 64); + Assert.Equal(bgra.R, 128); + + // Ochre + Cmyk cmyk2 = new Cmyk(20, 53.3f, 86.7f, 0); + Bgra bgra2 = cmyk2; + + Assert.Equal(bgra2.B, 34); + Assert.Equal(bgra2.G, 119); + Assert.Equal(bgra2.R, 204); + + // White + Cmyk cmyk3 = new Cmyk(0, 0, 0, 0); + Bgra bgra3 = cmyk3; + + Assert.Equal(bgra3.B, 255); + Assert.Equal(bgra3.G, 255); + Assert.Equal(bgra3.R, 255); + + // Check others. + Random random = new Random(0); + for (int i = 0; i < 1000; i++) + { + Bgra bgra4 = new Bgra((byte)random.Next(255), (byte)random.Next(255), (byte)random.Next(255)); + Cmyk cmyk4 = bgra4; + Assert.Equal(bgra4, (Bgra)cmyk4); + } + } } }