Improve color utils
authorColeman Watts <coleman@civicrm.org>
Thu, 1 Aug 2019 14:53:32 +0000 (10:53 -0400)
committerColeman Watts <coleman@civicrm.org>
Thu, 1 Aug 2019 14:53:32 +0000 (10:53 -0400)
CRM/Utils/Color.php
bower.json
settings/Core.setting.php
tests/phpunit/CRM/Utils/ColorTest.php

index 2cceca9ff33468d6027255e2f5361fa96bfc96fc..95872433f52598cc65e7334f0850928545d5de4e 100644 (file)
  */
 class CRM_Utils_Color {
 
+  const COLOR_FILE = '[civicrm.root]/bower_components/css-color-names/css-color-names.json';
+
   /**
    * Determine the appropriate text color for a given background.
    *
    * Based on YIQ value.
    *
-   * @param string $hexcolor
+   * @param string $color
    * @param string $black
    * @param string $white
    * @return string
    */
-  public static function getContrast($hexcolor, $black = 'black', $white = 'white') {
-    list($r, $g, $b) = self::getRgb($hexcolor);
+  public static function getContrast($color, $black = 'black', $white = 'white') {
+    list($r, $g, $b) = self::getRgb($color);
     $yiq = (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
     return ($yiq >= 128) ? $black : $white;
   }
 
   /**
-   * Convert hex color to decimal
+   * Parse any color string into rgb decimal values
+   *
+   * Accepted formats:
+   *   Full hex:     "#ffffff"
+   *   Short hex:    "#fff"
+   *   Color name    "white"
+   *   RGB notation: "rgb(255, 255, 255)"
    *
-   * @param string $hexcolor
-   * @return array
+   * @param string $color
+   * @return int[]|null
    *   [red, green, blue]
    */
-  public static function getRgb($hexcolor) {
-    $hexcolor = trim($hexcolor, ' #');
-    if (strlen($hexcolor) === 3) {
-      $hexcolor = $hexcolor[0] . $hexcolor[0] . $hexcolor[1] . $hexcolor[1] . $hexcolor[2] . $hexcolor[2];
+  public static function getRgb($color) {
+    $color = str_replace(' ', '', $color);
+    $color = self::nameToHex($color) ?? $color;
+    if (strpos($color, 'rgb(') === 0) {
+      return explode(',', substr($color, 4, strpos($color, ')') - 4));
+    }
+    $color = ltrim($color, '#');
+    if (strlen($color) === 3) {
+      $color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2];
+    }
+    if (!CRM_Utils_Rule::color('#' . $color)) {
+      return NULL;
     }
     return [
-      hexdec(substr($hexcolor, 0, 2)),
-      hexdec(substr($hexcolor, 2, 2)),
-      hexdec(substr($hexcolor, 4, 2)),
+      hexdec(substr($color, 0, 2)),
+      hexdec(substr($color, 2, 2)),
+      hexdec(substr($color, 4, 2)),
     ];
   }
 
   /**
    * Calculate a highlight color from a base color
    *
-   * @param $hexcolor
+   * @param $color
    * @return string
    */
-  public static function getHighlight($hexcolor) {
-    $rgb = CRM_Utils_Color::getRgb($hexcolor);
+  public static function getHighlight($color) {
+    $rgb = self::getRgb($color);
     $avg = array_sum($rgb) / 3;
     foreach ($rgb as &$v) {
       if ($avg > 242) {
@@ -90,7 +106,52 @@ class CRM_Utils_Color {
         $v = min(255, intval((-.0035 * ($v - 242) ** 2) + 260));
       }
     }
-    return '#' . implode(array_map('dechex', $rgb));
+    return self::rgbToHex($rgb);
+  }
+
+  /**
+   * Convert named color (e.g. springgreen) to hex
+   *
+   * @param $colorName
+   * @return string|null
+   */
+  public static function nameToHex($colorName) {
+    if (strpos($colorName, '#') !== FALSE || strpos($colorName, '(') !== FALSE) {
+      return NULL;
+    }
+    if (empty(Civi::$statics[__CLASS__]['names'])) {
+      Civi::$statics[__CLASS__]['names'] = json_decode(file_get_contents(Civi::paths()->getPath(self::COLOR_FILE)), TRUE);
+    }
+    return Civi::$statics[__CLASS__]['names'][strtolower($colorName)] ?? NULL;
+  }
+
+  /**
+   * Converts rgb array to hex string
+   *
+   * @param int[] $rgb
+   * @return string
+   */
+  public static function rgbToHex($rgb) {
+    $ret = '#';
+    foreach ($rgb as $dec) {
+      $ret .= str_pad(dechex($dec), 2, '0', STR_PAD_LEFT);
+    }
+    return $ret;
+  }
+
+  /**
+   * Validate color input and convert it to standard hex notation
+   *
+   * @param string $color
+   * @return bool
+   */
+  public static function normalize(&$color) {
+    $rgb = self::getRgb($color);
+    if ($rgb) {
+      $color = self::rgbToHex($rgb);
+      return TRUE;
+    }
+    return FALSE;
   }
 
 }
index 61c9e7415aecaecc6fccb852e4f2b32ddc72a474..75e67552ef9e45a1fb85bc5491478d3526c406ee 100644 (file)
@@ -34,7 +34,8 @@
     "phantomjs-polyfill": "^0.0.2",
     "es6-promise": "^4.2.4",
     "angular-xeditable": "^0.9.0",
-    "checklist-model": "~1"
+    "checklist-model": "~1",
+    "css-color-names": "~1"
   },
   "resolutions": {
     "angular": "~1.5.11",
index 262dfbd92c1139f9f620b9d04f5557c3dd47a915..68d82a2367aebaf3d5bf0ad766f9341a7b99ff0a 100644 (file)
@@ -1046,7 +1046,7 @@ return [
     'is_contact' => 0,
     'description' => ts('Color of the CiviCRM main menu.'),
     'help_text' => NULL,
-    'validate_callback' => 'CRM_Utils_Rule::color',
+    'validate_callback' => 'CRM_Utils_Color::normalize',
   ],
   'requestableMimeTypes' => [
     'group_name' => 'CiviCRM Preferences',
index 94cb87527c5f71432d30d75b9b9254c7a78f9cd5..107d258b84a73d622da29e51e88eb0ed2dfba6af 100644 (file)
@@ -29,16 +29,24 @@ class CRM_Utils_ColorTest extends CiviUnitTestCase {
   /**
    * @dataProvider rgbExamples
    */
-  public function testGetRgb($hex, $rgb) {
-    $this->assertEquals($rgb, CRM_Utils_Color::getRgb($hex));
+  public function testGetRgb($color, $expectedRGB, $expectedHex) {
+    $rgb = CRM_Utils_Color::getRgb($color);
+    $this->assertEquals($expectedRGB, $rgb);
+    $this->assertEquals($expectedHex, CRM_Utils_Color::rgbToHex($rgb));
   }
 
   public function rgbExamples() {
     return [
-      ['#fff', [255, 255, 255]],
-      ['#000000', [0, 0, 0]],
-      ['#111', [17, 17, 17]],
-      [' fffc99 ', [255, 252, 153]],
+      ['#fff', [255, 255, 255], '#ffffff'],
+      ['white', [255, 255, 255], '#ffffff'],
+      ['#000000', [0, 0, 0], '#000000'],
+      [' black', [0, 0, 0], '#000000'],
+      ['  #111 ', [17, 17, 17], '#111111'],
+      [' fffc99 ', [255, 252, 153], '#fffc99'],
+      ['blue', [0, 0, 255], '#0000ff'],
+      ['Green', [0, 128, 0], '#008000'],
+      ['rgb(12, 0, 123)', [12, 0, 123], '#0c007b'],
+      [' rgb ( 123, 0, 12 ) !important', [123, 0, 12], '#7b000c'],
     ];
   }