Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
101 / 101
100.00% covered (success)
100.00%
20 / 20
CRAP
100.00% covered (success)
100.00%
1 / 1
Hex
100.00% covered (success)
100.00%
101 / 101
100.00% covered (success)
100.00%
20 / 20
43
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHex
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
7
 setR
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setG
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setB
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setA
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 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
 getHex
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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
 isValid
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 toRgb
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 toHsl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 render
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 __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 Hex 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 Hex extends AbstractColor
30{
31
32    /**
33     * R value
34     * @var ?string
35     */
36    protected ?string $r = null;
37
38    /**
39     * G value
40     * @var ?string
41     */
42    protected ?string $g = null;
43
44    /**
45     * B value
46     * @var ?string
47     */
48    protected ?string $b = null;
49
50    /**
51     * Hex value
52     * @var ?string
53     */
54    protected ?string $hex = null;
55
56    /**
57     * Alpha value
58     * @var ?float
59     */
60    protected ?float $a = null;
61
62    /**
63     * Constructor
64     *
65     * Instantiate the CSS hex color object
66     *
67     * @param string $hex
68     */
69    public function __construct(string $hex)
70    {
71        $this->setHex($hex);
72    }
73
74    /**
75     * Set hex value
76     *
77     * @param  string $hex
78     * @throws OutOfRangeException
79     * @return self
80     */
81    public function setHex(string $hex): self
82    {
83        $hex = strtolower($hex);
84        $hex = (str_starts_with($hex, '#')) ? substr($hex, 1) : $hex;
85
86        if (!in_array(strlen($hex), [3, 4, 6, 8])) {
87            throw new OutOfRangeException('Error: The hex string was not the correct length.');
88        }
89        if (!$this->isValid($hex)) {
90            throw new OutOfRangeException('Error: The hex string was out of range.');
91        }
92
93        if (strlen($hex) == 3) {
94            $this->setR(substr($hex, 0, 1));
95            $this->setG(substr($hex, 1, 1));
96            $this->setB(substr($hex, 2, 1));
97            $this->a = null;
98        } else if (strlen($hex) == 4) {
99            $this->setR(substr($hex, 0, 1));
100            $this->setG(substr($hex, 1, 1));
101            $this->setB(substr($hex, 2, 1));
102            $this->a = round(hexdec(str_repeat(substr($hex, 3, 1), 2)) / 255, 4);
103        } else if (strlen($hex) == 6) {
104            $this->setR(substr($hex, 0, 2));
105            $this->setG(substr($hex, 2, 2));
106            $this->setB(substr($hex, 4, 2));
107            $this->a = null;
108        } else {
109            $this->setR(substr($hex, 0, 2));
110            $this->setG(substr($hex, 2, 2));
111            $this->setB(substr($hex, 4, 2));
112            $this->a = round(hexdec(substr($hex, 6, 2)) / 255, 4);
113        }
114
115        $this->hex = $hex;
116
117        return $this;
118    }
119
120    /**
121     * Set R value
122     *
123     * @param  string $r
124     * @throws OutOfRangeException
125     * @return self
126     */
127    public function setR(string $r): self
128    {
129        if (!$this->isValid($r)) {
130            throw new OutOfRangeException('Error: The $r hex string was out of range.');
131        }
132        $this->r = $r;
133        return $this;
134    }
135
136    /**
137     * Set G value
138     *
139     * @param  string $g
140     * @throws OutOfRangeException
141     * @return self
142     */
143    public function setG(string $g): self
144    {
145        if (!$this->isValid($g)) {
146            throw new OutOfRangeException('Error: The $g hex string was out of range.');
147        }
148        $this->g = $g;
149        return $this;
150    }
151
152    /**
153     * Set B value
154     *
155     * @param  string $b
156     * @throws OutOfRangeException
157     * @return self
158     */
159    public function setB(string $b): self
160    {
161        if (!$this->isValid($b)) {
162            throw new OutOfRangeException('Error: The $b hex string was out of range.');
163        }
164        $this->b = $b;
165        return $this;
166    }
167
168    /**
169     * Set A value
170     *
171     * @param  float|string $a
172     * @throws OutOfRangeException
173     * @return self
174     */
175    public function setA(float|string $a): self
176    {
177        $a = (float)$a;
178        if (($a > 1) || ($a < 0)) {
179            throw new OutOfRangeException('Error: The value of $a must be between 0 and 1.');
180        }
181
182        $alphaHex = str_pad(dechex((int)round($a * 255)), 2, '0', STR_PAD_LEFT);
183        $base     = (strlen($this->hex) <= 4) ? substr($this->hex, 0, 3) : substr($this->hex, 0, 6);
184
185        if (strlen($base) == 3) {
186            $base = $base[0] . $base[0] . $base[1] . $base[1] . $base[2] . $base[2];
187        }
188
189        $this->setR(substr($base, 0, 2));
190        $this->setG(substr($base, 2, 2));
191        $this->setB(substr($base, 4, 2));
192
193        $this->hex = $base . $alphaHex;
194        $this->a = round(hexdec($alphaHex) / 255, 4);
195
196        return $this;
197    }
198
199    /**
200     * Get A value
201     *
202     * @return float|null
203     */
204    public function getA(): float|null
205    {
206        return $this->a;
207    }
208
209    /**
210     * Determine if the color object has an alpha value
211     *
212     * @return bool
213     */
214    public function hasA(): bool
215    {
216        return ($this->a !== null);
217    }
218
219    /**
220     * Determine if the color object has an alpha value (alias)
221     *
222     * @return bool
223     */
224    public function hasAlpha(): bool
225    {
226        return ($this->a !== null);
227    }
228
229    /**
230     * Get hex value
231     *
232     * @return string
233     */
234    public function getHex(): string
235    {
236        return $this->hex;
237    }
238
239    /**
240     * Get R value
241     *
242     * @return string|null
243     */
244    public function getR(): string|null
245    {
246        return $this->r;
247    }
248
249    /**
250     * Get G value
251     *
252     * @return string|null
253     */
254    public function getG(): string|null
255    {
256        return $this->g;
257    }
258
259    /**
260     * Get B value
261     *
262     * @return string|null
263     */
264    public function getB(): string|null
265    {
266        return $this->b;
267    }
268
269    /**
270     * Method to determine if the hex value is valid
271     *
272     * @param  string $hex
273     * @return bool
274     */
275    public function isValid(string $hex): bool
276    {
277        $valid     = true;
278        $hexValues = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
279        $hexAry    = str_split($hex);
280
281        foreach ($hexAry as $h) {
282            if (!in_array($h, $hexValues)) {
283                $valid = false;
284                break;
285            }
286        }
287
288        return $valid;
289    }
290
291    /**
292     * Convert to RGB
293     *
294     * @return Rgb
295     */
296    public function toRgb(): Rgb
297    {
298        $hexR  = $this->r;
299        $hexG  = $this->g;
300        $hexB  = $this->b;
301
302        if (strlen($hexR) == 1) {
303            $hexR .= $hexR;
304        }
305        if (strlen($hexG) == 1) {
306            $hexG .= $hexG;
307        }
308        if (strlen($hexB) == 1) {
309            $hexB .= $hexB;
310        }
311
312        $r = base_convert($hexR, 16, 10);
313        $g = base_convert($hexG, 16, 10);
314        $b = base_convert($hexB, 16, 10);
315
316        return new Rgb($r, $g, $b, $this->a);
317    }
318
319    /**
320     * Convert to HSL
321     *
322     * @return Hsl
323     */
324    public function toHsl(): Hsl
325    {
326        return $this->toRgb()->toHsl();
327    }
328
329    /**
330     * Convert to array
331     *
332     * @param  bool $assoc
333     * @return array
334     */
335    public function toArray(bool $assoc = true): array
336    {
337        $hex = [];
338
339        if ($assoc) {
340            $hex['hex'] = '#' . $this->hex;
341            $hex['r']   = $this->r;
342            $hex['g']   = $this->g;
343            $hex['b']   = $this->b;
344            if ($this->a !== null) {
345                $hex['a'] = $this->a;
346            }
347        } else {
348            $hex[] = '#' . $this->hex;
349            $hex[] = $this->r;
350            $hex[] = $this->g;
351            $hex[] = $this->b;
352            if ($this->a !== null) {
353                $hex[] = $this->a;
354            }
355        }
356
357        return $hex;
358    }
359
360    /**
361     * Convert to readable string
362     *
363     * @param  ?string $format
364     * @return string
365     */
366    public function render(?string $format = null): string
367    {
368        return match ($format) {
369            self::COMMA, self::PERCENT => $this->toRgb()->render($format),
370            default => '#' . $this->hex,
371        };
372    }
373
374    /**
375     * Return CSS-formatted string
376     *
377     * @return string
378     */
379    public function __toString(): string
380    {
381        return $this->render(self::CSS);
382    }
383
384    /**
385     * Get the valid channel names
386     *
387     * @return array
388     */
389    protected function channels(): array
390    {
391        return ['r', 'g', 'b', 'hex', 'a'];
392    }
393
394}