diff --git a/CSSParser.php b/CSSParser.php index bc21e108..e365392f 100644 --- a/CSSParser.php +++ b/CSSParser.php @@ -1,4 +1,5 @@ parseCharacter(true); if($sResult === null) { throw new Exception("Identifier expected, got {$this->peek(5)}"); @@ -106,8 +107,18 @@ private function parseIdentifier($bAllowFunctions = true) { $sCharacter; while(($sCharacter = $this->parseCharacter(true)) !== null) { $sResult .= $sCharacter; - } - if($bAllowFunctions && $this->comes('(')) { + } + if($bAllowColors) + { + // is it a color name ? + if($aColor = ColorUtils::namedColor2rgb($sResult)) + { + $oColor = new CSSColor(); + return $oColor->fromRGB($aColor); + } + } + if($bAllowFunctions && $this->comes('(')) + { $this->consume('('); $sResult = new CSSFunction($sResult, $this->parseValue()); $this->consume(')'); @@ -252,7 +263,7 @@ private function parseSingleValue() { } else if($this->comes("'") || $this->comes('"')){ $oValue = $this->parseStringValue(); } else { - $oValue = $this->parseIdentifier(); + $oValue = $this->parseIdentifier(true, true); } $this->consumeWhiteSpace(); if($this->comes('/')) { @@ -260,7 +271,7 @@ private function parseSingleValue() { $oValue = new CSSSlashedValue($oValue, $this->parseSingleValue()); } return $oValue; - } + } private function parseNumericValue($bForColor = false) { $sSize = ''; @@ -305,19 +316,16 @@ private function parseNumericValue($bForColor = false) { } private function parseColorValue() { - $aColor = array(); if($this->comes('#')) { $this->consume('#'); - $sValue = $this->parseIdentifier(false); - if(mb_strlen($sValue, $this->sCharset) === 3) { - $sValue = $sValue[0].$sValue[0].$sValue[1].$sValue[1].$sValue[2].$sValue[2]; - } - $aColor = array('r' => new CSSSize(intval($sValue[0].$sValue[1], 16), null, true), 'g' => new CSSSize(intval($sValue[2].$sValue[3], 16), null, true), 'b' => new CSSSize(intval($sValue[4].$sValue[5], 16), null, true)); + $sValue = $this->parseIdentifier(); + return new CSSColor($sValue); } else { - $sColorMode = $this->parseIdentifier(false); + $aColor = array(); + $sColorMode = $this->parseIdentifier(); $this->consumeWhiteSpace(); $this->consume('('); - $iLength = mb_strlen($sColorMode, $this->sCharset); + $iLength = strlen($sColorMode); for($i=0;$i<$iLength;$i++) { $this->consumeWhiteSpace(); $aColor[$sColorMode[$i]] = $this->parseNumericValue(true); @@ -327,8 +335,8 @@ private function parseColorValue() { } } $this->consume(')'); + return new CSSColor($aColor); } - return new CSSColor($aColor); } private function parseURLValue() { diff --git a/lib/CSSValueList.php b/lib/CSSValueList.php index fa6d2155..d4934ef9 100644 --- a/lib/CSSValueList.php +++ b/lib/CSSValueList.php @@ -47,6 +47,10 @@ public function getName() { return $this->sName; } + public function setName($sName) { + $this->sName = $sName; + } + public function getArguments() { return $this->aComponents; } @@ -58,10 +62,66 @@ public function __toString() { } class CSSColor extends CSSFunction { - public function __construct($aColor) { - parent::__construct(implode('', array_keys($aColor)), $aColor); + + public function __construct($mColor=null) { + parent::__construct('rgb', null); + if(is_array($mColor)) { + if(isset($mColor['r'], $mColor['g'], $mColor['b'])) { + $this->fromRGB($mColor); + } + else if(isset($mColor['h'], $mColor['s'], $mColor['l'])) { + $this->fromHSL($mColor); + } + } + else if(is_string($mColor)) { + if($aRGB = ColorUtils::namedColor2rgb($mColor)) { + $this->fromRGB($aRGB); + } + else if($aRGB = ColorUtils::hex2rgb($mColor)) { + $this->fromRGB($aRGB); + } + } } - + + public function fromRGB(Array $aRGB) { + $this->aComponents = array(); + $sName = 'rgb'; + foreach(array('r', 'g', 'b', 'a') as $sChannel) { + if($sChannel == 'a') { + if(!isset($aRGB['a'])) continue; + $sValue = ColorUtils::constrainValue((string)$aRGB['a'], 0, 1); + if($sValue == 1) continue; + $sName .= 'a'; + } + else { + $sValue = ColorUtils::normalizeRGBValue((string)$aRGB[$sChannel]); + } + $this->aComponents[$sChannel] = new CSSSize($sValue, null, true); + } + $this->setName($sName); + return $this; + } + + public function fromHSL(Array $aHSL) { + $aRGB = ColorUtils::hsl2rgb( + (string)$aHSL['h'], + (string)$aHSL['s'], + (string)$aHSL['l'], + isset($aHSL['a']) ? (string)$aHSL['a'] : 1 + ); + return $this->fromRGB($aRGB); + } + + public function fromHex($sValue) { + $aRGB = ColorUtils::hex2rgb($sValue); + return $this->fromRGB($aRGB); + } + + public function fromNamedColor($sValue) { + $aRGB = ColorUtils::namedColor2rgb($sValue); + return $this->fromRGB($aRGB); + } + public function getColor() { return $this->aComponents; } @@ -69,6 +129,94 @@ public function getColor() { public function getColorDescription() { return $this->getName(); } -} + public function toRGB() { + $sName = $this->getName(); + $aComponents = $this->aComponents; + + if(!$sName || $sName == 'rgb') return; + if($sName == 'rgba') { + // If we don't need alpha channel, drop it + if($aComponents['a']->getSize() >= 1) { + unset($this->aComponents['a']); + $this->setName('rgb'); + } + return; + } + $aRGB = ColorUtils::hsl2rgb( + $aComponents['h']->getSize(), + $aComponents['s']->getSize(), + $aComponents['l']->getSize(), + isset($aComponents['a']) ? $aComponents['a']->getSize() : 1 + ); + + $this->aComponents = array(); + foreach($aRGB as $key => $val) { + $this->aComponents[$key] = new CSSSize($val, null, true); + } + $sName = isset($aRGB['a']) ? 'rgba' : 'rgb'; + $this->setName($sName); + return $this; + } + public function toHSL() { + $sName = $this->getName(); + $aComponents = $this->aComponents; + if(!$sName || $sName == 'hsl') return; + if($sName == 'hsla') { + // If we don't need alpha channel, drop it + if($aComponents['a']->getSize() >= 1) { + unset($this->aComponents['a']); + $this->setName('hsl'); + } + return; + } + $aHSL = ColorUtils::rgb2hsl( + $aComponents['r']->getSize(), + $aComponents['g']->getSize(), + $aComponents['b']->getSize(), + isset($aComponents['a']) ? $aComponents['a']->getSize() : 1 + ); + $this->aComponents = array(); + $this->aComponents['h'] = new CSSSize($aHSL['h'], null, true); + $this->aComponents['s'] = new CSSSize($aHSL['s'], '%', true); + $this->aComponents['l'] = new CSSSize($aHSL['l'], '%', true); + $sName = 'hsl'; + if(isset($aHSL['a'])) { + $this->aComponents['a'] = new CSSSize($aHSL['a'], null, true); + $sName = 'hsla'; + } + $this->setName($sName); + return $this; + } + + public function getNamedColor() { + $this->toRGB(); + $aComponents = $this->aComponents; + return ColorUtils::rgb2NamedColor( + $aComponents['r']->getSize(), + $aComponents['g']->getSize(), + $aComponents['b']->getSize() + ); + } + + public function getHexValue() { + $aComponents = $this->aComponents; + if(isset($aComponents['a']) && $aComponents['a']->getSize() !== 1) return null; + $sName = $this->getName(); + if($sName == 'rgb') { + return ColorUtils::rgb2hex( + $aComponents['r']->getSize(), + $aComponents['g']->getSize(), + $aComponents['b']->getSize() + ); + } + else if($sName == 'hsl') { + return ColorUtils::hsl2hex( + $aComponents['h']->getSize(), + $aComponents['s']->getSize(), + $aComponents['l']->getSize() + ); + } + } +} diff --git a/lib/ColorUtils.php b/lib/ColorUtils.php new file mode 100644 index 00000000..bb1a02cf --- /dev/null +++ b/lib/ColorUtils.php @@ -0,0 +1,382 @@ + array('r' => 240, 'g' => 248, 'b' => 255), + 'antiquewhite' => array('r' => 250, 'g' => 235, 'b' => 215), + 'aqua' => array('r' => 0, 'g' => 255, 'b' => 255), + 'aquamarine' => array('r' => 127, 'g' => 255, 'b' => 212), + 'azure' => array('r' => 240, 'g' => 255, 'b' => 255), + 'beige' => array('r' => 245, 'g' => 245, 'b' => 220), + 'bisque' => array('r' => 255, 'g' => 228, 'b' => 196), + 'black' => array('r' => 0, 'g' => 0, 'b' => 0), + 'blanchedalmond' => array('r' => 255, 'g' => 235, 'b' => 205), + 'blue' => array('r' => 0, 'g' => 0, 'b' => 255), + 'blueviolet' => array('r' => 138, 'g' => 43, 'b' => 226), + 'brown' => array('r' => 165, 'g' => 42, 'b' => 42), + 'burlywood' => array('r' => 222, 'g' => 184, 'b' => 135), + 'cadetblue' => array('r' => 95, 'g' => 158, 'b' => 160), + 'chartreuse' => array('r' => 127, 'g' => 255, 'b' => 0), + 'chocolate' => array('r' => 210, 'g' => 105, 'b' => 30), + 'coral' => array('r' => 255, 'g' => 127, 'b' => 80), + 'cornflowerblue' => array('r' => 100, 'g' => 149, 'b' => 237), + 'cornsilk' => array('r' => 255, 'g' => 248, 'b' => 220), + 'crimson' => array('r' => 220, 'g' => 20, 'b' => 60), + 'cyan' => array('r' => 0, 'g' => 255, 'b' => 255), + 'darkblue' => array('r' => 0, 'g' => 0, 'b' => 139), + 'darkcyan' => array('r' => 0, 'g' => 139, 'b' => 139), + 'darkgoldenrod' => array('r' => 184, 'g' => 134, 'b' => 11), + 'darkgray' => array('r' => 169, 'g' => 169, 'b' => 169), + 'darkgreen' => array('r' => 0, 'g' => 100, 'b' => 0), + 'darkgrey' => array('r' => 169, 'g' => 169, 'b' => 169), + 'darkkhaki' => array('r' => 189, 'g' => 183, 'b' => 107), + 'darkmagenta' => array('r' => 139, 'g' => 0, 'b' => 139), + 'darkolivegreen' => array('r' => 85, 'g' => 107, 'b' => 47), + 'darkorange' => array('r' => 255, 'g' => 140, 'b' => 0), + 'darkorchid' => array('r' => 153, 'g' => 50, 'b' => 204), + 'darkred' => array('r' => 139, 'g' => 0, 'b' => 0), + 'darksalmon' => array('r' => 233, 'g' => 150, 'b' => 122), + 'darkseagreen' => array('r' => 143, 'g' => 188, 'b' => 143), + 'darkslateblue' => array('r' => 72, 'g' => 61, 'b' => 139), + 'darkslategray' => array('r' => 47, 'g' => 79, 'b' => 79), + 'darkslategrey' => array('r' => 47, 'g' => 79, 'b' => 79), + 'darkturquoise' => array('r' => 0, 'g' => 206, 'b' => 209), + 'darkviolet' => array('r' => 148, 'g' => 0, 'b' => 211), + 'deeppink' => array('r' => 255, 'g' => 20, 'b' => 147), + 'deepskyblue' => array('r' => 0, 'g' => 191, 'b' => 255), + 'dimgray' => array('r' => 105, 'g' => 105, 'b' => 105), + 'dimgrey' => array('r' => 105, 'g' => 105, 'b' => 105), + 'dodgerblue' => array('r' => 30, 'g' => 144, 'b' => 255), + 'firebrick' => array('r' => 178, 'g' => 34, 'b' => 34), + 'floralwhite' => array('r' => 255, 'g' => 250, 'b' => 240), + 'forestgreen' => array('r' => 34, 'g' => 139, 'b' => 34), + 'fuchsia' => array('r' => 255, 'g' => 0, 'b' => 255), + 'gainsboro' => array('r' => 220, 'g' => 220, 'b' => 220), + 'ghostwhite' => array('r' => 248, 'g' => 248, 'b' => 255), + 'gold' => array('r' => 255, 'g' => 215, 'b' => 0), + 'goldenrod' => array('r' => 218, 'g' => 165, 'b' => 32), + 'gray' => array('r' => 128, 'g' => 128, 'b' => 128), + 'green' => array('r' => 0, 'g' => 128, 'b' => 0), + 'greenyellow' => array('r' => 173, 'g' => 255, 'b' => 47), + 'grey' => array('r' => 128, 'g' => 128, 'b' => 128), + 'honeydew' => array('r' => 240, 'g' => 255, 'b' => 240), + 'hotpink' => array('r' => 255, 'g' => 105, 'b' => 180), + 'indianred' => array('r' => 205, 'g' => 92, 'b' => 92), + 'indigo' => array('r' => 75, 'g' => 0, 'b' => 130), + 'ivory' => array('r' => 255, 'g' => 255, 'b' => 240), + 'khaki' => array('r' => 240, 'g' => 230, 'b' => 140), + 'lavender' => array('r' => 230, 'g' => 230, 'b' => 250), + 'lavenderblush' => array('r' => 255, 'g' => 240, 'b' => 245), + 'lawngreen' => array('r' => 124, 'g' => 252, 'b' => 0), + 'lemonchiffon' => array('r' => 255, 'g' => 250, 'b' => 205), + 'lightblue' => array('r' => 173, 'g' => 216, 'b' => 230), + 'lightcoral' => array('r' => 240, 'g' => 128, 'b' => 128), + 'lightcyan' => array('r' => 224, 'g' => 255, 'b' => 255), + 'lightgoldenrodyellow' => array('r' => 250, 'g' => 250, 'b' => 210), + 'lightgray' => array('r' => 211, 'g' => 211, 'b' => 211), + 'lightgreen' => array('r' => 144, 'g' => 238, 'b' => 144), + 'lightgrey' => array('r' => 211, 'g' => 211, 'b' => 211), + 'lightpink' => array('r' => 255, 'g' => 182, 'b' => 193), + 'lightsalmon' => array('r' => 255, 'g' => 160, 'b' => 122), + 'lightseagreen' => array('r' => 32, 'g' => 178, 'b' => 170), + 'lightskyblue' => array('r' => 135, 'g' => 206, 'b' => 250), + 'lightslategray' => array('r' => 119, 'g' => 136, 'b' => 153), + 'lightslategrey' => array('r' => 119, 'g' => 136, 'b' => 153), + 'lightsteelblue' => array('r' => 176, 'g' => 196, 'b' => 222), + 'lightyellow' => array('r' => 255, 'g' => 255, 'b' => 224), + 'lime' => array('r' => 0, 'g' => 255, 'b' => 0), + 'limegreen' => array('r' => 50, 'g' => 205, 'b' => 50), + 'linen' => array('r' => 250, 'g' => 240, 'b' => 230), + 'magenta' => array('r' => 255, 'g' => 0, 'b' => 255), + 'maroon' => array('r' => 128, 'g' => 0, 'b' => 0), + 'mediumaquamarine' => array('r' => 102, 'g' => 205, 'b' => 170), + 'mediumblue' => array('r' => 0, 'g' => 0, 'b' => 205), + 'mediumorchid' => array('r' => 186, 'g' => 85, 'b' => 211), + 'mediumpurple' => array('r' => 147, 'g' => 112, 'b' => 219), + 'mediumseagreen' => array('r' => 60, 'g' => 179, 'b' => 113), + 'mediumslateblue' => array('r' => 123, 'g' => 104, 'b' => 238), + 'mediumspringgreen' => array('r' => 0, 'g' => 250, 'b' => 154), + 'mediumturquoise' => array('r' => 72, 'g' => 209, 'b' => 204), + 'mediumvioletred' => array('r' => 199, 'g' => 21, 'b' => 133), + 'midnightblue' => array('r' => 25, 'g' => 25, 'b' => 112), + 'mintcream' => array('r' => 245, 'g' => 255, 'b' => 250), + 'mistyrose' => array('r' => 255, 'g' => 228, 'b' => 225), + 'moccasin' => array('r' => 255, 'g' => 228, 'b' => 181), + 'navajowhite' => array('r' => 255, 'g' => 222, 'b' => 173), + 'navy' => array('r' => 0, 'g' => 0, 'b' => 128), + 'oldlace' => array('r' => 253, 'g' => 245, 'b' => 230), + 'olive' => array('r' => 128, 'g' => 128, 'b' => 0), + 'olivedrab' => array('r' => 107, 'g' => 142, 'b' => 35), + 'orange' => array('r' => 255, 'g' => 165, 'b' => 0), + 'orangered' => array('r' => 255, 'g' => 69, 'b' => 0), + 'orchid' => array('r' => 218, 'g' => 112, 'b' => 214), + 'palegoldenrod' => array('r' => 238, 'g' => 232, 'b' => 170), + 'palegreen' => array('r' => 152, 'g' => 251, 'b' => 152), + 'paleturquoise' => array('r' => 175, 'g' => 238, 'b' => 238), + 'palevioletred' => array('r' => 219, 'g' => 112, 'b' => 147), + 'papayawhip' => array('r' => 255, 'g' => 239, 'b' => 213), + 'peachpuff' => array('r' => 255, 'g' => 218, 'b' => 185), + 'peru' => array('r' => 205, 'g' => 133, 'b' => 63), + 'pink' => array('r' => 255, 'g' => 192, 'b' => 203), + 'plum' => array('r' => 221, 'g' => 160, 'b' => 221), + 'powderblue' => array('r' => 176, 'g' => 224, 'b' => 230), + 'purple' => array('r' => 128, 'g' => 0, 'b' => 128), + 'red' => array('r' => 255, 'g' => 0, 'b' => 0), + 'rosybrown' => array('r' => 188, 'g' => 143, 'b' => 143), + 'royalblue' => array('r' => 65, 'g' => 105, 'b' => 225), + 'saddlebrown' => array('r' => 139, 'g' => 69, 'b' => 19), + 'salmon' => array('r' => 250, 'g' => 128, 'b' => 114), + 'sandybrown' => array('r' => 244, 'g' => 164, 'b' => 96), + 'seagreen' => array('r' => 46, 'g' => 139, 'b' => 87), + 'seashell' => array('r' => 255, 'g' => 245, 'b' => 238), + 'sienna' => array('r' => 160, 'g' => 82, 'b' => 45), + 'silver' => array('r' => 192, 'g' => 192, 'b' => 192), + 'skyblue' => array('r' => 135, 'g' => 206, 'b' => 235), + 'slateblue' => array('r' => 106, 'g' => 90, 'b' => 205), + 'slategray' => array('r' => 112, 'g' => 128, 'b' => 144), + 'slategrey' => array('r' => 112, 'g' => 128, 'b' => 144), + 'snow' => array('r' => 255, 'g' => 250, 'b' => 250), + 'springgreen' => array('r' => 0, 'g' => 255, 'b' => 127), + 'steelblue' => array('r' => 70, 'g' => 130, 'b' => 180), + 'tan' => array('r' => 210, 'g' => 180, 'b' => 140), + 'teal' => array('r' => 0, 'g' => 128, 'b' => 128), + 'thistle' => array('r' => 216, 'g' => 191, 'b' => 216), + 'tomato' => array('r' => 255, 'g' => 99, 'b' => 71), + 'turquoise' => array('r' => 64, 'g' => 224, 'b' => 208), + 'violet' => array('r' => 238, 'g' => 130, 'b' => 238), + 'wheat' => array('r' => 245, 'g' => 222, 'b' => 179), + 'white' => array('r' => 255, 'g' => 255, 'b' => 255), + 'whitesmoke' => array('r' => 245, 'g' => 245, 'b' => 245), + 'yellow' => array('r' => 255, 'g' => 255, 'b' => 0), + 'yellowgreen' => array('r' => 154, 'g' => 205, 'b' => 50) + ); + + static function namedColor2rgb($sColor, $bAsString=false) + { + $sColor = mb_strtolower($sColor); + if($sColor == 'transparent') { + return array('r'=>0, 'g'=>0, 'b'=>0, 'a'=>0); + } + if(isset(self::$X11_COLORS_MAP[$sColor])) + { + $aRGB = self::$X11_COLORS_MAP[$sColor]; + //return $bAsString ? 'rgb('.implode(',', $aRGB).')' : $aRGB; + return $aRGB; + } + return null; + } + + static function rgb2namedColor($r, $g, $b, $a=1) + { + if($a !== 1) return null; + if($a == 0) return 'transparent'; + $result = array_search( + array('r' => $r, 'g' => $g, 'b' => $b), + self::$X11_COLORS_MAP + ); + return $result === false ? null : $result; + } + + /** + * Converts Hexadecimal color to RGB + **/ + static function hex2rgb($value) + { + if($value[0] == '#') $value = substr($value, 1); + if(strlen($value) == 3) + { + $value = $value[0].$value[0].$value[1].$value[1].$value[2].$value[2]; + } + //If a proper hex code, convert using bitwise operation. No overhead... faster + if (strlen($value) == 6) + { + $decimal = hexdec($value); + return array( + 'r' => 0xFF & ($decimal >> 0x10), + 'g' => 0xFF & ($decimal >> 0x8), + 'b' => 0xFF & $decimal + ); + } + return false; //Invalid hex color code + } + + /** + * Converts RGB to Hexadecimal + **/ + static function rgb2hex($r, $g, $b, $asString=true) + { + $r = self::normalizeRGBValue($r); + $g = self::normalizeRGBValue($g); + $b = self::normalizeRGBValue($b); + $value = dechex($r << 16 | $g << 8 | $b); + $value = str_pad($value, 6, '0', STR_PAD_LEFT); + return $asString ? '#'.$value : (int)'0x'.$value; + } + + /** + * Converts HSL to RGB + **/ + static function hsl2rgb($h, $s, $l, $a=1) + { + // normalize to float between 0..1 + $s = self::normalizeFraction($s); + $l = self::normalizeFraction($l); + $a = self::constrainValue($a, 0, 1); + + if($l == 1) + { + // white + $aRGB = array('r' => 255, 'g' => 255, 'b' => 255); + if($a < 1) $aRGB['a'] = $a; + return $aRGB; + } + if ($l == 0) + { + // black + $aRGB = array('r' => 0, 'g' => 0, 'b' => 0); + if($a < 1) $aRGB['a'] = $a; + return $aRGB; + } + if($s == 0) + { + // Grayscale: we don't need no fancy calculation ! + $v = round(255 * $l); + $aRGB = array('r' => $v, 'g' => $v, 'b' => $v); + if($a < 1) $aRGB['a'] = $a; + return $aRGB; + } + // normalize to int between [0,360) + $h = (($h % 360) + 360) % 360; + // then to float between 0..1 + $h /= 360; + + if($l < 0.5) $m2 = $l * ($s +1); + else $m2 = ($l + $s) - ($l * $s); + $m1 = $l * 2 - $m2; + + $aRGB = array( + 'r' => round(255 * self::hue2rgb($m1, $m2, $h + (1/3))), + 'g' => round(255 * self::hue2rgb($m1, $m2, $h)), + 'b' => round(255 * self::hue2rgb($m1, $m2, $h - (1/3))) + ); + if($a < 1) $aRGB['a'] = $a; + return $aRGB; + } + + static private function hue2rgb($m1, $m2, $h) + { + if($h < 0) $h++; + if($h > 1) $h--; + if(($h * 6) < 1) return $m1 + ($m2 - $m1) * $h * 6; + if(($h * 2) < 1) return $m2; + if(($h * 3) < 2) return $m1 + ($m2 - $m1) * (2/3 - $h) * 6; + return $m1; + } + + /** + * Converts RGB to HSL colorspace + * returns S & L as percentages + **/ + static function rgb2hsl($r, $g, $b, $a=1) + { + // normalize to float between 0..1 + $r = self::normalizeRGBValue($r) / 255; + $g = self::normalizeRGBValue($g) / 255; + $b = self::normalizeRGBValue($b) / 255; + $a = self::constrainValue($a, 0, 1); + + $min = min($r, $g, $b); //Min. value of RGB + $max = max($r, $g, $b); //Max. value of RGB + $delta_max = $max - $min; //Delta RGB value + + $l = ($max + $min) / 2; + + if($delta_max == 0) //This is a gray, no chroma... + { + //HSL results from 0 to 1 + $h = 0; + $s = 0; + } + else //Chromatic data... + { + if($l < 0.5) + { + $s = $delta_max / ($max + $min); + } + else + { + $s = $delta_max / (2 - $max - $min); + } + + $delta_r = ((($max - $r) / 6) + ($delta_max / 2)) / $delta_max; + $delta_g = ((($max - $g) / 6) + ($delta_max / 2)) / $delta_max; + $delta_b = ((($max - $b) / 6) + ($delta_max / 2)) / $delta_max; + + if($r == $max) + { + $h = $delta_b - $delta_g; + } + else if($g == $max) + { + $h = (1/3) + $delta_r - $delta_b; + } + else if($b == $max) + { + $h = (2/3) + $delta_g - $delta_r; + } + if ($h < 0) $h++; + if ($h > 1) $h--; + } + $aHSL = array( + 'h' => round($h * 360), + 's' => round($s * 100) . '%', + 'l' => round($l * 100) . '%' + ); + if($a < 1) $aHSL['a'] = $a; + return $aHSL; + } + + /** + * Normalize a fraction value: + * @param $value the divided of the fraction, either a percentage or a number. + * @param $max the divisor of the fraction. + * @returns a float in range 0..1 + **/ + static function normalizeFraction($value, $max=100) + { + $i = strpos($value, '%'); + if($i !== false) + { + $value = substr($value, 0, $i); + $max = 100; + } + $value = self::constrainValue($value, 0, $max); + return $value / $max; + } + + /** + * Normalize a rgb value: + * @param $value either a percentage or a number + * @returns an integer in range 0..255 + **/ + static function normalizeRGBValue($value) + { + $i = strpos($value, '%'); + // percentage value + if($i !== false) + { + $value = substr($value, 0, $i); + $value = self::constrainValue($value, 0, 100); + return round($value * 255 / 100); + } + // normal value + return self::constrainValue($value, 0, 255); + } + + static function constrainValue($value, $min, $max) + { + return max($min, min($value, $max)); + } +} + diff --git a/tests/CSSColorTest.php b/tests/CSSColorTest.php new file mode 100644 index 00000000..c18dcb58 --- /dev/null +++ b/tests/CSSColorTest.php @@ -0,0 +1,137 @@ +fromRGB($aRGB); + $this->assertEquals($oColor->getColor(), $aExpectedColor); + $this->assertEquals($oColor->getColorDescription(), $aExpectedDescription); + } + public function fromRGBProvider() + { + return array( + array( + array('r'=>0, 'g'=>255, 'b'=>255), + array( + 'r'=> new CSSSize(0, null, true), + 'g'=> new CSSSize(255, null, true), + 'b'=> new CSSSize(255, null, true) + ), + 'rgb' + ), + array( + array('r'=>'100%', 'g'=>-5, 'b'=>303), + array( + 'r'=> new CSSSize(255, null, true), + 'g'=> new CSSSize(0, null, true), + 'b'=> new CSSSize(255, null, true), + ), + 'rgb' + ), + array( + array('r'=>0, 'g'=>0, 'b'=>0, 'a'=>2), + array( + 'r'=> new CSSSize(0, null, true), + 'g'=> new CSSSize(0, null, true), + 'b'=> new CSSSize(0, null, true) + ), + 'rgb' + ), + ); + } + + /** + * @dataProvider fromHSLProvider + **/ + public function testFromHSL($aHSL, $aExpectedColor, $aExpectedDescription) + { + $oColor = new CSSColor(); + $oColor->fromHSL($aHSL); + $this->assertEquals($oColor->getColor(), $aExpectedColor); + $this->assertEquals($oColor->getColorDescription(), $aExpectedDescription); + } + public function fromHSLProvider() + { + return array( + array( + array('h'=>60, 's'=>'100%', 'l'=>'50%'), + array( + 'r'=> new CSSSize(255, null, true), + 'g'=> new CSSSize(255, null, true), + 'b'=> new CSSSize(0, null, true) + ), + 'rgb' + ), + array( + array('h'=>540, 's'=>'120%', 'l'=>'50%'), + array( + 'r'=> new CSSSize(0, null, true), + 'g'=> new CSSSize(255, null, true), + 'b'=> new CSSSize(255, null, true) + ), + 'rgb' + ), + array( + array('h'=>480, 's'=>'120%', 'l'=>'-50%', 'a'=>0.3), + array( + 'r'=> new CSSSize(0, null, true), + 'g'=> new CSSSize(0, null, true), + 'b'=> new CSSSize(0, null, true), + 'a'=> new CSSSize(0.3, null, true) + ), + 'rgba' + ), + ); + } + + /** + * @dataProvider toHSLProvider + **/ + public function testToHSL($oColor, $aExpectedColor) + { + $aOriginalColor = $oColor->getColor(); + $oColor->toHSL(); + $this->assertEquals($oColor->getColor(), $aExpectedColor); + $oColor->toRGB(); + $this->assertEquals( + $oColor->getColor(), $aOriginalColor, + 'Failed to convert color back to RGB' + ); + } + public function toHSLProvider() + { + return array( + array( + new CSSColor('blue'), + array( + 'h'=> new CSSSize(240, null, true), + 's'=> new CSSSize(100, '%', true), + 'l'=> new CSSSize(50, '%', true) + ) + ), + array( + new CSSColor(array('r'=>255, 'g'=>0, 'b'=>0)), + array( + 'h'=> new CSSSize(0, null, true), + 's'=> new CSSSize(100, '%', true), + 'l'=> new CSSSize(50, '%', true) + ) + ), + array( + new CSSColor('transparent'), + array( + 'h'=> new CSSSize(0, null, true), + 's'=> new CSSSize(0, '%', true), + 'l'=> new CSSSize(0, '%', true), + 'a'=> new CSSSize(0, null, true) + ) + ), + ); + } +} diff --git a/tests/CSSParserTests.php b/tests/CSSParserTest.php similarity index 76% rename from tests/CSSParserTests.php rename to tests/CSSParserTest.php index a2c2f9c7..1e24420b 100644 --- a/tests/CSSParserTests.php +++ b/tests/CSSParserTest.php @@ -21,7 +21,8 @@ function testCssFiles() { //Either a file which SHOULD fail or a future test of a as-of-now missing feature continue; } - $oParser = new CSSParser(file_get_contents($sDirectory.DIRECTORY_SEPARATOR.$sFileName)); + $file = $sDirectory.DIRECTORY_SEPARATOR.$sFileName; + $oParser = new CSSParser(file_get_contents($file)); try { $oParser->parse()->__toString(); } catch(Exception $e) { @@ -37,35 +38,27 @@ function testCssFiles() { */ function testColorParsing() { $oDoc = $this->parsedStructureForFile('colortest'); - foreach($oDoc->getAllRuleSets() as $oRuleSet) { - if(!$oRuleSet instanceof CSSDeclarationBlock) { - continue; - } - $sSelector = $oRuleSet->getSelectors(); - $sSelector = $sSelector[0]->getSelector(); - if($sSelector == '#mine') { - $aColorRule = $oRuleSet->getRules('color'); - $aValues = $aColorRule['color']->getValues(); - $this->assertSame('red', $aValues[0][0]); - $aColorRule = $oRuleSet->getRules('background-'); - $aValues = $aColorRule['background-color']->getValues(); - $this->assertEquals(array('r' => new CSSSize(35.0, null, true), 'g' => new CSSSize(35.0, null, true), 'b' => new CSSSize(35.0, null, true)), $aValues[0][0]->getColor()); - $aColorRule = $oRuleSet->getRules('border-color'); - $aValues = $aColorRule['border-color']->getValues(); - $this->assertEquals(array('r' => new CSSSize(10.0, null, true), 'g' => new CSSSize(100.0, null, true), 'b' => new CSSSize(230.0, null, true), 'a' => new CSSSize(0.3, null, true)), $aValues[0][0]->getColor()); - $aColorRule = $oRuleSet->getRules('outline-color'); - $aValues = $aColorRule['outline-color']->getValues(); - $this->assertEquals(array('r' => new CSSSize(34.0, null, true), 'g' => new CSSSize(34.0, null, true), 'b' => new CSSSize(34.0, null, true)), $aValues[0][0]->getColor()); - } - } - foreach($oDoc->getAllValues('background-') as $oColor) { - if($oColor->getColorDescription() === 'hsl') { - $this->assertEquals(array('h' => new CSSSize(220.0, null, true), 's' => new CSSSize(10.0, null, true), 'l' => new CSSSize(220.0, null, true)), $oColor->getColor()); - } - } - foreach($oDoc->getAllValues('color') as $sColor) { - $this->assertSame('red', $sColor); - } + $this->assertSame( + "#mine {color: rgb(255,0,0);border-color: rgba(10,100,230,0.3);border-left-color: rgb(128,204,26);outline-color: rgb(34,34,34);background-color: rgb(35,35,35);}#yours {background-color: rgb(255,255,255);color: notacolor;outline-color: rgba(0,0,0,0);border-color: rgb(255,0,255);}", + $oDoc->__toString() + ); + foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration) + { + foreach($oDeclaration->getRules() as $oRule) + { + foreach($oRule->getValues() as $aValues) + { + if($aValues[0] instanceof CSSColor) + { + $aValues[0]->toHSL(); + } + } + } + } + $this->assertSame( + "#mine {color: hsl(0,100%,50%);border-color: hsla(215,92%,47%,0.3);border-left-color: hsl(86,77%,45%);outline-color: hsl(0,0%,13%);background-color: hsl(0,0%,14%);}#yours {background-color: hsl(0,0%,100%);color: notacolor;outline-color: hsla(0,0%,0%,0);border-color: hsl(300,100%,50%);}", + $oDoc->__toString() + ); } function testUnicodeParsing() { @@ -156,11 +149,11 @@ function testManipulation() { $this->assertSame('@charset "utf-8";@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}#my_id html, #my_id body {font-size: 1.6em;}', $oDoc->__toString()); $oDoc = $this->parsedStructureForFile('values'); - $this->assertSame('#header {margin: 10px 2em 1cm 2%;font-family: Verdana, Helvetica, "Gill Sans", sans-serif;font-size: 10px;color: red !important;}body {color: green;font: 75% "Lucida Grande", "Trebuchet MS", Verdana, sans-serif;}', $oDoc->__toString()); + $this->assertSame('#header {margin: 10px 2em 1cm 2%;font-family: Verdana, Helvetica, "Gill Sans", sans-serif;font-size: 10px;color: rgb(255,0,0) !important;}body {color: rgb(0,128,0);font: 75% "Lucida Grande", "Trebuchet MS", Verdana, sans-serif;}', $oDoc->__toString()); foreach($oDoc->getAllRuleSets() as $oRuleSet) { $oRuleSet->removeRule('font-'); } - $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;}body {color: green;}', $oDoc->__toString()); + $this->assertSame('#header {margin: 10px 2em 1cm 2%;color: rgb(255,0,0) !important;}body {color: rgb(0,128,0);}', $oDoc->__toString()); } function testSlashedValues() { diff --git a/tests/ColorUtilsTest.php b/tests/ColorUtilsTest.php new file mode 100644 index 00000000..f3128a5e --- /dev/null +++ b/tests/ColorUtilsTest.php @@ -0,0 +1,127 @@ +assertEquals($fResult, $fExcpected); + } + public function normalizeFractionProvider() + { + return array( + array(150, 1), + array('150%', 1), + array('50%', 0.5), + array(50, 0.5), + array(-150, 0), + array('-150%', 0), + ); + } + + /** + * @dataProvider normalizeRGBValueProvider + **/ + public function testNormalizeRGBValue($mValue, $fExcpected) + { + $fResult = ColorUtils::normalizeRGBValue($mValue); + $this->assertEquals($fResult, $fExcpected); + } + public function normalizeRGBValueProvider() + { + return array( + array(150, 150), + array('150%', 255), + array('50%', 128), + array(-150, 0), + array('-150%', 0), + ); + } + + /** + * @dataProvider hex2rgbProvider + **/ + public function testHex2rgb($sHexValue, $aExpected) + { + $aRGB = ColorUtils::hex2rgb($sHexValue); + $this->assertSame($aRGB, $aExpected); + } + public function hex2rgbProvider() + { + return array( + array('#ff0000', array('r'=>255, 'g'=>0, 'b'=>0)), + array('00ff00', array('r'=>0, 'g'=>255, 'b'=>0)), + array('#00f', array('r'=>0, 'g'=>0, 'b'=>255)), + array('BADA55', array('r'=>186, 'g'=>218, 'b'=>85)), + array('#FAIL', false), + // TODO: how do we handle that ? + array('FOOBAR', array('r'=>0, 'g'=>15, 'b'=>186)), + ); + } + + /** + * @dataProvider rgb2hexProvider + * @depends testNormalizeRGBValue + **/ + public function testRgb2hex($r, $g, $b, $sExpected) + { + $sHexValue = ColorUtils::rgb2hex($r, $g, $b); + $this->assertSame($sHexValue, $sExpected); + } + public function rgb2hexProvider() + { + return array( + array(255, 0, 0, '#ff0000'), + array(0, 0, 255, '#0000ff'), + array(186, 218, 85, '#bada55'), + array(302, -3, 'fail', '#ff0000'), + ); + } + + /** + * @dataProvider hsl2rgbProvider + * @depends testNormalizeFraction + **/ + public function testHsl2rgb($h, $s, $l, $aExpected) + { + $aRGB = ColorUtils::hsl2rgb($h, $s, $l, $aExpected); + // assertEquals because hsl2rgb returns an array of floats, + // even if they are rounded + $this->assertEquals($aRGB, $aExpected); + } + public function hsl2rgbProvider() + { + return array( + array(60, '100%', '50%', array('r'=>255, 'g'=>255, 'b'=>0)), + array(60, '100%', '25%', array('r'=>128, 'g'=>128, 'b'=>0)), + array(480, '120%', '-50%', array('r'=>0, 'g'=>0, 'b'=>0)), + array(540, '120%', '50%', array('r'=>0, 'g'=>255, 'b'=>255)), + ); + } + + /** + * @dataProvider rgb2hslProvider + * @depends testNormalizeRGBValue + **/ + public function testRgb2hsl($r, $g, $b, $aExpected) + { + $aHSL = ColorUtils::rgb2hsl($r, $g, $b, $aExpected); + // assertEquals because hsl2rgb returns an array of floats, + // even if they are rounded + $this->assertEquals($aHSL, $aExpected); + } + public function rgb2hslProvider() + { + return array( + array(0, 0, 255, array('h'=>240, 's'=>'100%', 'l'=>'50%')), + array(0, 255, 255, array('h'=>180, 's'=>'100%', 'l'=>'50%')), + array('100%', 255, 0, array('h'=>60, 's'=>'100%', 'l'=>'50%')), + array(382, '150%', -50, array('h'=>60, 's'=>'100%', 'l'=>'50%')), + ); + } + +} diff --git a/tests/files/colortest.css b/tests/files/colortest.css index 41fe2a2f..9e9e4e12 100644 --- a/tests/files/colortest.css +++ b/tests/files/colortest.css @@ -1,10 +1,14 @@ #mine { - color: red; - border-color: rgba(10, 100, 230, 0.3); - outline-color: #222; - background-color: #232323; + color: red; + border-color: rgba(10, 100, 230, 0.3); + border-left-color: rgb(50%, 80%, 10%); + outline-color: #222; + background-color: #232323; } #yours { - background-color: hsl(220, 10, 220); + background-color: hsl(220, 10%, 220%); + color: notacolor; + outline-color: transparent; + border-color: rgba(322, -5, 200%, 3.4); }