using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Globalization; using System.Linq; using System.Text; using System.Runtime.CompilerServices; //Adapted from C++: // QColorMatrix // // Extension of the GDI+ struct ColorMatrix. // Adds some member functions so you can actually do something with it. // Use QColorMatrix like ColorMatrix to update the ImmageAttributes class. // Use at your own risk. Comments welcome. // // See: http://www.sgi.com/grafica/matrix/ // http://www.sgi.com/software/opengl/advanced98/notes/node182.html // // (c) 2003, Sjaak Priester, Amsterdam. // mailto:sjaak@sjaakpriester.nl namespace ShomreiTorah.WinForms{ ///A functional replacement for the standard GDI+ ColorMatrix class. public class QColorMatrix { // The luminance weight factors for the RGB color space. // These values are actually preferable to the better known factors of // Y = 0.30R + 0.59G + 0.11B, the formula which is used in color television technique. const float LuminanceRed = 0.3086f; const float LuminanceGreen = 0.6094f; const float LuminanceBlue = 0.0820f; float[][] values; static float[][] CopyMatrixValues(float[][] values) { return new[] { new [] { values[0][0], values[0][1], values[0][2], values[0][3], values[0][4] }, new [] { values[1][0], values[1][1], values[1][2], values[1][3], values[1][4] }, new [] { values[2][0], values[2][1], values[2][2], values[2][3], values[2][4] }, new [] { values[3][0], values[3][1], values[3][2], values[3][3], values[3][4] }, new [] { values[4][0], values[4][1], values[4][2], values[4][3], values[4][4] } }; } #region Basic Interface ///Creates a new QColorMatrix for the identity matrix. public QColorMatrix() { values = new[] { new float[] { 1, 0, 0, 0, 0 }, new float[] { 0, 1, 0, 0, 0 }, new float[] { 0, 0, 1, 0, 0 }, new float[] { 0, 0, 0, 1, 0 }, new float[] { 0, 0, 0, 0, 1 } }; } ///Creates a QColorMatrix by copying a native . public QColorMatrix(ColorMatrix native) { if (native == null) throw new ArgumentNullException("native"); values = new[] { new [] { native.Matrix00, native.Matrix01, native.Matrix02, native.Matrix03, native.Matrix04 }, new [] { native.Matrix10, native.Matrix11, native.Matrix12, native.Matrix13, native.Matrix14 }, new [] { native.Matrix20, native.Matrix21, native.Matrix22, native.Matrix23, native.Matrix24 }, new [] { native.Matrix30, native.Matrix31, native.Matrix32, native.Matrix33, native.Matrix34 }, new [] { native.Matrix40, native.Matrix41, native.Matrix42, native.Matrix43, native.Matrix44 }, }; } ///Creates a QColorMatrix by copying a 5x5 matrix. public QColorMatrix(float[][] values) { if (values == null) throw new ArgumentNullException("values"); if (values.Length != 5 || values.Any(row => row == null || row.Length != 5)) throw new ArgumentException("values must be a 5x5 matrix", "values"); this.values = CopyMatrixValues(values); } ///Gets or sets a single value in the matrix. [SuppressMessage("Microsoft.Design", "CA1023:IndexersShouldNotBeMultidimensional")] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] public float this[int x, int y] { get { return values[y][x]; } set { values[y][x] = value; } } ///Creates a deep copy of this matrix. ///A new QColorMatrix instance with the same values as this instance. public QColorMatrix CreateCopy() { return new QColorMatrix(values); } //The constructor copies its parameter. ///Converts a QColorMatrix to a native . public static implicit operator ColorMatrix(QColorMatrix matrix) { return matrix == null ? null : matrix.ToNative(); } ///Converts this QColorMatrix to a native . public ColorMatrix ToNative() { return new ColorMatrix(values); } ///Converts this QColorMatrix to its string representation. public override string ToString() { return ToString("0.00", CultureInfo.CurrentCulture); } ///Converts this QColorMatrix to its string representation. public string ToString(string format, IFormatProvider provider) { var builder = new StringBuilder(100); for (int y = 0; y < 5; y++) { builder.Append("| "); for (int x = 0; x < 5; x++) { builder.Append(this[x, y].ToString(format, provider)); builder.Append(' '); } builder.AppendLine("|"); } return builder.ToString(0, builder.Length - 2); } #endregion ///Creates an ImageAttributes that contains this matrix. public ImageAttributes CreateAttributes() { var retVal = new ImageAttributes(); retVal.SetColorMatrix(ToNative()); return retVal; } ///Multiplies this matrix by another matrix by prepending the other matrix. ///The original (modified) QColorMatrix. public QColorMatrix MultiplyBy(QColorMatrix other) { return MultiplyBy(other, MatrixOrder.Prepend); } ///Multiplies this matrix by another matrix in the specified order. ///The original (modified) QColorMatrix. public QColorMatrix MultiplyBy(QColorMatrix other, MatrixOrder order) { if (order == MatrixOrder.Append) values = (this * other).values; else values = (other * this).values; return this; } ///Multiplies two matricies. [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "MultiplyBy")] public static QColorMatrix operator *(QColorMatrix left, QColorMatrix right) { if (left == null || right == null) return null; var retVal = new QColorMatrix(); for (int y = 0; y < 5; y++) { for (int x = 0; x < 5; x++) { retVal[x, y] = 0; for (int i = 0; i < 5; i++) retVal[x, y] += left[i, y] * right[x, i]; } } return retVal; } ///Transforms a vector by this matrix. ///An array with four or five elements. ///A transformed vector with four or five elements. If the vector had four elements, the fifth is assumed to be 0. public float[] TransformVector(params float[] vector) { if (vector == null) throw new ArgumentNullException("vector"); if (vector.Length != 4 && vector.Length != 5) throw new ArgumentException("vector must have 4 or 5 elements", "vector"); var retVal = new float[vector.Length]; for (int x = 0; x < retVal.Length; x++) { retVal[x] = 0; for (int y = 0; y < 5; y++) { retVal[x] += this[x, y] * (y == vector.Length ? 1 : vector[y]); } } return retVal; } ///Transforms a color by this matrix. public Color TransformColor(Color color) { var rgba = TransformVector(color.R, color.G, color.B, color.A); return Color.FromArgb(ConstrainByte(rgba[3]), ConstrainByte(rgba[0]), ConstrainByte(rgba[1]), ConstrainByte(rgba[2])); } static int ConstrainByte(float value) { if (value < 0) return 0; if (value > 255) return 255; return (int)value; } #region Per-color methods ///Rotates colors by the given angle. ///The angle to rotate by in degrees. ///The X coordinate that receives the sin. ///The Y coordinate that receives the sin. ///The order to apply the rotation. ///The original (modified) QColorMatrix. QColorMatrix RotateColor(float phi, int x, int y, MatrixOrder order) { phi *= (float)(Math.PI / 180); var multiplier = new QColorMatrix(); multiplier[x, x] = multiplier[y, y] = (float)Math.Cos(phi); var sin = (float)Math.Sin(phi); multiplier[x, y] = sin; multiplier[y, x] = -sin; return MultiplyBy(multiplier, order); } ///Rotates the matrix around the red color axis. ///The angle of rotation in degrees (between -180 and +180). ///The original (modified) QColorMatrix. public QColorMatrix RotateRed(float phi) { return RotateRed(phi, MatrixOrder.Prepend); } ///Rotates the matrix around the red color axis. ///The angle of rotation in degrees (between -180 and +180). ///The order to apply the rotation. ///The original (modified) QColorMatrix. public QColorMatrix RotateRed(float phi, MatrixOrder order) { return RotateColor(phi, 2, 1, order); } ///Rotates the matrix around the green color axis. ///The angle of rotation in degrees (between -180 and +180). ///The original (modified) QColorMatrix. public QColorMatrix RotateGreen(float phi) { return RotateGreen(phi, MatrixOrder.Prepend); } ///Rotates the matrix around the green color axis. ///The angle of rotation in degrees (between -180 and +180). ///The order to apply the rotation. ///The original (modified) QColorMatrix. public QColorMatrix RotateGreen(float phi, MatrixOrder order) { return RotateColor(phi, 0, 2, order); } ///Rotates the matrix around the blue color axis. ///The angle of rotation in degrees (between -180 and +180). ///The original (modified) QColorMatrix. public QColorMatrix RotateBlue(float phi) { return RotateBlue(phi, MatrixOrder.Prepend); } ///Rotates the matrix around the blue color axis. ///The angle of rotation in degrees (between -180 and +180). ///The order to apply the rotation. ///The original (modified) QColorMatrix. public QColorMatrix RotateBlue(float phi, MatrixOrder order) { return RotateColor(phi, 1, 0, order); } QColorMatrix ShearColor(int x, int y1, float d1, int y2, float d2, MatrixOrder order) { var multiplier = new QColorMatrix(); multiplier[x, y1] = d1; multiplier[x, y2] = d2; return MultiplyBy(multiplier, order); } ///Shears the matrix in the red color plane. ///The original (modified) QColorMatrix. public QColorMatrix ShearRed(float green, float blue) { return ShearRed(green, blue, MatrixOrder.Prepend); } ///Shears the matrix in the red color plane. ///The original (modified) QColorMatrix. public QColorMatrix ShearRed(float green, float blue, MatrixOrder order) { return ShearColor(0, 1, green, 2, blue, order); } ///Shears the matrix in the green color plane. ///The original (modified) QColorMatrix. public QColorMatrix ShearGreen(float red, float blue) { return ShearGreen(red, blue, MatrixOrder.Prepend); } ///Shears the matrix in the green color plane. ///The original (modified) QColorMatrix. public QColorMatrix ShearGreen(float red, float blue, MatrixOrder order) { return ShearColor(1, 0, red, 2, blue, order); } ///Shears the matrix in the blue color plane. ///The original (modified) QColorMatrix. public QColorMatrix ShearBlue(float red, float green) { return ShearBlue(red, green, MatrixOrder.Prepend); } ///Shears the matrix in the blue color plane. ///The original (modified) QColorMatrix. public QColorMatrix ShearBlue(float red, float green, MatrixOrder order) { return ShearColor(2, 0, red, 1, green, order); } #endregion ///Translates colors. ///The original (modified) QColorMatrix. public QColorMatrix Translate(float red, float green, float blue, float alpha) { return Translate(red, green, blue, alpha, MatrixOrder.Prepend); } ///Translates colors. ///The original (modified) QColorMatrix. public QColorMatrix Translate(float red, float green, float blue, float alpha, MatrixOrder order) { var multiplier = new QColorMatrix(); multiplier[4, 0] = red; multiplier[4, 1] = green; multiplier[4, 2] = blue; multiplier[4, 3] = alpha; return MultiplyBy(multiplier, order); } ///Scales colors. ///The original (modified) QColorMatrix. public QColorMatrix Scale(float red, float green, float blue, float alpha) { return Scale(red, green, blue, alpha, MatrixOrder.Prepend); } ///Scales colors. ///The original (modified) QColorMatrix. public QColorMatrix Scale(float red, float green, float blue, float alpha, MatrixOrder order) { var multiplier = new QColorMatrix(); multiplier[0, 0] = red; multiplier[1, 1] = green; multiplier[2, 2] = blue; multiplier[3, 3] = alpha; return MultiplyBy(multiplier, order); } ///Sets the saturation. ///The original (modified) QColorMatrix. public QColorMatrix SetSaturation(float saturation) { return SetSaturation(saturation, MatrixOrder.Prepend); } ///Sets the saturation. ///The original (modified) QColorMatrix. public QColorMatrix SetSaturation(float saturation, MatrixOrder order) { //if (saturation < 0 || saturation > 1) throw new ArgumentOutOfRangeException("saturation", "Saturation must be between zero and one"); // For the theory behind this, see the web sites at the top of this file. // In short: if saturation is 1.0f, m becomes the identity matrix, and this matrix is // unchanged. If saturation is 0.0f, each color is scaled by it's luminance weight. float satComplement = 1.0f - saturation; float satComplR = satComplement * LuminanceRed; float satComplG = satComplement * LuminanceGreen; float satComplB = satComplement * LuminanceBlue; var multiplier = new QColorMatrix(new[] { new [] { satComplR + saturation, satComplR, satComplR, 0.0f, 0.0f }, new [] { satComplG, satComplG + saturation, satComplG, 0.0f, 0.0f }, new [] { satComplB, satComplB, satComplB + saturation, 0.0f, 0.0f }, new [] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f }, new [] { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f } }); return MultiplyBy(multiplier, order); } #region RotateHue static class HueMatricies { public static readonly QColorMatrix PreHue = new QColorMatrix(); public static readonly QColorMatrix PostHue = new QColorMatrix(); const float greenRotation = 35.0f; //const float greenRotation = 39.182655f; // NOTE: theoretically, greenRotation should have the value of 39.182655 degrees, // being the angle for which the sine is 1/(sqrt(3)), and the cosine is sqrt(2/3). // However, I found that using a slightly smaller angle works better. // In particular, the greys in the image are not visibly affected with the smaller // angle, while they deviate a little bit with the theoretical value. // An explanation escapes me for now. // If you rather stick with the theory, change the comments in the previous lines. [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Impossible, beforefieldinit")] static HueMatricies() { // Rotating the hue of an image is a rather convoluted task, involving several matrix // multiplications. For efficiency, we prepare two static matrices. // This is by far the most complicated part of this class. For the background // theory, refer to the sgi-sites mentioned at the top of this file. // Prepare the preHue matrix. // Rotate the grey vector in the green plane. PreHue.RotateRed(45.0f); // Next, rotate it again in the green plane, so it coincides with the blue axis. PreHue.RotateGreen(-greenRotation, MatrixOrder.Append); // Hue rotations keep the color luminations constant, so that only the hues change // visible. To accomplish that, we shear the blue plane. var lum = PreHue.TransformVector(LuminanceRed, LuminanceGreen, LuminanceBlue, 1.0f); // Transform the luminance vector. // Calculate the shear factors for red and green. float red = lum[0] / lum[2]; float green = lum[1] / lum[2]; // Shear the blue plane. PreHue.ShearBlue(red, green, MatrixOrder.Append); // Prepare the postHue matrix. This holds the opposite transformations of the // preHue matrix. In fact, postHue is the inversion of preHue. PostHue.ShearBlue(-red, -green); PostHue.RotateGreen(greenRotation, MatrixOrder.Append); PostHue.RotateRed(-45.0f, MatrixOrder.Append); } } ///Rotates the hue around the grey axis, keeping luminance fixed. ///The original (modified) QColorMatrix. public QColorMatrix RotateHue(float phi) { return MultiplyBy(HueMatricies.PreHue, MatrixOrder.Append) .RotateBlue(phi, MatrixOrder.Append) .MultiplyBy(HueMatricies.PostHue, MatrixOrder.Append); } #endregion } }