FormatsConverter::hsv2rgb()   F
last analyzed

Complexity

Conditions 14
Paths 896

Size

Total Lines 44
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 35
c 1
b 0
f 0
dl 0
loc 44
rs 2.2444
cc 14
nc 896
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * File containing the class {@see FormatsConverter}.
4
 *
5
 * @see FormatsConverter
6
 *@subpackage RGBAColor
7
 * @package AppUtils
8
 */
9
10
declare(strict_types=1);
11
12
namespace AppUtils\RGBAColor;
13
14
use AppUtils\RGBAColor;
15
use AppUtils\RGBAColor\FormatsConverter\HEXParser;
16
use function AppUtils\parseVariable;
17
18
/**
19
 * The converter static class is used to convert between color
20
 * information formats.
21
 *
22
 * @package AppUtils
23
 * @subpackage RGBAColor
24
 * @author Sebastian Mordziol <[email protected]>
25
 */
26
class FormatsConverter
27
{
28
    public const ERROR_INVALID_COLOR_ARRAY = 99701;
29
30
    private static ?HEXParser $hexParser = null;
31
32
    /**
33
     * Converts the color to a HEX color value. This is either
34
     * a RRGGBB or RRGGBBAA string, depending on whether there
35
     * is an alpha channel value.
36
     *
37
     * NOTE: The HEX letters are always uppercase.
38
     *
39
     * @param RGBAColor $color
40
     * @return string
41
     */
42
    public static function color2HEX(RGBAColor $color) : string
43
    {
44
        $hex =
45
            UnitsConverter::int2hex($color->getRed()->get8Bit()) .
46
            UnitsConverter::int2hex($color->getGreen()->get8Bit()) .
47
            UnitsConverter::int2hex($color->getBlue()->get8Bit());
48
49
        if($color->hasTransparency())
50
        {
51
            $hex .= UnitsConverter::int2hex($color->getAlpha()->get8Bit());
52
        }
53
54
        return $hex;
55
    }
56
57
    /**
58
     * Converts the color to a CSS `rgb()` or `rgba()` value.
59
     *
60
     * @param RGBAColor $color
61
     * @return string
62
     */
63
    public static function color2CSS(RGBAColor $color) : string
64
    {
65
        if($color->hasTransparency())
66
        {
67
            return sprintf(
68
                'rgba(%s, %s, %s, %s)',
69
                $color->getRed()->get8Bit(),
70
                $color->getGreen()->get8Bit(),
71
                $color->getBlue()->get8Bit(),
72
                $color->getAlpha()->getAlpha()
73
            );
74
        }
75
76
        return sprintf(
77
            'rgb(%s, %s, %s)',
78
            $color->getRed()->get8Bit(),
79
            $color->getGreen()->get8Bit(),
80
            $color->getBlue()->get8Bit()
81
        );
82
    }
83
84
    public static function color2array(RGBAColor $color) : ArrayConverter
85
    {
86
        return new ArrayConverter($color);
87
    }
88
89
    /**
90
     * Checks if the array is a valid color array with
91
     * all expected color keys present. The `alpha` key
92
     * is optional. If it's not valid, throws an exception.
93
     *
94
     * @param array<string|int,int|float> $color
95
     * @throws ColorException
96
     * @see RGBAColor::ERROR_INVALID_COLOR_ARRAY
97
     */
98
    public static function requireValidColorArray(array $color) : void
99
    {
100
        if(self::isColorArray($color))
101
        {
102
            return;
103
        }
104
105
        throw new ColorException(
106
            'Not a valid color array.',
107
            sprintf(
108
                'The color array is in the wrong format, or is missing required keys. '.
109
                'Given: '.PHP_EOL.
110
                '%s',
111
                parseVariable($color)->toString()
112
            ),
113
            RGBAColor::ERROR_INVALID_COLOR_ARRAY
114
        );
115
    }
116
117
    /**
118
     * Checks whether the specified array contains all required
119
     * color keys.
120
     *
121
     * @param array<string|int,int|float> $color
122
     * @return bool
123
     */
124
    public static function isColorArray(array $color) : bool
125
    {
126
        $keys = array(
127
            RGBAColor::CHANNEL_RED,
128
            RGBAColor::CHANNEL_GREEN,
129
            RGBAColor::CHANNEL_BLUE
130
        );
131
132
        foreach($keys as $key)
133
        {
134
            if(!isset($color[$key]))
135
            {
136
                return false;
137
            }
138
        }
139
140
        return true;
141
    }
142
143
    /**
144
     * Human-readable label of the color. Automatically
145
     * switches between RGBA and RGB depending on whether
146
     * the color has any transparency.
147
     *
148
     * @param RGBAColor $color
149
     * @return string
150
     */
151
    public static function color2readable(RGBAColor $color) : string
152
    {
153
        if($color->hasTransparency())
154
        {
155
            return sprintf(
156
                'RGBA(%s %s %s %s)',
157
                $color->getRed()->get8Bit(),
158
                $color->getGreen()->get8Bit(),
159
                $color->getBlue()->get8Bit(),
160
                $color->getAlpha()->get8Bit()
161
            );
162
        }
163
164
        return sprintf(
165
            'RGB(%s %s %s)',
166
            $color->getRed()->get8Bit(),
167
            $color->getGreen()->get8Bit(),
168
            $color->getBlue()->get8Bit()
169
        );
170
    }
171
172
    /**
173
     * Parses a HEX color value, and converts it to
174
     * an RGBA color array.
175
     *
176
     * Examples:
177
     *
178
     * <pre>
179
     * $color = RGBAColor_Utilities::parseHexColor('CCC');
180
     * $color = RGBAColor_Utilities::parseHexColor('CCDDEE');
181
     * $color = RGBAColor_Utilities::parseHexColor('CCDDEEFA');
182
     * </pre>
183
     *
184
     * @param string $hex
185
     * @param string $name
186
     * @return RGBAColor
187
     *
188
     * @throws ColorException
189
     * @see RGBAColor::ERROR_INVALID_HEX_LENGTH
190
     */
191
    public static function hex2color(string $hex, string $name='') : RGBAColor
192
    {
193
        if(!isset(self::$hexParser))
194
        {
195
            self::$hexParser = new HEXParser();
196
        }
197
198
        return self::$hexParser->parse($hex, $name);
0 ignored issues
show
Bug introduced by
The method parse() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

198
        return self::$hexParser->/** @scrutinizer ignore-call */ parse($hex, $name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
199
    }
200
201
    /**
202
     * @var array<int,array{key:string,mandatory:bool}>
203
     */
204
    private static array $keys = array(
205
        array(
206
            'key' => RGBAColor::CHANNEL_RED,
207
            'mandatory' => true
208
        ),
209
        array(
210
            'key' => RGBAColor::CHANNEL_GREEN,
211
            'mandatory' => true
212
        ),
213
        array(
214
            'key' => RGBAColor::CHANNEL_BLUE,
215
            'mandatory' => true
216
        ),
217
        array(
218
            'key' => RGBAColor::CHANNEL_ALPHA,
219
            'mandatory' => false
220
        )
221
    );
222
223
    /**
224
     * Converts a color array to an associative color
225
     * array. Works with indexed color arrays, as well
226
     * as arrays that are already associative.
227
     *
228
     * Expects the color values to always be in the same order:
229
     *
230
     * - red
231
     * - green
232
     * - blue
233
     * - alpha (optional)
234
     *
235
     * @param array<int|string,int|float> $color
236
     * @return array<string,int|float>
237
     *
238
     * @throws ColorException
239
     * @see FormatsConverter::ERROR_INVALID_COLOR_ARRAY
240
     */
241
    public static function array2associative(array $color) : array
242
    {
243
        // If one associative key is present, we assume
244
        // that the color array is already correct.
245
        if(isset($color[RGBAColor::CHANNEL_RED]))
246
        {
247
            return $color;
248
        }
249
250
        $values = array_values($color);
251
        $result = array();
252
253
        foreach(self::$keys as $idx => $def)
254
        {
255
            if(isset($values[$idx]))
256
            {
257
                $result[$def['key']] = $values[$idx];
258
                continue;
259
            }
260
261
            if(!$def['mandatory'])
262
            {
263
                continue;
264
            }
265
266
            throw new ColorException(
267
                'Invalid color array',
268
                sprintf(
269
                    'The value for [%s] is missing at index [%s] in the source array, and it is mandatory.',
270
                    $def['key'],
271
                    $idx
272
                ),
273
                self::ERROR_INVALID_COLOR_ARRAY
274
            );
275
        }
276
277
        return $result;
278
    }
279
280
    /**
281
     * Converts an RGB color value to HSV.
282
     *
283
     * @param int $red 0-255
284
     * @param int $green 0-255
285
     * @param int $blue 0-255
286
     * @return array{hue:float,saturation:float,brightness:float} HSV values: 0-360, 0-100, 0-100
287
     */
288
    public static function rgb2hsv(int $red, int $green, int $blue) : array
289
    {
290
        // Convert the RGB byte-values to percentages
291
        $R = ($red / 255);
292
        $G = ($green / 255);
293
        $B = ($blue / 255);
294
295
        // Calculate a few basic values, the maximum value of R,G,B, the
296
        //   minimum value, and the difference of the two (chroma).
297
        $maxRGB = max($R, $G, $B);
298
        $minRGB = min($R, $G, $B);
299
        $chroma = $maxRGB - $minRGB;
300
301
        // Value (also called Brightness) is the easiest component to calculate,
302
        //   and is simply the highest value among the R,G,B components.
303
        // We multiply by 100 to turn the decimal into a readable percent value.
304
        $computedV = 100 * $maxRGB;
305
306
        // Special case if hueless (equal parts RGB make black, white, or grays)
307
        // Note that Hue is technically undefined when chroma is zero, as
308
        //   attempting to calculate it would cause division by zero (see
309
        //   below), so most applications simply substitute a Hue of zero.
310
        // Saturation will always be zero in this case, see below for details.
311
        if ($chroma === 0)
312
        {
313
            return array(
314
                'hue' => 0.0,
315
                'saturation' => 0.0,
316
                'brightness' => $computedV
317
            );
318
        }
319
320
        // Saturation is also simple to compute, and is simply the chroma
321
        //   over the Value (or Brightness)
322
        // Again, multiplied by 100 to get a percentage.
323
        $computedS = 100 * ($chroma / $maxRGB);
324
325
        // Calculate Hue component
326
        // Hue is calculated on the "chromacity plane", which is represented
327
        //   as a 2D hexagon, divided into six 60-degree sectors. We calculate
328
        //   the bisecting angle as a value 0 <= x < 6, that represents which
329
        //   portion of which sector the line falls on.
330
        if ($R === $minRGB)
331
        {
332
            $h = 3 - (($G - $B) / $chroma);
333
        }
334
        elseif ($B === $minRGB)
335
        {
336
            $h = 1 - (($R - $G) / $chroma);
337
        }
338
        else
339
        { // $G == $minRGB
340
            $h = 5 - (($B - $R) / $chroma);
341
        }
342
343
        // After we have the sector position, we multiply it by the size of
344
        //   each sector's arc (60 degrees) to obtain the angle in degrees.
345
        $computedH = 60 * $h;
346
347
        return array(
348
            'hue' => $computedH,
349
            'saturation' => $computedS,
350
            'brightness' => $computedV
351
        );
352
    }
353
354
    /**
355
     * Converts an HSV value to RGB.
356
     *
357
     * @param float $hue 0-360
358
     * @param float $saturation 0-100
359
     * @param float $brightness 0-100
360
     * @return array{red:int,green:int,blue:int} 0-255
361
     * @link https://gist.github.com/vkbo/2323023
362
     */
363
    public static function hsv2rgb(float $hue, float $saturation, float $brightness) : array
364
    {
365
366
        if($hue < 0) {  $hue = 0.0; } // Hue:
367
        if($hue > 360) { $hue = 360.0; } // 0.0 to 360.0
368
        if($saturation < 0) { $saturation = 0.0; } // Saturation:
369
        if($saturation > 100) { $saturation = 100.0; } // 0.0 to 100.0
370
        if($brightness < 0) { $brightness = 0.0; }  // Brightness:
371
        if($brightness > 100) { $brightness = 100.0; } // 0.0 to 100.0
372
373
        $dS = $saturation/100.0; // Saturation: 0.0 to 1.0
374
        $dV = $brightness/100.0; // Brightness: 0.0 to 1.0
375
        $dC = $dV*$dS; // Chroma: 0.0 to 1.0
376
        $dH = $hue/60.0; // H-Prime: 0.0 to 6.0
377
        $dT = $dH; // Temp variable
378
379
        while($dT >= 2.0) { $dT -= 2.0; } // php modulus does not work with float
380
        $dX = $dC*(1-abs($dT-1)); // as used in the Wikipedia link
381
382
        switch(floor($dH)) {
383
            case 0:
384
                $dR = $dC; $dG = $dX; $dB = 0.0; break;
385
            case 1:
386
                $dR = $dX; $dG = $dC; $dB = 0.0; break;
387
            case 2:
388
                $dR = 0.0; $dG = $dC; $dB = $dX; break;
389
            case 3:
390
                $dR = 0.0; $dG = $dX; $dB = $dC; break;
391
            case 4:
392
                $dR = $dX; $dG = 0.0; $dB = $dC; break;
393
            case 5:
394
                $dR = $dC; $dG = 0.0; $dB = $dX; break;
395
            default:
396
                $dR = 0.0; $dG = 0.0; $dB = 0.0; break;
397
        }
398
399
        $dM  = $dV - $dC;
400
        $dR += $dM; $dG += $dM; $dB += $dM;
401
        $dR *= 255; $dG *= 255; $dB *= 255;
402
403
        return array(
404
            'red' => (int)round($dR),
405
            'green' => (int)round($dG),
406
            'blue' => (int)round($dB)
407
        );
408
    }
409
}
410