From 0f4485f5e6ba1973e4583da30b3013bcc327f5b8 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Thu, 1 Aug 2019 10:53:32 -0400 Subject: [PATCH] Improve color utils --- CRM/Utils/Color.php | 95 ++++++++++++++++++++++----- bower.json | 3 +- settings/Core.setting.php | 2 +- tests/phpunit/CRM/Utils/ColorTest.php | 20 ++++-- 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/CRM/Utils/Color.php b/CRM/Utils/Color.php index 2cceca9ff3..95872433f5 100644 --- a/CRM/Utils/Color.php +++ b/CRM/Utils/Color.php @@ -36,49 +36,65 @@ */ 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; } } diff --git a/bower.json b/bower.json index 61c9e7415a..75e67552ef 100644 --- a/bower.json +++ b/bower.json @@ -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", diff --git a/settings/Core.setting.php b/settings/Core.setting.php index 262dfbd92c..68d82a2367 100644 --- a/settings/Core.setting.php +++ b/settings/Core.setting.php @@ -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', diff --git a/tests/phpunit/CRM/Utils/ColorTest.php b/tests/phpunit/CRM/Utils/ColorTest.php index 94cb87527c..107d258b84 100644 --- a/tests/phpunit/CRM/Utils/ColorTest.php +++ b/tests/phpunit/CRM/Utils/ColorTest.php @@ -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'], ]; } -- 2.25.1