1+ // Copyright (c) Lookup Foundation and Contributors
2+ //
3+ // Permission to use, copy, modify, and distribute this software in
4+ // object code form for any purpose and without fee is hereby granted,
5+ // provided that the above copyright notice appears in all copies and
6+ // that both that copyright notice and the limited warranty and
7+ // restricted rights notice below appear in all supporting
8+ // documentation.
9+ //
10+ // THIS PROGRAM IS PROVIDED "AS IS" AND WITH ALL FAULTS.
11+ // NO IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE IS PROVIDED.
12+ // THERE IS NO GUARANTEE THAT THE OPERATION OF THE PROGRAM WILL BE
13+ // UNINTERRUPTED OR ERROR FREE.
14+
15+ using Color = System . Drawing . Color ;
16+
17+ namespace RevitDevTool . Controls ;
18+
19+ /// <summary>
20+ /// Helper class to easier work with color formats
21+ /// </summary>
22+ /// <remarks>
23+ /// Implementation: https://github.com/microsoft/PowerToys/blob/main/src/common/ManagedCommon/ColorFormatHelper.cs
24+ /// </remarks>
25+ public static class ColorFormatUtils
26+ {
27+ /// <summary>
28+ /// Return a drawing color of a given <see cref="System.Windows.Media.Color"/>
29+ /// </summary>
30+ public static Color GetDrawingColor ( this System . Windows . Media . Color color )
31+ {
32+ return Color . FromArgb ( 1 , color . R , color . G , color . B ) ;
33+ }
34+
35+ /// <summary>
36+ /// Convert a given <see cref="Color"/> to a CMYK color (cyan, magenta, yellow, black key)
37+ /// </summary>
38+ /// <param name="color">The <see cref="Color"/> to convert</param>
39+ /// <returns>The cyan[0..1], magenta[0..1], yellow[0..1] and black key[0..1] of the converted color</returns>
40+ public static ( double Cyan , double Magenta , double Yellow , double BlackKey ) ConvertToCmykColor ( Color color )
41+ {
42+ // special case for black (avoid division by zero)
43+ if ( color is { R : 0 , G : 0 , B : 0 } )
44+ {
45+ return ( 0d , 0d , 0d , 1d ) ;
46+ }
47+
48+ var red = color . R / 255d ;
49+ var green = color . G / 255d ;
50+ var blue = color . B / 255d ;
51+
52+ var blackKey = 1d - Math . Max ( Math . Max ( red , green ) , blue ) ;
53+
54+ // special case for black (avoid division by zero)
55+ if ( 1d - blackKey == 0d )
56+ {
57+ return ( 0d , 0d , 0d , 1d ) ;
58+ }
59+
60+ var cyan = ( 1d - red - blackKey ) / ( 1d - blackKey ) ;
61+ var magenta = ( 1d - green - blackKey ) / ( 1d - blackKey ) ;
62+ var yellow = ( 1d - blue - blackKey ) / ( 1d - blackKey ) ;
63+
64+ return ( cyan , magenta , yellow , blackKey ) ;
65+ }
66+
67+ /// <summary>
68+ /// Convert a given <see cref="Color"/> to a HSB color (hue, saturation, brightness)
69+ /// </summary>
70+ /// <param name="color">The <see cref="Color"/> to convert</param>
71+ /// <returns>The hue [0°..360°], saturation [0..1] and brightness [0..1] of the converted color</returns>
72+ public static ( double Hue , double Saturation , double Brightness ) ConvertToHsbColor ( Color color )
73+ {
74+ // HSB and HSV represents the same color space
75+ return ConvertToHsvColor ( color ) ;
76+ }
77+
78+ /// <summary>
79+ /// Convert a given <see cref="Color"/> to a HSV color (hue, saturation, value)
80+ /// </summary>
81+ /// <param name="color">The <see cref="Color"/> to convert</param>
82+ /// <returns>The hue [0°..360°], saturation [0..1] and value [0..1] of the converted color</returns>
83+ public static ( double Hue , double Saturation , double Value ) ConvertToHsvColor ( Color color )
84+ {
85+ var min = Math . Min ( Math . Min ( color . R , color . G ) , color . B ) / 255d ;
86+ var max = Math . Max ( Math . Max ( color . R , color . G ) , color . B ) / 255d ;
87+
88+ return ( color . GetHue ( ) , max == 0d ? 0d : ( max - min ) / max , max ) ;
89+ }
90+
91+ /// <summary>
92+ /// Convert a given <see cref="Color"/> to a HSI color (hue, saturation, intensity)
93+ /// </summary>
94+ /// <param name="color">The <see cref="Color"/> to convert</param>
95+ /// <returns>The hue [0°..360°], saturation [0..1] and intensity [0..1] of the converted color</returns>
96+ public static ( double Hue , double Saturation , double Intensity ) ConvertToHsiColor ( Color color )
97+ {
98+ // special case for black
99+ if ( color . R == 0 && color . G == 0 && color . B == 0 )
100+ {
101+ return ( 0d , 0d , 0d ) ;
102+ }
103+
104+ var red = color . R / 255d ;
105+ var green = color . G / 255d ;
106+ var blue = color . B / 255d ;
107+
108+ var intensity = ( red + green + blue ) / 3d ;
109+
110+ var min = Math . Min ( Math . Min ( color . R , color . G ) , color . B ) / 255d ;
111+
112+ return ( color . GetHue ( ) , 1d - ( min / intensity ) , intensity ) ;
113+ }
114+
115+ /// <summary>
116+ /// Convert a given <see cref="Color"/> to a HSL color (hue, saturation, lightness)
117+ /// </summary>
118+ /// <param name="color">The <see cref="Color"/> to convert</param>
119+ /// <returns>The hue [0°..360°], saturation [0..1] and lightness [0..1] values of the converted color</returns>
120+ public static ( double Hue , double Saturation , double Lightness ) ConvertToHslColor ( Color color )
121+ {
122+ var min = Math . Min ( Math . Min ( color . R , color . G ) , color . B ) / 255d ;
123+ var max = Math . Max ( Math . Max ( color . R , color . G ) , color . B ) / 255d ;
124+
125+ var lightness = ( max + min ) / 2d ;
126+
127+ if ( lightness == 0d || Math . Abs ( min - max ) < 1e-9 )
128+ {
129+ return ( color . GetHue ( ) , 0d , lightness ) ;
130+ }
131+
132+ if ( lightness is > 0d and <= 0.5d )
133+ {
134+ return ( color . GetHue ( ) , ( max - min ) / ( max + min ) , lightness ) ;
135+ }
136+
137+ return ( color . GetHue ( ) , ( max - min ) / ( 2d - ( max + min ) ) , lightness ) ;
138+ }
139+
140+ /// <summary>
141+ /// Convert a given <see cref="Color"/> to a HWB color (hue, whiteness, blackness)
142+ /// </summary>
143+ /// <param name="color">The <see cref="Color"/> to convert</param>
144+ /// <returns>The hue [0°..360°], whiteness [0..1] and blackness [0..1] of the converted color</returns>
145+ public static ( double Hue , double Whiteness , double Blackness ) ConvertToHwbColor ( Color color )
146+ {
147+ var min = Math . Min ( Math . Min ( color . R , color . G ) , color . B ) / 255d ;
148+ var max = Math . Max ( Math . Max ( color . R , color . G ) , color . B ) / 255d ;
149+
150+ return ( color . GetHue ( ) , min , 1 - max ) ;
151+ }
152+
153+ /// <summary>
154+ /// Convert a given <see cref="Color"/> to a CIE LAB color (LAB)
155+ /// </summary>
156+ /// <param name="color">The <see cref="Color"/> to convert</param>
157+ /// <returns>The lightness [0..100] and two chromaticities [-128..127]</returns>
158+ public static ( double Lightness , double ChromaticityA , double ChromaticityB ) ConvertToCielabColor ( Color color )
159+ {
160+ var xyz = ConvertToCiexyzColor ( color ) ;
161+ var lab = GetCielabColorFromCieXyz ( xyz . X , xyz . Y , xyz . Z ) ;
162+
163+ return lab ;
164+ }
165+
166+ /// <summary>
167+ /// Convert a given <see cref="Color"/> to a CIE XYZ color (XYZ)
168+ /// The constants of the formula matches this Wikipedia page, but at a higher precision:
169+ /// https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ)
170+ /// This page provides a method to calculate the constants:
171+ /// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
172+ /// </summary>
173+ /// <param name="color">The <see cref="Color"/> to convert</param>
174+ /// <returns>The X [0..1], Y [0..1] and Z [0..1]</returns>
175+ public static ( double X , double Y , double Z ) ConvertToCiexyzColor ( Color color )
176+ {
177+ var r = color . R / 255d ;
178+ var g = color . G / 255d ;
179+ var b = color . B / 255d ;
180+
181+ // inverse companding, gamma correction must be undone
182+ var rLinear = ( r > 0.04045 ) ? Math . Pow ( ( r + 0.055 ) / 1.055 , 2.4 ) : ( r / 12.92 ) ;
183+ var gLinear = ( g > 0.04045 ) ? Math . Pow ( ( g + 0.055 ) / 1.055 , 2.4 ) : ( g / 12.92 ) ;
184+ var bLinear = ( b > 0.04045 ) ? Math . Pow ( ( b + 0.055 ) / 1.055 , 2.4 ) : ( b / 12.92 ) ;
185+
186+ return (
187+ ( rLinear * 0.41239079926595948 ) + ( gLinear * 0.35758433938387796 ) + ( bLinear * 0.18048078840183429 ) ,
188+ ( rLinear * 0.21263900587151036 ) + ( gLinear * 0.71516867876775593 ) + ( bLinear * 0.07219231536073372 ) ,
189+ ( rLinear * 0.01933081871559185 ) + ( gLinear * 0.11919477979462599 ) + ( bLinear * 0.95053215224966058 )
190+ ) ;
191+ }
192+
193+ /// <summary>
194+ /// Convert a CIE XYZ color <see cref="double"/> to a CIE LAB color (LAB) adapted to sRGB D65 white point
195+ /// The constants of the formula used come from this wikipedia page:
196+ /// https://en.wikipedia.org/wiki/CIELAB_color_space#Converting_between_CIELAB_and_CIEXYZ_coordinates
197+ /// </summary>
198+ /// <param name="x">The <see cref="x"/> represents a mix of the three CIE RGB curves</param>
199+ /// <param name="y">The <see cref="y"/> represents the luminance</param>
200+ /// <param name="z">The <see cref="z"/> is quasi-equal to blue (of CIE RGB)</param>
201+ /// <returns>The lightness [0..100] and two chromaticities [-128..127]</returns>
202+ private static ( double Lightness , double ChromaticityA , double ChromaticityB ) GetCielabColorFromCieXyz ( double x , double y , double z )
203+ {
204+ // sRGB reference white (x=0.3127, y=0.3290, Y=1.0), actually CIE Standard Illuminant D65 truncated to 4 decimal places,
205+ // then converted to XYZ using the formula:
206+ // X = x * (Y / y)
207+ // Y = Y
208+ // Z = (1 - x - y) * (Y / y)
209+ const double xN = 0.9504559270516717 ;
210+ const double yN = 1.0 ;
211+ const double zN = 1.0890577507598784 ;
212+
213+ // Scale XYZ values relative to reference white
214+ x /= xN ;
215+ y /= yN ;
216+ z /= zN ;
217+
218+ // XYZ to CIELab transformation
219+ const double delta = 6d / 29 ;
220+ var m = ( 1d / 3 ) * Math . Pow ( delta , - 2 ) ;
221+ var t = Math . Pow ( delta , 3 ) ;
222+
223+ var fx = ( x > t ) ? Math . Pow ( x , 1.0 / 3.0 ) : ( x * m ) + ( 16.0 / 116.0 ) ;
224+ var fy = ( y > t ) ? Math . Pow ( y , 1.0 / 3.0 ) : ( y * m ) + ( 16.0 / 116.0 ) ;
225+ var fz = ( z > t ) ? Math . Pow ( z , 1.0 / 3.0 ) : ( z * m ) + ( 16.0 / 116.0 ) ;
226+
227+ var l = ( 116 * fy ) - 16 ;
228+ var a = 500 * ( fx - fy ) ;
229+ var b = 200 * ( fy - fz ) ;
230+
231+ return ( l , a , b ) ;
232+ }
233+
234+ /// <summary>
235+ /// Convert a given <see cref="Color"/> to a natural color (hue, whiteness, blackness)
236+ /// </summary>
237+ /// <param name="color">The <see cref="Color"/> to convert</param>
238+ /// <returns>The hue, whiteness [0..1] and blackness [0..1] of the converted color</returns>
239+ public static ( string Hue , double Whiteness , double Blackness ) ConvertToNaturalColor ( Color color )
240+ {
241+ var min = Math . Min ( Math . Min ( color . R , color . G ) , color . B ) / 255d ;
242+ var max = Math . Max ( Math . Max ( color . R , color . G ) , color . B ) / 255d ;
243+
244+ return ( GetNaturalColorFromHue ( color . GetHue ( ) ) , min , 1 - max ) ;
245+ }
246+
247+ /// <summary>
248+ /// Return the natural color for the given hue value
249+ /// </summary>
250+ /// <param name="hue">The hue value to convert</param>
251+ /// <returns>A natural color</returns>
252+ private static string GetNaturalColorFromHue ( double hue )
253+ {
254+ return hue switch
255+ {
256+ < 60d => $ "R{ Math . Round ( hue / 0.6d , 0 ) } ",
257+ < 120d => $ "Y{ Math . Round ( ( hue - 60d ) / 0.6d , 0 ) } ",
258+ < 180d => $ "G{ Math . Round ( ( hue - 120d ) / 0.6d , 0 ) } ",
259+ < 240d => $ "C{ Math . Round ( ( hue - 180d ) / 0.6d , 0 ) } ",
260+ < 300d => $ "B{ Math . Round ( ( hue - 240d ) / 0.6d , 0 ) } ",
261+ _ => $ "M{ Math . Round ( ( hue - 300d ) / 0.6d , 0 ) } "
262+ } ;
263+ }
264+ }
0 commit comments