1: <?php
2:
3: /*
4: * The MIT License (MIT)
5: *
6: * Copyright (c) 2013 Jonathan Vollebregt (jnvsor@gmail.com), Rokas Šleinius (raveren@gmail.com)
7: *
8: * Permission is hereby granted, free of charge, to any person obtaining a copy of
9: * this software and associated documentation files (the "Software"), to deal in
10: * the Software without restriction, including without limitation the rights to
11: * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12: * the Software, and to permit persons to whom the Software is furnished to do so,
13: * subject to the following conditions:
14: *
15: * The above copyright notice and this permission notice shall be included in all
16: * copies or substantial portions of the Software.
17: *
18: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20: * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21: * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22: * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23: * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24: */
25:
26: namespace Kint\Object\Representation;
27:
28: use InvalidArgumentException;
29:
30: class ColorRepresentation extends Representation
31: {
32: const COLOR_NAME = 1;
33: const COLOR_HEX_3 = 2;
34: const COLOR_HEX_6 = 3;
35: const COLOR_RGB = 4;
36: const COLOR_RGBA = 5;
37: const COLOR_HSL = 6;
38: const COLOR_HSLA = 7;
39: const COLOR_HEX_4 = 8;
40: const COLOR_HEX_8 = 9;
41:
42: public static $color_map = array(
43: 'aliceblue' => 'f0f8ff',
44: 'antiquewhite' => 'faebd7',
45: 'aqua' => '00ffff',
46: 'aquamarine' => '7fffd4',
47: 'azure' => 'f0ffff',
48: 'beige' => 'f5f5dc',
49: 'bisque' => 'ffe4c4',
50: 'black' => '000000',
51: 'blanchedalmond' => 'ffebcd',
52: 'blue' => '0000ff',
53: 'blueviolet' => '8a2be2',
54: 'brown' => 'a52a2a',
55: 'burlywood' => 'deb887',
56: 'cadetblue' => '5f9ea0',
57: 'chartreuse' => '7fff00',
58: 'chocolate' => 'd2691e',
59: 'coral' => 'ff7f50',
60: 'cornflowerblue' => '6495ed',
61: 'cornsilk' => 'fff8dc',
62: 'crimson' => 'dc143c',
63: 'cyan' => '00ffff',
64: 'darkblue' => '00008b',
65: 'darkcyan' => '008b8b',
66: 'darkgoldenrod' => 'b8860b',
67: 'darkgray' => 'a9a9a9',
68: 'darkgreen' => '006400',
69: 'darkgrey' => 'a9a9a9',
70: 'darkkhaki' => 'bdb76b',
71: 'darkmagenta' => '8b008b',
72: 'darkolivegreen' => '556b2f',
73: 'darkorange' => 'ff8c00',
74: 'darkorchid' => '9932cc',
75: 'darkred' => '8b0000',
76: 'darksalmon' => 'e9967a',
77: 'darkseagreen' => '8fbc8f',
78: 'darkslateblue' => '483d8b',
79: 'darkslategray' => '2f4f4f',
80: 'darkslategrey' => '2f4f4f',
81: 'darkturquoise' => '00ced1',
82: 'darkviolet' => '9400d3',
83: 'deeppink' => 'ff1493',
84: 'deepskyblue' => '00bfff',
85: 'dimgray' => '696969',
86: 'dimgrey' => '696969',
87: 'dodgerblue' => '1e90ff',
88: 'firebrick' => 'b22222',
89: 'floralwhite' => 'fffaf0',
90: 'forestgreen' => '228b22',
91: 'fuchsia' => 'ff00ff',
92: 'gainsboro' => 'dcdcdc',
93: 'ghostwhite' => 'f8f8ff',
94: 'gold' => 'ffd700',
95: 'goldenrod' => 'daa520',
96: 'gray' => '808080',
97: 'green' => '008000',
98: 'greenyellow' => 'adff2f',
99: 'grey' => '808080',
100: 'honeydew' => 'f0fff0',
101: 'hotpink' => 'ff69b4',
102: 'indianred' => 'cd5c5c',
103: 'indigo' => '4b0082',
104: 'ivory' => 'fffff0',
105: 'khaki' => 'f0e68c',
106: 'lavender' => 'e6e6fa',
107: 'lavenderblush' => 'fff0f5',
108: 'lawngreen' => '7cfc00',
109: 'lemonchiffon' => 'fffacd',
110: 'lightblue' => 'add8e6',
111: 'lightcoral' => 'f08080',
112: 'lightcyan' => 'e0ffff',
113: 'lightgoldenrodyellow' => 'fafad2',
114: 'lightgray' => 'd3d3d3',
115: 'lightgreen' => '90ee90',
116: 'lightgrey' => 'd3d3d3',
117: 'lightpink' => 'ffb6c1',
118: 'lightsalmon' => 'ffa07a',
119: 'lightseagreen' => '20b2aa',
120: 'lightskyblue' => '87cefa',
121: 'lightslategray' => '778899',
122: 'lightslategrey' => '778899',
123: 'lightsteelblue' => 'b0c4de',
124: 'lightyellow' => 'ffffe0',
125: 'lime' => '00ff00',
126: 'limegreen' => '32cd32',
127: 'linen' => 'faf0e6',
128: 'magenta' => 'ff00ff',
129: 'maroon' => '800000',
130: 'mediumaquamarine' => '66cdaa',
131: 'mediumblue' => '0000cd',
132: 'mediumorchid' => 'ba55d3',
133: 'mediumpurple' => '9370db',
134: 'mediumseagreen' => '3cb371',
135: 'mediumslateblue' => '7b68ee',
136: 'mediumspringgreen' => '00fa9a',
137: 'mediumturquoise' => '48d1cc',
138: 'mediumvioletred' => 'c71585',
139: 'midnightblue' => '191970',
140: 'mintcream' => 'f5fffa',
141: 'mistyrose' => 'ffe4e1',
142: 'moccasin' => 'ffe4b5',
143: 'navajowhite' => 'ffdead',
144: 'navy' => '000080',
145: 'oldlace' => 'fdf5e6',
146: 'olive' => '808000',
147: 'olivedrab' => '6b8e23',
148: 'orange' => 'ffa500',
149: 'orangered' => 'ff4500',
150: 'orchid' => 'da70d6',
151: 'palegoldenrod' => 'eee8aa',
152: 'palegreen' => '98fb98',
153: 'paleturquoise' => 'afeeee',
154: 'palevioletred' => 'db7093',
155: 'papayawhip' => 'ffefd5',
156: 'peachpuff' => 'ffdab9',
157: 'peru' => 'cd853f',
158: 'pink' => 'ffc0cb',
159: 'plum' => 'dda0dd',
160: 'powderblue' => 'b0e0e6',
161: 'purple' => '800080',
162: 'rebeccapurple' => '663399',
163: 'red' => 'ff0000',
164: 'rosybrown' => 'bc8f8f',
165: 'royalblue' => '4169e1',
166: 'saddlebrown' => '8b4513',
167: 'salmon' => 'fa8072',
168: 'sandybrown' => 'f4a460',
169: 'seagreen' => '2e8b57',
170: 'seashell' => 'fff5ee',
171: 'sienna' => 'a0522d',
172: 'silver' => 'c0c0c0',
173: 'skyblue' => '87ceeb',
174: 'slateblue' => '6a5acd',
175: 'slategray' => '708090',
176: 'slategrey' => '708090',
177: 'snow' => 'fffafa',
178: 'springgreen' => '00ff7f',
179: 'steelblue' => '4682b4',
180: 'tan' => 'd2b48c',
181: 'teal' => '008080',
182: 'thistle' => 'd8bfd8',
183: 'tomato' => 'ff6347',
184: // To quote MDN:
185: // "Technically, transparent is a shortcut for rgba(0,0,0,0)."
186: 'transparent' => '00000000',
187: 'turquoise' => '40e0d0',
188: 'violet' => 'ee82ee',
189: 'wheat' => 'f5deb3',
190: 'white' => 'ffffff',
191: 'whitesmoke' => 'f5f5f5',
192: 'yellow' => 'ffff00',
193: 'yellowgreen' => '9acd32',
194: );
195:
196: public $r = 0;
197: public $g = 0;
198: public $b = 0;
199: public $a = 1.0;
200: public $variant;
201: public $implicit_label = true;
202: public $hints = array('color');
203:
204: public function __construct($value)
205: {
206: parent::__construct('Color');
207:
208: $this->contents = $value;
209: $this->setValues($value);
210: }
211:
212: public function getColor($variant = null)
213: {
214: if (!$variant) {
215: $variant = $this->variant;
216: }
217:
218: switch ($variant) {
219: case self::COLOR_NAME:
220: $hex = \sprintf('%02x%02x%02x', $this->r, $this->g, $this->b);
221: $hex_alpha = \sprintf('%02x%02x%02x%02x', $this->r, $this->g, $this->b, \round($this->a * 0xFF));
222:
223: return \array_search($hex, self::$color_map, true) ?: \array_search($hex_alpha, self::$color_map, true);
224: case self::COLOR_HEX_3:
225: if (0 === $this->r % 0x11 && 0 === $this->g % 0x11 && 0 === $this->b % 0x11) {
226: return \sprintf(
227: '#%1X%1X%1X',
228: \round($this->r / 0x11),
229: \round($this->g / 0x11),
230: \round($this->b / 0x11)
231: );
232: }
233:
234: return false;
235: case self::COLOR_HEX_6:
236: return \sprintf('#%02X%02X%02X', $this->r, $this->g, $this->b);
237: case self::COLOR_RGB:
238: if (1.0 === $this->a) {
239: return \sprintf('rgb(%d, %d, %d)', $this->r, $this->g, $this->b);
240: }
241:
242: return \sprintf('rgb(%d, %d, %d, %s)', $this->r, $this->g, $this->b, \round($this->a, 4));
243: case self::COLOR_RGBA:
244: return \sprintf('rgba(%d, %d, %d, %s)', $this->r, $this->g, $this->b, \round($this->a, 4));
245: case self::COLOR_HSL:
246: $val = self::rgbToHsl($this->r, $this->g, $this->b);
247: if (1.0 === $this->a) {
248: return \vsprintf('hsl(%d, %d%%, %d%%)', $val);
249: }
250:
251: return \sprintf('hsl(%d, %d%%, %d%%, %s)', $val[0], $val[1], $val[2], \round($this->a, 4));
252: case self::COLOR_HSLA:
253: $val = self::rgbToHsl($this->r, $this->g, $this->b);
254:
255: return \sprintf('hsla(%d, %d%%, %d%%, %s)', $val[0], $val[1], $val[2], \round($this->a, 4));
256: case self::COLOR_HEX_4:
257: if (0 === $this->r % 0x11 && 0 === $this->g % 0x11 && 0 === $this->b % 0x11 && 0 === ($this->a * 255) % 0x11) {
258: return \sprintf(
259: '#%1X%1X%1X%1X',
260: \round($this->r / 0x11),
261: \round($this->g / 0x11),
262: \round($this->b / 0x11),
263: \round($this->a * 0xF)
264: );
265: }
266:
267: return false;
268:
269: case self::COLOR_HEX_8:
270: return \sprintf('#%02X%02X%02X%02X', $this->r, $this->g, $this->b, \round($this->a * 0xFF));
271: }
272:
273: return false;
274: }
275:
276: public function hasAlpha($variant = null)
277: {
278: if (null === $variant) {
279: $variant = $this->variant;
280: }
281:
282: switch ($variant) {
283: case self::COLOR_NAME:
284: case self::COLOR_RGB:
285: case self::COLOR_HSL:
286: return \abs($this->a - 1) >= 0.0001;
287: case self::COLOR_RGBA:
288: case self::COLOR_HSLA:
289: case self::COLOR_HEX_4:
290: case self::COLOR_HEX_8:
291: return true;
292: default:
293: return false;
294: }
295: }
296:
297: protected function setValues($value)
298: {
299: $value = \strtolower(\trim($value));
300: // Find out which variant of color input it is
301: if (isset(self::$color_map[$value])) {
302: if (!$this->setValuesFromHex(self::$color_map[$value])) {
303: return;
304: }
305:
306: $variant = self::COLOR_NAME;
307: } elseif ('#' === $value[0]) {
308: $variant = $this->setValuesFromHex(\substr($value, 1));
309:
310: if (!$variant) {
311: return;
312: }
313: } else {
314: $variant = $this->setValuesFromFunction($value);
315:
316: if (!$variant) {
317: return;
318: }
319: }
320:
321: // If something has gone horribly wrong
322: if ($this->r > 0xFF || $this->g > 0xFF || $this->b > 0xFF || $this->a > 1) {
323: $this->variant = null; // @codeCoverageIgnore
324: } else {
325: $this->variant = $variant;
326: $this->r = (int) $this->r;
327: $this->g = (int) $this->g;
328: $this->b = (int) $this->b;
329: $this->a = (float) $this->a;
330: }
331: }
332:
333: protected function setValuesFromHex($hex)
334: {
335: if (!\ctype_xdigit($hex)) {
336: return null;
337: }
338:
339: switch (\strlen($hex)) {
340: case 3:
341: $variant = self::COLOR_HEX_3;
342: break;
343: case 6:
344: $variant = self::COLOR_HEX_6;
345: break;
346: case 4:
347: $variant = self::COLOR_HEX_4;
348: break;
349: case 8:
350: $variant = self::COLOR_HEX_8;
351: break;
352: default:
353: return null;
354: }
355:
356: switch ($variant) {
357: case self::COLOR_HEX_4:
358: $this->a = \hexdec($hex[3]) / 0xF;
359: // no break
360: case self::COLOR_HEX_3:
361: $this->r = \hexdec($hex[0]) * 0x11;
362: $this->g = \hexdec($hex[1]) * 0x11;
363: $this->b = \hexdec($hex[2]) * 0x11;
364: break;
365: case self::COLOR_HEX_8:
366: $this->a = \hexdec(\substr($hex, 6, 2)) / 0xFF;
367: // no break
368: case self::COLOR_HEX_6:
369: $hex = \str_split($hex, 2);
370: $this->r = \hexdec($hex[0]);
371: $this->g = \hexdec($hex[1]);
372: $this->b = \hexdec($hex[2]);
373: break;
374: }
375:
376: return $variant;
377: }
378:
379: protected function setValuesFromFunction($value)
380: {
381: if (!\preg_match('/^((?:rgb|hsl)a?)\\s*\\(([0-9\\.%,\\s\\/\\-]+)\\)$/i', $value, $match)) {
382: return null;
383: }
384:
385: switch (\strtolower($match[1])) {
386: case 'rgb':
387: $variant = self::COLOR_RGB;
388: break;
389: case 'rgba':
390: $variant = self::COLOR_RGBA;
391: break;
392: case 'hsl':
393: $variant = self::COLOR_HSL;
394: break;
395: case 'hsla':
396: $variant = self::COLOR_HSLA;
397: break;
398: default:
399: return null; // @codeCoverageIgnore
400: }
401:
402: $params = \preg_replace('/[,\\s\\/]+/', ',', \trim($match[2]));
403: $params = \explode(',', $params);
404: $params = \array_map('trim', $params);
405:
406: if (\count($params) < 3 || \count($params) > 4) {
407: return null;
408: }
409:
410: foreach ($params as $i => &$color) {
411: if (false !== \strpos($color, '%')) {
412: $color = (float) \str_replace('%', '', $color);
413:
414: if (3 === $i) {
415: $color = $color / 100;
416: } elseif (\in_array($variant, array(self::COLOR_RGB, self::COLOR_RGBA), true)) {
417: $color = \round($color / 100 * 0xFF);
418: }
419: }
420:
421: $color = (float) $color;
422:
423: if (0 === $i && \in_array($variant, array(self::COLOR_HSL, self::COLOR_HSLA), true)) {
424: $color = ($color % 360 + 360) % 360;
425: }
426: }
427:
428: /** @var float[] Psalm bug workaround */
429: $params = \array_map('floatval', $params);
430:
431: switch ($variant) {
432: case self::COLOR_RGBA:
433: case self::COLOR_RGB:
434: if (\min($params) < 0 || \max($params) > 0xFF) {
435: return null;
436: }
437: break;
438: case self::COLOR_HSLA:
439: case self::COLOR_HSL:
440: if (\min($params) < 0 || $params[0] > 360 || \max($params[1], $params[2]) > 100) {
441: return null;
442: }
443: break;
444: }
445:
446: if (4 === \count($params)) {
447: if ($params[3] > 1) {
448: return null;
449: }
450:
451: $this->a = $params[3];
452: }
453:
454: if (self::COLOR_HSLA === $variant || self::COLOR_HSL === $variant) {
455: $params = self::hslToRgb($params[0], $params[1], $params[2]);
456: }
457:
458: list($this->r, $this->g, $this->b) = $params;
459:
460: return $variant;
461: }
462:
463: /**
464: * Turns HSL color to RGB. Black magic.
465: *
466: * @param float $h Hue
467: * @param float $s Saturation
468: * @param float $l Lightness
469: *
470: * @return int[] RGB array
471: */
472: public static function hslToRgb($h, $s, $l)
473: {
474: if (\min($h, $s, $l) < 0) {
475: throw new InvalidArgumentException('The parameters for hslToRgb should be no less than 0');
476: }
477:
478: if ($h > 360 || \max($s, $l) > 100) {
479: throw new InvalidArgumentException('The parameters for hslToRgb should be no more than 360, 100, and 100 respectively');
480: }
481:
482: $h /= 360;
483: $s /= 100;
484: $l /= 100;
485:
486: $m2 = ($l <= 0.5) ? $l * ($s + 1) : $l + $s - $l * $s;
487: $m1 = $l * 2 - $m2;
488:
489: return array(
490: (int) \round(self::hueToRgb($m1, $m2, $h + 1 / 3) * 0xFF),
491: (int) \round(self::hueToRgb($m1, $m2, $h) * 0xFF),
492: (int) \round(self::hueToRgb($m1, $m2, $h - 1 / 3) * 0xFF),
493: );
494: }
495:
496: /**
497: * Converts RGB to HSL. Color inversion of previous black magic is white magic?
498: *
499: * @param float|int $red Red
500: * @param float|int $green Green
501: * @param float|int $blue Blue
502: *
503: * @return float[] HSL array
504: */
505: public static function rgbToHsl($red, $green, $blue)
506: {
507: if (\min($red, $green, $blue) < 0) {
508: throw new InvalidArgumentException('The parameters for rgbToHsl should be no less than 0');
509: }
510:
511: if (\max($red, $green, $blue) > 0xFF) {
512: throw new InvalidArgumentException('The parameters for rgbToHsl should be no more than 255');
513: }
514:
515: $clrMin = \min($red, $green, $blue);
516: $clrMax = \max($red, $green, $blue);
517: $deltaMax = $clrMax - $clrMin;
518:
519: $L = ($clrMax + $clrMin) / 510;
520:
521: if (0 == $deltaMax) {
522: $H = 0;
523: $S = 0;
524: } else {
525: if (0.5 > $L) {
526: $S = $deltaMax / ($clrMax + $clrMin);
527: } else {
528: $S = $deltaMax / (510 - $clrMax - $clrMin);
529: }
530:
531: if ($clrMax === $red) {
532: $H = ($green - $blue) / (6.0 * $deltaMax);
533:
534: if (0 > $H) {
535: $H += 1.0;
536: }
537: } elseif ($clrMax === $green) {
538: $H = 1 / 3 + ($blue - $red) / (6.0 * $deltaMax);
539: } else {
540: $H = 2 / 3 + ($red - $green) / (6.0 * $deltaMax);
541: }
542: }
543:
544: return array(
545: (float) ($H * 360 % 360),
546: (float) ($S * 100),
547: (float) ($L * 100),
548: );
549: }
550:
551: /**
552: * Helper function for hslToRgb. Even blacker magic.
553: *
554: *
555: * @param float $m1
556: * @param float $m2
557: * @param float $hue
558: *
559: * @return float Color value
560: */
561: private static function hueToRgb($m1, $m2, $hue)
562: {
563: $hue = ($hue < 0) ? $hue + 1 : (($hue > 1) ? $hue - 1 : $hue);
564: if ($hue * 6 < 1) {
565: return $m1 + ($m2 - $m1) * $hue * 6;
566: }
567: if ($hue * 2 < 1) {
568: return $m2;
569: }
570: if ($hue * 3 < 2) {
571: return $m1 + ($m2 - $m1) * (2 / 3 - $hue) * 6;
572: }
573:
574: return $m1;
575: }
576: }
577: