Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
17 / 17
CRAP
100.00% covered (success)
100.00%
1 / 1
Lab
100.00% covered (success)
100.00%
76 / 76
100.00% covered (success)
100.00%
17 / 17
38
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
 setL
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
 setB
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setAlpha
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getL
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
 getB
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAlpha
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
 toRgb
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 toLch
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 labFInv
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 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%
7 / 7
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 CIE Lab 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 Lab extends AbstractColor
30{
31
32    /**
33     * L value
34     * @var float
35     */
36    protected float $l = 0;
37
38    /**
39     * A value
40     * @var float
41     */
42    protected float $a = 0;
43
44    /**
45     * B value
46     * @var float
47     */
48    protected float $b = 0;
49
50    /**
51     * Alpha value
52     * @var ?float
53     */
54    protected ?float $alpha = null;
55
56    /**
57     * Constructor
58     *
59     * Instantiate the CIE Lab color object
60     *
61     * @param float|string      $l
62     * @param float|string      $a
63     * @param float|string      $b
64     * @param float|string|null $alpha
65     */
66    public function __construct(float|string $l, float|string $a, float|string $b, float|string|null $alpha = null)
67    {
68        $this->setL($l);
69        $this->setA($a);
70        $this->setB($b);
71        if ($alpha !== null) {
72            $this->setAlpha($alpha);
73        }
74    }
75
76    /**
77     * Set L value
78     *
79     * @param  float|string $l
80     * @throws OutOfRangeException
81     * @return self
82     */
83    public function setL(float|string $l): self
84    {
85        $l = (float)$l;
86        if (($l > 100) || ($l < 0)) {
87            throw new OutOfRangeException('Error: The value of $l must be between 0 and 100.');
88        }
89        $this->l = $l;
90        return $this;
91    }
92
93    /**
94     * Set A value
95     *
96     * @param  float|string $a
97     * @throws OutOfRangeException
98     * @return self
99     */
100    public function setA(float|string $a): self
101    {
102        $a = (float)$a;
103        if (($a > 127) || ($a < -128)) {
104            throw new OutOfRangeException('Error: The value of $a must be between -128 and 127.');
105        }
106        $this->a = $a;
107        return $this;
108    }
109
110    /**
111     * Set B value
112     *
113     * @param  float|string $b
114     * @throws OutOfRangeException
115     * @return self
116     */
117    public function setB(float|string $b): self
118    {
119        $b = (float)$b;
120        if (($b > 127) || ($b < -128)) {
121            throw new OutOfRangeException('Error: The value of $b must be between -128 and 127.');
122        }
123        $this->b = $b;
124        return $this;
125    }
126
127    /**
128     * Set alpha value
129     *
130     * @param  float|string $alpha
131     * @throws OutOfRangeException
132     * @return self
133     */
134    public function setAlpha(float|string $alpha): self
135    {
136        $alpha = (float)$alpha;
137        if (($alpha > 1) || ($alpha < 0)) {
138            throw new OutOfRangeException('Error: The value of $alpha must be between 0 and 1.');
139        }
140        $this->alpha = $alpha;
141        return $this;
142    }
143
144    /**
145     * Get L value
146     *
147     * @return float
148     */
149    public function getL(): float
150    {
151        return $this->l;
152    }
153
154    /**
155     * Get A value
156     *
157     * @return float
158     */
159    public function getA(): float
160    {
161        return $this->a;
162    }
163
164    /**
165     * Get B value
166     *
167     * @return float
168     */
169    public function getB(): float
170    {
171        return $this->b;
172    }
173
174    /**
175     * Get alpha value
176     *
177     * @return float|null
178     */
179    public function getAlpha(): float|null
180    {
181        return $this->alpha;
182    }
183
184    /**
185     * Determine if the color object has an alpha value
186     *
187     * @return bool
188     */
189    public function hasAlpha(): bool
190    {
191        return ($this->alpha !== null);
192    }
193
194    /**
195     * Convert to RGB
196     *
197     * @return Rgb
198     */
199    public function toRgb(): Rgb
200    {
201        $xn    = 0.9504559270516716;
202        $yn    = 1.0;
203        $zn    = 1.0890577507598784;
204        $kappa = 24389 / 27;
205
206        $fy = ($this->l + 16) / 116;
207        $fx = $fy + ($this->a / 500);
208        $fz = $fy - ($this->b / 200);
209
210        $x = $xn * self::labFInv($fx, $kappa);
211        $y = $yn * self::labFInv($fy, $kappa);
212        $z = $zn * self::labFInv($fz, $kappa);
213
214        $lr =  3.2409699419045226 * $x - 1.537383177570094  * $y - 0.4986107602930034  * $z;
215        $lg = -0.9692436362808796 * $x + 1.8759675015077202 * $y + 0.04155505740717559 * $z;
216        $lb =  0.05563007969699366 * $x - 0.20397695888897652 * $y + 1.0569715142428786 * $z;
217
218        $r = (int)max(0, min(255, round(Rgb::delinearize($lr) * 255)));
219        $g = (int)max(0, min(255, round(Rgb::delinearize($lg) * 255)));
220        $b = (int)max(0, min(255, round(Rgb::delinearize($lb) * 255)));
221
222        return new Rgb($r, $g, $b, $this->alpha);
223    }
224
225    /**
226     * Convert to LCH
227     *
228     * @return Lch
229     */
230    public function toLch(): Lch
231    {
232        $c = sqrt(($this->a * $this->a) + ($this->b * $this->b));
233        $h = rad2deg(atan2($this->b, $this->a));
234        if ($h < 0) {
235            $h += 360;
236        }
237
238        return new Lch(round($this->l, 2), round($c, 2), (int)round($h), $this->alpha);
239    }
240
241    /**
242     * Inverse of the CIE Lab f() nonlinearity
243     *
244     * @param  float $t
245     * @param  float $kappa
246     * @return float
247     */
248    private static function labFInv(float $t, float $kappa): float
249    {
250        $t3 = $t ** 3;
251        return ($t3 > (216 / 24389)) ? $t3 : (((116 * $t) - 16) / $kappa);
252    }
253
254    /**
255     * Convert to array
256     *
257     * @param  bool $assoc
258     * @return array
259     */
260    public function toArray(bool $assoc = true): array
261    {
262        $lab = [];
263
264        if ($assoc) {
265            $lab['l'] = $this->l;
266            $lab['a'] = $this->a;
267            $lab['b'] = $this->b;
268            if ($this->alpha !== null) {
269                $lab['alpha'] = $this->alpha;
270            }
271        } else {
272            $lab[] = $this->l;
273            $lab[] = $this->a;
274            $lab[] = $this->b;
275            if ($this->alpha !== null) {
276                $lab[] = $this->alpha;
277            }
278        }
279
280        return $lab;
281    }
282
283    /**
284     * Convert to readable string
285     *
286     * @param  ?string $format
287     * @return string
288     */
289    public function render(?string $format = null): string
290    {
291        return match ($format) {
292            self::COMMA => $this->l . ', ' . $this->a . ', ' . $this->b .
293                (($this->alpha !== null) ? ', ' . $this->alpha : ''),
294            self::CSS => 'lab(' . $this->l . '% ' . $this->a . ' ' . $this->b .
295                (($this->alpha !== null) ? ' / ' . $this->alpha : '') . ')',
296            self::PERCENT => $this->toRgb()->render(self::PERCENT),
297            default => $this->l . ' ' . $this->a . ' ' . $this->b .
298                (($this->alpha !== null) ? ' ' . $this->alpha : ''),
299        };
300    }
301
302    /**
303     * Return CSS-formatted string
304     *
305     * @return string
306     */
307    public function __toString(): string
308    {
309        return $this->render(self::CSS);
310    }
311
312    /**
313     * Get the valid channel names
314     *
315     * @return array
316     */
317    protected function channels(): array
318    {
319        return ['l', 'a', 'b', 'alpha'];
320    }
321
322}