/ src / common / ManagedCommon / ColorNameHelper.cs
ColorNameHelper.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System;
  6  using System.Drawing;
  7  
  8  namespace ManagedCommon
  9  {
 10      public static class ColorNameHelper
 11      {
 12          public const float DBLEPSILON = 2.2204460492503131e-16f;
 13  
 14          // For the purposes of naming colors, there are four steps that we go through.
 15          //
 16          // 1. For numerical ease, we convert the HSL values from the range [0, 1]
 17          // to the range [0, 255].
 18          //
 19          // 2. If luminosity is sufficiently high or low (> 240 or < 20), or if
 20          // saturation is sufficiently low (<= 20), then we declare that we're in the
 21          // achromatic range.  In this case, we return either white, black, or three
 22          // different shades of gray (depending on luminosity).
 23          //
 24          // 3. If we do have a chromatic color, the first thing we need to determine
 25          // about it is what the hue limits are for its saturation value - at different
 26          // levels of saturation, we have different hue values that we'll consider the
 27          // boundaries for different classes of named colors.  The hue limits for various
 28          // saturation values are as below.
 29          //
 30          // The numbers correspond to the following color buckets, with 0 meaning that
 31          // that bucket does not apply to the given saturation value:
 32          //
 33          // 1 - coral, 2 - red, 3 - orange, 4 - brown, 5 - tan, 6 - gold, 7 - yellow, 8 - olive green (with brown),
 34          // 9 - olive green (with green) 10 - lime green, 11 - green - 12 - bright green 13 - teal, 14 - aqua,
 35          // 15 - turquoise, 16 - pale blue, 17 - blue, 18 - blue-gray, 19 - indigo, 20 - purple, 21 - pink, 22 - brown, 23 - red
 36          private static int[] hueLimitsForSatLevel1 = // Sat: 20-75
 37          {
 38          // 1,  2,  3,  4,  5,  6,  7,  8,  9,  10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23
 39              8,  0,  0, 44,  0,  0,  0, 63,  0,   0, 122,   0, 134,   0,   0,   0,   0, 166, 176, 241,   0, 256,   0,
 40          };
 41  
 42          private static int[] hueLimitsForSatLevel2 = // Sat: 75-115
 43          {
 44          // 1,  2,  3,  4,  5,  6,  7,  8,  9,  10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23
 45              0, 10,  0, 32, 46,  0,  0,  0, 61,   0, 106,   0, 136, 144,   0,   0,   0, 158, 166, 241,   0,   0, 256,
 46          };
 47  
 48          private static int[] hueLimitsForSatLevel3 = // Sat: 115-150
 49          {
 50          // 1,  2,  3,  4,  5,  6,  7,  8,  9,  10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23
 51              0,  8,  0,  0, 39, 46,  0,  0,  0,  71, 120,   0, 131, 144,   0,   0, 163,   0, 177, 211, 249,   0, 256,
 52          };
 53  
 54          private static int[] hueLimitsForSatLevel4 = // Sat: 150-240
 55          {
 56          // 1,  2,  3,  4,  5,  6,  7,  8,  9,  10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23
 57              0, 11, 26,  0,  0, 38, 45,  0,  0,  56, 100, 121, 129,   0, 140,   0, 180,   0,   0, 224, 241,   0, 256,
 58          };
 59  
 60          private static int[] hueLimitsForSatLevel5 = // Sat: 240-255
 61          {
 62          // 1,  2,  3,  4,  5,  6,  7,  8,  9,  10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23
 63              0, 13, 27,  0,  0, 36, 45,  0,  0,  59, 118,   0, 127, 136, 142,   0, 185,   0,   0, 216, 239,   0, 256,
 64          };
 65  
 66          // 4. Once we have the color bucket, next we have three sub-buckets that we need to worry about,
 67          // corresponding to three different levels of luminosity.  For example, if we're in the "blue" bucket,
 68          // that might correspond to light blue, blue, or dark blue, depending on luminosity.
 69          // For each bucket, the luminosity cutoffs for the purposes of discerning between light, mid, and dark colors
 70          // are different, so we define luminosity limits for low and high luminosity for each bucket, as follows:
 71          private static int[] lumLimitsForHueIndexLow =
 72          {
 73          // 1,   2,   3,   4,   5,   6,   7,    8,  9,    10, 11,  12,  13,  14,   15, 16,  17,  18,  19,  20,  21,  22,  23
 74              130, 100, 115, 100, 100, 100, 110,  75, 100,  90, 100, 100, 100, 100,  80, 100, 100, 100, 100, 100, 100, 100, 100,
 75          };
 76  
 77          private static int[] lumLimitsForHueIndexHigh =
 78          {
 79          // 1,   2,   3,   4,   5,   6,   7,   8,   9,   10,  11,  12,  13,  14,  15,  16,  17,  18,  19,  20,  21,  22,  23
 80              170, 170, 170, 155, 170, 170, 170, 170, 170, 115, 170, 170, 170, 170, 170, 170, 170, 170, 150, 150, 170, 140, 165,
 81          };
 82  
 83          // 5. Finally, once we have a luminosity sub-bucket in the saturation color bucket, we have everything we need
 84          // to retrieve a name.  For each of the 23 buckets, we have names associated with light, mid, and dark variations
 85          // of that color, which are defined as follows:
 86          private static string[] colorNamesLight =
 87          {
 88              "TEXT_COLOR_CORAL",
 89              "TEXT_COLOR_ROSE",
 90              "TEXT_COLOR_LIGHTORANGE",
 91              "TEXT_COLOR_TAN",
 92              "TEXT_COLOR_TAN",
 93              "TEXT_COLOR_LIGHTYELLOW",
 94              "TEXT_COLOR_LIGHTYELLOW",
 95              "TEXT_COLOR_TAN",
 96              "TEXT_COLOR_LIGHTGREEN",
 97              "TEXT_COLOR_LIME",
 98              "TEXT_COLOR_LIGHTGREEN",
 99              "TEXT_COLOR_LIGHTGREEN",
100              "TEXT_COLOR_AQUA",
101              "TEXT_COLOR_SKYBLUE",
102              "TEXT_COLOR_LIGHTTURQUOISE",
103              "TEXT_COLOR_PALEBLUE",
104              "TEXT_COLOR_LIGHTBLUE",
105              "TEXT_COLOR_ICEBLUE",
106              "TEXT_COLOR_PERIWINKLE",
107              "TEXT_COLOR_LAVENDER",
108              "TEXT_COLOR_PINK",
109              "TEXT_COLOR_TAN",
110              "TEXT_COLOR_ROSE",
111          };
112  
113          private static string[] colorNamesMid =
114          {
115              "TEXT_COLOR_CORAL",
116              "TEXT_COLOR_RED",
117              "TEXT_COLOR_ORANGE",
118              "TEXT_COLOR_BROWN",
119              "TEXT_COLOR_TAN",
120              "TEXT_COLOR_GOLD",
121              "TEXT_COLOR_YELLOW",
122              "TEXT_COLOR_OLIVEGREEN",
123              "TEXT_COLOR_OLIVEGREEN",
124              "TEXT_COLOR_GREEN",
125              "TEXT_COLOR_GREEN",
126              "TEXT_COLOR_BRIGHTGREEN",
127              "TEXT_COLOR_TEAL",
128              "TEXT_COLOR_AQUA",
129              "TEXT_COLOR_TURQUOISE",
130              "TEXT_COLOR_PALEBLUE",
131              "TEXT_COLOR_BLUE",
132              "TEXT_COLOR_BLUEGRAY",
133              "TEXT_COLOR_INDIGO",
134              "TEXT_COLOR_PURPLE",
135              "TEXT_COLOR_PINK",
136              "TEXT_COLOR_BROWN",
137              "TEXT_COLOR_RED",
138          };
139  
140          private static string[] colorNamesDark =
141          {
142              "TEXT_COLOR_BROWN",
143              "TEXT_COLOR_DARKRED",
144              "TEXT_COLOR_BROWN",
145              "TEXT_COLOR_BROWN",
146              "TEXT_COLOR_BROWN",
147              "TEXT_COLOR_DARKYELLOW",
148              "TEXT_COLOR_DARKYELLOW",
149              "TEXT_COLOR_BROWN",
150              "TEXT_COLOR_DARKGREEN",
151              "TEXT_COLOR_DARKGREEN",
152              "TEXT_COLOR_DARKGREEN",
153              "TEXT_COLOR_DARKGREEN",
154              "TEXT_COLOR_DARKTEAL",
155              "TEXT_COLOR_DARKTEAL",
156              "TEXT_COLOR_DARKTEAL",
157              "TEXT_COLOR_DARKBLUE",
158              "TEXT_COLOR_DARKBLUE",
159              "TEXT_COLOR_BLUEGRAY",
160              "TEXT_COLOR_INDIGO",
161              "TEXT_COLOR_DARKPURPLE",
162              "TEXT_COLOR_PLUM",
163              "TEXT_COLOR_BROWN",
164              "TEXT_COLOR_DARKRED",
165          };
166  
167          public static string GetColorNameIdentifier(Color color)
168          {
169              var (hue, sat, lum) = ColorFormatHelper.ConvertToHSLColor(color);
170  
171              hue = (hue == 0 ? 0 : hue / 360) * 255; // this implementation is using normalization to 0-255 instead of 0-360°
172              sat = sat * 255;
173              lum = lum * 255;
174  
175              // First, if we're in the achromatic state, return the appropriate achromatic color name.
176              if (lum > 240)
177              {
178                  return "TEXT_COLOR_WHITE";
179              }
180              else if (lum < 20)
181              {
182                  return "TEXT_COLOR_BLACK";
183              }
184  
185              if (sat <= 20)
186              {
187                  if (lum > 170)
188                  {
189                      return "TEXT_COLOR_LIGHTGRAY";
190                  }
191                  else if (lum > 100)
192                  {
193                      return "TEXT_COLOR_GRAY";
194                  }
195                  else
196                  {
197                      return "TEXT_COLOR_DARKGRAY";
198                  }
199              }
200  
201              // If we have a chromatic color, we need to first get the hue limits for the saturation value.
202              int[] pHueLimits;
203              if (sat > 20 && sat <= 75)
204              {
205                  pHueLimits = hueLimitsForSatLevel1;
206              }
207              else if (sat > 75 && sat <= 115)
208              {
209                  pHueLimits = hueLimitsForSatLevel2;
210              }
211              else if (sat > 115 && sat <= 150)
212              {
213                  pHueLimits = hueLimitsForSatLevel3;
214              }
215              else if (sat > 150 && sat <= 240)
216              {
217                  pHueLimits = hueLimitsForSatLevel4;
218              }
219              else
220              {
221                  pHueLimits = hueLimitsForSatLevel5;
222              }
223  
224              // Now that we have that, we can get the color index, which represents which
225              // of the 23 buckets we're located in.
226              int colorIndex = -1;
227              for (int i = 0; i < colorNamesMid.Length; ++i)
228              {
229                  if (hue < pHueLimits[i])
230                  {
231                      colorIndex = i;
232                      break;
233                  }
234              }
235  
236              // Assuming we got a color index (and we always should get one), then next we need to
237              // figure out which luminosity sub-bucket we're located in.
238              // Once we have that, we'll return the color name from the appropriate array.
239              if (colorIndex != -1)
240              {
241                  if (lum > lumLimitsForHueIndexHigh[colorIndex])
242                  {
243                      return colorNamesLight[colorIndex];
244                  }
245                  else if (lum < lumLimitsForHueIndexLow[colorIndex])
246                  {
247                      return colorNamesDark[colorIndex];
248                  }
249                  else
250                  {
251                      return colorNamesMid[colorIndex];
252                  }
253              }
254  
255              return string.Empty;
256          }
257  
258          public static bool AreClose(double a, double b)
259          {
260              return (float)Math.Abs(a - b) <= DBLEPSILON * (float)Math.Abs(a);
261          }
262      }
263  }