Color::__toString()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
/*
4
 * This file is part of the ILess
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace ILess;
11
12
use InvalidArgumentException;
13
14
/**
15
 * Color utility class.
16
 */
17
final class Color
18
{
19
    /**
20
     * HSL and HSV cache.
21
     *
22
     * @var array
23
     */
24
    protected $hsv, $hsl;
25
26
    /**
27
     * Luma cache.
28
     *
29
     * @var string
30
     */
31
    protected $luma;
32
33
    /**
34
     * Luminance cache.
35
     *
36
     * @var string
37
     */
38
    protected $luminance;
39
40
    /**
41
     * The rgb channels.
42
     *
43
     * @var array
44
     */
45
    public $rgb = [];
46
47
    /**
48
     * The alpha channel.
49
     *
50
     * @var int
51
     */
52
    public $alpha = 1;
53
54
    /**
55
     * Original format.
56
     *
57
     * @var bool
58
     */
59
    protected $short = false;
60
61
    /**
62
     * Created from keyword?
63
     *
64
     * @var bool
65
     */
66
    public $keyword = false;
67
68
    /**
69
     * @var string
70
     */
71
    protected $originalForm;
72
73
    /**
74
     * Transparent keyword?
75
     *
76
     * @var bool
77
     */
78
    public $isTransparentKeyword = false;
79
80
    /**
81
     * Constructor.
82
     *
83
     * @param array|string $rgb The RGB components as an array or string definition
84
     * @param int $alpha The alpha channel
85
     * @param string $originalForm
86
     */
87
    public function __construct($rgb = [255, 255, 255], $alpha = 1, $originalForm = null)
88
    {
89
        if (is_array($rgb)) {
90
            $this->rgb = $rgb;
91
        } // string
92
        else {
93
94
            // this is a named color
95
            if ($color = self::color($rgb)) {
96
                $this->keyword = $rgb;
0 ignored issues
show
Documentation Bug introduced by
The property $keyword was declared of type boolean, but $rgb is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
97
                $rgb = $color;
98
            }
99
100
            // strip #
101
            $rgb = trim($rgb, '#');
102
            if (strlen($rgb) == 6) {
103
                foreach (str_split($rgb, 2) as $c) {
104
                    $this->rgb[] = hexdec($c);
105
                }
106
            } elseif (strlen($rgb) == 3) {
107
                $this->short = true;
108
                foreach (str_split($rgb, 1) as $c) {
109
                    $this->rgb[] = hexdec($c . $c);
110
                }
111
            } elseif (strtolower($rgb) == 'transparent') {
112
                $this->rgb = [255, 255, 255];
113
                $this->isTransparentKeyword = true;
114
                $alpha = 0;
115
            } else {
116
                throw new InvalidArgumentException('Argument must be a color keyword or 3/6 digit hex e.g. #FFF.');
117
            }
118
        }
119
120
        $this->originalForm = $originalForm;
121
        // limit alpha channel
122
        $this->alpha = is_numeric($alpha) ? $alpha : 1;
0 ignored issues
show
Documentation Bug introduced by
It seems like is_numeric($alpha) ? $alpha : 1 can also be of type double or string. However, the property $alpha is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
123
    }
124
125
    /**
126
     * Returns the fixed RGB components (fitted into 0 - 255 range).
127
     *
128
     * @return array Array of red, green and blue components
129
     */
130
    protected function getFixedRGB()
131
    {
132
        $components = [];
133
        foreach ($this->rgb as $i) {
134
            $i = Math::round($i);
135
            if ($i > 255) {
136
                $i = 255;
137
            } elseif ($i < 0) {
138
                $i = 0;
139
            }
140
            $components[] = $i;
141
        }
142
143
        return $components;
144
    }
145
146
    protected function clamp($value, $max)
147
    {
148
        return min(max($value, 0), $max);
149
    }
150
151
    /**
152
     * Creates new color from the keyword.
153
     *
154
     * @param string $keyword
155
     *
156
     * @return Color
157
     */
158
    public static function fromKeyword($keyword)
159
    {
160
        $color = null;
161
        // is this named color?
162
        if (self::isNamedColor($keyword)) {
163
            $color = new self(substr(self::color($keyword), 1));
164
            $color->keyword = $keyword;
0 ignored issues
show
Documentation Bug introduced by
The property $keyword was declared of type boolean, but $keyword is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
165
        } elseif ($keyword === 'transparent') {
166
            $color = new self([255, 255, 255], 0);
167
            $color->isTransparentKeyword = true;
168
        }
169
170
        return $color;
171
    }
172
173
    /**
174
     * Returns the red channel.
175
     *
176
     * @return int
177
     */
178
    public function getRed()
179
    {
180
        return $this->rgb[0];
181
    }
182
183
    /**
184
     * Returns the green channel.
185
     *
186
     * @return int
187
     */
188
    public function getGreen()
189
    {
190
        return $this->rgb[1];
191
    }
192
193
    /**
194
     * Returns the blue channel.
195
     *
196
     * @return int
197
     */
198
    public function getBlue()
199
    {
200
        return $this->rgb[2];
201
    }
202
203
    /**
204
     * Returns the alpha channel.
205
     *
206
     * @return int
207
     */
208
    public function getAlpha()
209
    {
210
        return $this->alpha;
211
    }
212
213
    /**
214
     * Returns the color saturation.
215
     *
216
     * @return string
217
     */
218
    public function getSaturation()
219
    {
220
        $this->toHSL();
221
222
        return $this->hsl['s'];
223
    }
224
225
    /**
226
     * Returns the color hue.
227
     *
228
     * @param bool $round
229
     *
230
     * @return string
231
     */
232
    public function getHue()
233
    {
234
        $this->toHSL();
235
236
        return $this->hsl['h'];
237
    }
238
239
    /**
240
     * Returns the color lightness.
241
     *
242
     * @return string
243
     */
244
    public function getLightness()
245
    {
246
        $this->toHSL();
247
248
        return $this->hsl['l'];
249
    }
250
251
    /**
252
     * Returns the luma.
253
     *
254
     * @return int
255
     */
256
    public function getLuma()
257
    {
258
        if ($this->luma !== null) {
259
            return $this->luma;
260
        }
261
262
        $r = $this->rgb[0] / 255;
263
        $g = $this->rgb[1] / 255;
264
        $b = $this->rgb[2] / 255;
265
266
        $r = ($r <= 0.03928) ? $r / 12.92 : pow((($r + 0.055) / 1.055), 2.4);
267
        $g = ($g <= 0.03928) ? $g / 12.92 : pow((($g + 0.055) / 1.055), 2.4);
268
        $b = ($b <= 0.03928) ? $b / 12.92 : pow((($b + 0.055) / 1.055), 2.4);
269
270
        $this->luma = 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
0 ignored issues
show
Documentation Bug introduced by
The property $luma was declared of type string, but 0.2126 * $r + 0.71519999...9999 * $g + 0.0722 * $b is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
271
272
        return $this->luma;
273
    }
274
275
    /**
276
     * @return string
277
     */
278
    public function getLuminance()
279
    {
280
        if ($this->luminance !== null) {
281
            return $this->luminance;
282
        }
283
284
        $this->luminance = (0.2126 * $this->rgb[0] / 255) +
0 ignored issues
show
Documentation Bug introduced by
The property $luminance was declared of type string, but 0.2126 * $this->rgb[0] /...2 * $this->rgb[2] / 255 is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
285
            (0.7152 * $this->rgb[1] / 255) +
286
            (0.0722 * $this->rgb[2] / 255);
287
288
        return $this->luminance;
289
    }
290
291
    /**
292
     * Converts to HSL.
293
     *
294
     * @return array
295
     */
296
    public function toHSL()
297
    {
298
        if ($this->hsl) {
299
            return $this->hsl;
300
        }
301
302
        $r = $this->rgb[0] / 255;
303
        $g = $this->rgb[1] / 255;
304
        $b = $this->rgb[2] / 255;
305
        $a = $this->alpha;
306
307
        $max = max($r, $g, $b);
308
        $min = min($r, $g, $b);
309
        $l = ($max + $min) / 2;
310
        $d = $max - $min;
311
312
        if ($max === $min) {
313
            $h = $s = 0;
314
        } else {
315
            $s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
316
317
            switch ($max) {
318
                case $r:
319
                    $h = ($g - $b) / $d + ($g < $b ? 6 : 0);
320
                    break;
321
                case $g:
322
                    $h = ($b - $r) / $d + 2;
323
                    break;
324
                case $b:
325
                    $h = ($r - $g) / $d + 4;
326
                    break;
327
            }
328
            $h /= 6;
329
        }
330
331
        $this->hsl = ['h' => $h * 360, 's' => $s, 'l' => $l, 'a' => $a];
332
333
        return $this->hsl;
334
    }
335
336
    /**
337
     * Converts to HSV.
338
     *
339
     * @return array
340
     */
341
    public function toHSV()
342
    {
343
        if ($this->hsv) {
344
            return $this->hsv;
345
        }
346
347
        $r = $this->rgb[0] / 255;
348
        $g = $this->rgb[1] / 255;
349
        $b = $this->rgb[2] / 255;
350
        $a = $this->alpha;
351
352
        $max = max($r, $g, $b);
353
        $min = min($r, $g, $b);
354
355
        $v = $max;
356
357
        $d = $max - $min;
358
        if ($max === 0) {
359
            $s = 0;
360
        } else {
361
            $s = $d / $max;
362
        }
363
364
        if ($max === $min) {
365
            $h = 0;
366
        } else {
367
            switch ($max) {
368
                case $r:
369
                    $h = ($g - $b) / $d + ($g < $b ? 6 : 0);
370
                    break;
371
                case $g:
372
                    $h = ($b - $r) / $d + 2;
373
                    break;
374
                case $b:
375
                    $h = ($r - $g) / $d + 4;
376
                    break;
377
            }
378
            $h /= 6;
379
        }
380
381
        return ['h' => $h * 360, 's' => $s, 'v' => $v, 'a' => $a];
382
    }
383
384
    /**
385
     * Returns the string representation in ARGB model.
386
     *
387
     * @return string
388
     */
389
    public function toARGB()
390
    {
391
        $argb = array_merge(
392
            [$this->alpha * 255],
393
            $this->rgb
394
        );
395
396
        $result = '';
397
        foreach ($argb as $i) {
398
            $i = dechex($this->clamp(Math::round($i), 255));
399
            $result .= str_pad($i, 2, '0', STR_PAD_LEFT);
400
        }
401
402
        return '#' . $result;
403
    }
404
405
    private function toHex($rgb)
406
    {
407
        $parts = array_map(
408
            function ($c) {
409
                $c = $this->clamp(round($c), 255);
410
411
                return ($c < 16 ? '0' : '') . dechex($c);
412
            },
413
            $rgb
414
        );
415
416
        return '#' . implode('', $parts);
417
    }
418
419
    public function toRGB()
420
    {
421
        return $this->toHex($this->rgb);
422
    }
423
424
    /**
425
     * Returns the color as HEX string (when transparency present, in RGBA model).
426
     *
427
     * @param bool $compress Compress the color?
428
     * @param bool $canShorten Can the color be shortened if possible?
429
     *
430
     * @return string
431
     */
432
    public function toString($compress = false, $canShorten = false)
433
    {
434
        if ($this->isTransparentKeyword) {
435
            return 'transparent';
436
        }
437
438
        if ($this->originalForm) {
439
            return $this->originalForm;
440
        }
441
442
        $alpha = Math::toFixed($this->alpha + 2e-16, 8);
443
444
        if ($alpha < 1) {
445
            $fixedRGB = $this->getFixedRGB();
446
447
            return sprintf(
448
                'rgba(%s)',
449
                implode(
450
                    $compress ? ',' : ', ',
451
                    [
452
                        $fixedRGB[0],
453
                        $fixedRGB[1],
454
                        $fixedRGB[2],
455
                        Math::clean($this->clamp($alpha, 1)),
456
                    ]
457
                )
458
            );
459
        }
460
461
        // prevent named colors
462
        if ($this->keyword) {
463
            return $this->keyword;
464
        }
465
466
        $color = [];
467
        foreach ($this->getFixedRgb() as $i) {
468
            $color[] = str_pad(dechex(Math::round($i)), 2, '0', STR_PAD_LEFT);
469
        }
470
471
        $color = implode('', $color);
472
473
        // convert color to short format
474
        if ($canShorten && $color[0] === $color[1] && $color[2] === $color[3] && $color[4] === $color[5]) {
475
            $color = $color[0] . $color[2] . $color[4];
476
        }
477
478
        $color = '#' . $color;
479
480
        return $color;
481
    }
482
483
    /**
484
     * Converts the color to string.
485
     *
486
     * @return string
487
     */
488
    public function __toString()
489
    {
490
        return $this->toString();
491
    }
492
493
    /**
494
     * Does the color exits?
495
     *
496
     * @param string $color The color name
497
     *
498
     * @return string
499
     */
500
    public static function isNamedColor($color)
501
    {
502
        return isset(NamedColors::$colors[$color]);
503
    }
504
505
    /**
506
     * Returns the color hex representation or false.
507
     *
508
     * @param string $color Color name
509
     *
510
     * @return string|false
511
     */
512
    public static function color($color)
513
    {
514
        return self::isNamedColor($color) ? NamedColors::$colors[$color] : false;
515
    }
516
}
517