Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
188 / 188
100.00% covered (success)
100.00%
29 / 29
CRAP
100.00% covered (success)
100.00%
1 / 1
Rgb
100.00% covered (success)
100.00%
188 / 188
100.00% covered (success)
100.00%
29 / 29
93
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setR
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setG
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setB
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setA
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getR
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getG
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getB
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getA
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasA
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAlpha
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toCmyk
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 toGray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toHsl
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
10
 toHsv
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
10
 toHsb
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
10
 toHwb
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
9
 toLab
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 toLch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toOklab
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 toOklch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 labF
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 toHex
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 linearize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 delinearize
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 toArray
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 render
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
8
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 channels
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Color\Color;
16
17use OutOfRangeException;
18
19/**
20 * Pop Color RGB color class
21 *
22 * @category   Pop
23 * @package    Pop\Color
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    2.0.0
28 */
29class Rgb extends AbstractColor
30{
31
32    /**
33     * R value
34     * @var int
35     */
36    protected int $r = 0;
37
38    /**
39     * G value
40     * @var int
41     */
42    protected int $g = 0;
43
44    /**
45     * B value
46     * @var int
47     */
48    protected int $b = 0;
49
50    /**
51     * Alpha value
52     * @var ?float
53     */
54    protected ?float $a = null;
55
56    /**
57     * Constructor
58     *
59     * Instantiate the CSS RGB color object
60     *
61     * @param int|string        $r
62     * @param int|string        $g
63     * @param int|string        $b
64     * @param float|string|null $a
65     */
66    public function __construct(int|string $r, int|string $g, int|string $b, float|string|null $a = null)
67    {
68        $this->setR($r);
69        $this->setG($g);
70        $this->setB($b);
71        if ($a !== null) {
72            $this->setA($a);
73        }
74    }
75
76    /**
77     * Set R value
78     *
79     * @param  int|string $r
80     * @throws OutOfRangeException
81     * @return self
82     */
83    public function setR(int|string $r): self
84    {
85        $r = (int)$r;
86        if (($r > 255) || ($r < 0)) {
87            throw new OutOfRangeException('Error: The value of $r must be between 0 and 255.');
88        }
89        $this->r = $r;
90        return $this;
91    }
92
93    /**
94     * Set G value
95     *
96     * @param  int|string $g
97     * @throws OutOfRangeException
98     * @return self
99     */
100    public function setG(int|string $g): self
101    {
102        $g = (int)$g;
103        if (($g > 255) || ($g < 0)) {
104            throw new OutOfRangeException('Error: The value of $g must be between 0 and 255.');
105        }
106        $this->g = $g;
107        return $this;
108    }
109
110    /**
111     * Set B value
112     *
113     * @param  int|string $b
114     * @throws OutOfRangeException
115     * @return self
116     */
117    public function setB(int|string $b): self
118    {
119        $b = (int)$b;
120        if (($b > 255) || ($b < 0)) {
121            throw new OutOfRangeException('Error: The value of $b must be between 0 and 255.');
122        }
123        $this->b = $b;
124        return $this;
125    }
126
127    /**
128     * Set A value
129     *
130     * @param  float|string $a
131     * @throws OutOfRangeException
132     * @return self
133     */
134    public function setA(float|string $a): self
135    {
136        $a = (float)$a;
137        if (($a > 1) || ($a < 0)) {
138            throw new OutOfRangeException('Error: The value of $l must be between 0 and 1.');
139        }
140        $this->a = $a;
141        return $this;
142    }
143
144    /**
145     * Get R value
146     *
147     * @return int
148     */
149    public function getR(): int
150    {
151        return $this->r;
152    }
153
154    /**
155     * Get G value
156     *
157     * @return int
158     */
159    public function getG(): int
160    {
161        return $this->g;
162    }
163
164    /**
165     * Get B value
166     *
167     * @return int
168     */
169    public function getB(): int
170    {
171        return $this->b;
172    }
173
174    /**
175     * Get A value
176     *
177     * @return float|null
178     */
179    public function getA(): float|null
180    {
181        return $this->a;
182    }
183
184    /**
185     * Determine if the color object has an alpha value
186     *
187     * @return bool
188     */
189    public function hasA(): bool
190    {
191        return ($this->a !== null);
192    }
193
194    /**
195     * Determine if the color object has an alpha value (alias)
196     *
197     * @return bool
198     */
199    public function hasAlpha(): bool
200    {
201        return ($this->a !== null);
202    }
203
204    /**
205     * Convert to CMYK
206     *
207     * @return Cmyk
208     */
209    public function toCmyk(): Cmyk
210    {
211        $K = 1;
212
213        // Calculate CMY.
214        $cyan    = 1 - ($this->r / 255);
215        $magenta = 1 - ($this->g / 255);
216        $yellow  = 1 - ($this->b / 255);
217
218        // Calculate K.
219        if ($cyan < $K) {
220            $K = $cyan;
221        }
222        if ($magenta < $K) {
223            $K = $magenta;
224        }
225        if ($yellow < $K) {
226            $K = $yellow;
227        }
228
229        if ($K == 1) {
230            $cyan    = 0;
231            $magenta = 0;
232            $yellow  = 0;
233        } else {
234            $cyan    = round((($cyan - $K) / (1 - $K)) * 100);
235            $magenta = round((($magenta - $K) / (1 - $K)) * 100);
236            $yellow  = round((($yellow - $K) / (1 - $K)) * 100);
237        }
238
239        $black = round($K * 100);
240
241        return new Cmyk($cyan, $magenta, $yellow, $black);
242    }
243
244    /**
245     * Convert to Gray
246     *
247     * @return Grayscale
248     */
249    public function toGray(): Grayscale
250    {
251        return new Grayscale(floor(((floor(($this->r + $this->g + $this->b) / 3) / 255) * 100)));
252    }
253
254    /**
255     * Convert to HSL
256     *
257     * @return Hsl
258     */
259    public function toHsl(): Hsl
260    {
261        $r = $this->getR();
262        $g = $this->getG();
263        $b = $this->getB();
264
265        $min = min($r, min($g, $b));
266        $max = max($r, max($g, $b));
267        $delta = $max - $min;
268        $h = 0;
269
270        if ($delta > 0) {
271            if ($max == $r && $max != $g) $h += ($g - $b) / $delta;
272            if ($max == $g && $max != $b) $h += (2 + ($b - $r) / $delta);
273            if ($max == $b && $max != $r) $h += (4 + ($r - $g) / $delta);
274            $h /= 6;
275            if ($h < 0) {
276                $h += 1;
277            }
278        }
279
280        // Calculate the saturation and brightness.
281        $r = $this->getR() / 255;
282        $g = $this->getG() / 255;
283        $b = $this->getB() / 255;
284
285        $max = max($r, $g, $b);
286        $min = min($r, $g, $b);
287
288        $l = $max;
289        $d = $max - $min;
290        $s = ($d == 0) ? 0 : $d / $max;
291
292        return new Hsl((int)round($h * 360), (int)round($s * 100), (int)round($l * 100), $this->a);
293    }
294
295    /**
296     * Convert to HSV
297     *
298     * @return Hsv
299     */
300    public function toHsv(): Hsv
301    {
302        $r = $this->getR() / 255;
303        $g = $this->getG() / 255;
304        $b = $this->getB() / 255;
305
306        $max   = max($r, $g, $b);
307        $min   = min($r, $g, $b);
308        $delta = $max - $min;
309        $h     = 0;
310
311        if ($delta > 0) {
312            if ($max == $r && $max != $g) $h += ($g - $b) / $delta;
313            if ($max == $g && $max != $b) $h += (2 + ($b - $r) / $delta);
314            if ($max == $b && $max != $r) $h += (4 + ($r - $g) / $delta);
315            $h /= 6;
316            if ($h < 0) {
317                $h += 1;
318            }
319        }
320
321        $v = $max;
322        $s = ($max == 0) ? 0 : $delta / $max;
323
324        return new Hsv((int)round($h * 360), (int)round($s * 100), (int)round($v * 100), $this->a);
325    }
326
327    /**
328     * Convert to HSB
329     *
330     * @return Hsb
331     */
332    public function toHsb(): Hsb
333    {
334        $r = $this->getR() / 255;
335        $g = $this->getG() / 255;
336        $b = $this->getB() / 255;
337
338        $max   = max($r, $g, $b);
339        $min   = min($r, $g, $b);
340        $delta = $max - $min;
341        $h     = 0;
342
343        if ($delta > 0) {
344            if ($max == $r && $max != $g) $h += ($g - $b) / $delta;
345            if ($max == $g && $max != $b) $h += (2 + ($b - $r) / $delta);
346            if ($max == $b && $max != $r) $h += (4 + ($r - $g) / $delta);
347            $h /= 6;
348            if ($h < 0) {
349                $h += 1;
350            }
351        }
352
353        $brightness = $max;
354        $s = ($max == 0) ? 0 : $delta / $max;
355
356        return new Hsb((int)round($h * 360), (int)round($s * 100), (int)round($brightness * 100), $this->a);
357    }
358
359    /**
360     * Convert to HWB
361     *
362     * @return Hwb
363     */
364    public function toHwb(): Hwb
365    {
366        $r = $this->getR() / 255;
367        $g = $this->getG() / 255;
368        $b = $this->getB() / 255;
369
370        $max   = max($r, $g, $b);
371        $min   = min($r, $g, $b);
372        $delta = $max - $min;
373        $h     = 0;
374
375        if ($delta > 0) {
376            if ($max == $r && $max != $g) $h += ($g - $b) / $delta;
377            if ($max == $g && $max != $b) $h += (2 + ($b - $r) / $delta);
378            if ($max == $b && $max != $r) $h += (4 + ($r - $g) / $delta);
379            $h /= 6;
380            if ($h < 0) {
381                $h += 1;
382            }
383        }
384
385        $white = $min;
386        $black = 1 - $max;
387
388        return new Hwb((int)round($h * 360), (int)round($white * 100), (int)round($black * 100), $this->a);
389    }
390
391    /**
392     * Convert to CIE Lab
393     *
394     * @return Lab
395     */
396    public function toLab(): Lab
397    {
398        $xn = 0.9504559270516716;
399        $yn = 1.0;
400        $zn = 1.0890577507598784;
401
402        $epsilon = 216 / 24389;
403        $kappa   = 24389 / 27;
404
405        $lr = self::linearize($this->r / 255);
406        $lg = self::linearize($this->g / 255);
407        $lb = self::linearize($this->b / 255);
408
409        $x = 0.41239079926595934 * $lr + 0.357584339383878   * $lg + 0.1804807884018343  * $lb;
410        $y = 0.21263900587151027 * $lr + 0.715168678767756   * $lg + 0.07219231536073371 * $lb;
411        $z = 0.01933081871559182 * $lr + 0.11919477979462598 * $lg + 0.9505321522496607  * $lb;
412
413        $fx = self::labF($x / $xn, $epsilon, $kappa);
414        $fy = self::labF($y / $yn, $epsilon, $kappa);
415        $fz = self::labF($z / $zn, $epsilon, $kappa);
416
417        $L = (116 * $fy) - 16;
418        $A = 500 * ($fx - $fy);
419        $B = 200 * ($fy - $fz);
420
421        return new Lab(round($L, 2), round($A, 2), round($B, 2), $this->a);
422    }
423
424    /**
425     * Convert to CIE LCH
426     *
427     * @return Lch
428     */
429    public function toLch(): Lch
430    {
431        return $this->toLab()->toLch();
432    }
433
434    /**
435     * Convert to Oklab
436     *
437     * @return Oklab
438     */
439    public function toOklab(): Oklab
440    {
441        $lr = self::linearize($this->r / 255);
442        $lg = self::linearize($this->g / 255);
443        $lb = self::linearize($this->b / 255);
444
445        $l = 0.4122214708 * $lr + 0.5363325363 * $lg + 0.0514459929 * $lb;
446        $m = 0.2119034982 * $lr + 0.6806995451 * $lg + 0.1073969566 * $lb;
447        $s = 0.0883024619 * $lr + 0.2817188376 * $lg + 0.6299787005 * $lb;
448
449        $l_ = $l ** (1 / 3);
450        $m_ = $m ** (1 / 3);
451        $s_ = $s ** (1 / 3);
452
453        $L = 0.2104542553 * $l_ + 0.7936177850 * $m_ - 0.0040720468 * $s_;
454        $A = 1.9779984951 * $l_ - 2.4285922050 * $m_ + 0.4505937099 * $s_;
455        $B = 0.0259040371 * $l_ + 0.7827717662 * $m_ - 0.8086757660 * $s_;
456
457        return new Oklab(round($L, 4), round($A, 4), round($B, 4), $this->a);
458    }
459
460    /**
461     * Convert to Oklch
462     *
463     * @return Oklch
464     */
465    public function toOklch(): Oklch
466    {
467        return $this->toOklab()->toOklch();
468    }
469
470    /**
471     * CIE Lab f() nonlinearity
472     *
473     * @param  float $t
474     * @param  float $epsilon
475     * @param  float $kappa
476     * @return float
477     */
478    private static function labF(float $t, float $epsilon, float $kappa): float
479    {
480        return ($t > $epsilon) ? ($t ** (1 / 3)) : ((($kappa * $t) + 16) / 116);
481    }
482
483    /**
484     * Convert to hex
485     *
486     * @return Hex
487     */
488    public function toHex(): Hex
489    {
490        $hex = str_pad(dechex($this->r), 2, '0', STR_PAD_LEFT) . str_pad(dechex($this->g), 2, '0', STR_PAD_LEFT) . str_pad(dechex($this->b), 2, '0', STR_PAD_LEFT);
491        if ($this->a !== null) {
492            $hex .= str_pad(dechex((int)round($this->a * 255)), 2, '0', STR_PAD_LEFT);
493        }
494        return new Hex($hex);
495    }
496
497    /**
498     * Convert a gamma-encoded sRGB channel value (0-1) to its linear-light equivalent
499     *
500     * @param  float $value
501     * @return float
502     */
503    public static function linearize(float $value): float
504    {
505        return ($value <= 0.04045) ? ($value / 12.92) : ((($value + 0.055) / 1.055) ** 2.4);
506    }
507
508    /**
509     * Convert a linear-light sRGB channel value (0-1) back to its gamma-encoded equivalent
510     *
511     * @param  float $value
512     * @return float
513     */
514    public static function delinearize(float $value): float
515    {
516        if ($value == 0.0) {
517            return 0.0;
518        }
519        if ($value == 1.0) {
520            return 1.0;
521        }
522        return ($value <= 0.0031308) ? ($value * 12.92) : ((1.055 * ($value ** (1 / 2.4))) - 0.055);
523    }
524
525    /**
526     * Convert to array
527     *
528     * @param  bool $assoc
529     * @return array
530     */
531    public function toArray(bool $assoc = true): array
532    {
533        $rgb = [];
534
535        if ($assoc) {
536            $rgb['r'] = $this->r;
537            $rgb['g'] = $this->g;
538            $rgb['b'] = $this->b;
539            if ($this->a !== null) {
540                $rgb['a'] = $this->a;
541            }
542        } else {
543            $rgb[] = $this->r;
544            $rgb[] = $this->g;
545            $rgb[] = $this->b;
546            if ($this->a !== null) {
547                $rgb[] = $this->a;
548            }
549        }
550
551        return $rgb;
552    }
553
554    /**
555     * Convert to readable string
556     *
557     * @param  ?string $format
558     * @return string
559     */
560    public function render(?string $format = null): string
561    {
562        return match ($format) {
563            self::COMMA => $this->r . ', ' . $this->g . ', ' . $this->b . (!empty($this->a) ? ', ' . $this->a : ''),
564            self::CSS => (($this->a !== null) ? 'rgba(' : 'rgb(') . implode(', ', $this->toArray()) . ')',
565            self::PERCENT => round(($this->r / 255), 2) . ' ' . round(($this->g / 255), 2) . ' ' . round(($this->b / 255), 2),
566            default => $this->r . ' ' . $this->g . ' ' . $this->b . (!empty($this->a) ? ' ' . $this->a : ''),
567        };
568    }
569
570    /**
571     * Return CSS-formatted string
572     *
573     * @return string
574     */
575    public function __toString(): string
576    {
577        return $this->render(self::CSS);
578    }
579
580    /**
581     * Get the valid channel names
582     *
583     * @return array
584     */
585    protected function channels(): array
586    {
587        return ['r', 'g', 'b', 'a'];
588    }
589
590}