Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.10% covered (warning)
67.10%
104 / 155
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageUtils
67.10% covered (warning)
67.10%
104 / 155
50.00% covered (danger)
50.00%
3 / 6
342.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 decodeBase64Image
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 lightness
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 lightnessFromRgb
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 resize
64.10% covered (warning)
64.10%
25 / 39
0.00% covered (danger)
0.00%
0 / 1
41.40
 difference
60.00% covered (warning)
60.00%
54 / 90
0.00% covered (danger)
0.00%
0 / 1
195.46
 getAverageColor
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
8.06
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Utils
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Utils;
16
17/**
18 * Image utils class.
19 *
20 * This class provides static helper functionalities for images.
21 *
22 * @package phpOMS\Utils
23 * @license OMS License 2.0
24 * @link    https://jingga.app
25 * @since   1.0.0
26 */
27final class ImageUtils
28{
29    /**
30     * Constructor.
31     *
32     * @since 1.0.0
33     * @codeCoverageIgnore
34     */
35    private function __construct()
36    {
37    }
38
39    /**
40     * Decode base64 image.
41     *
42     * @param string $img Encoded image
43     *
44     * @return string Decoded image
45     *
46     * @since 1.0.0
47     */
48    public static function decodeBase64Image(string $img) : string
49    {
50        $img = \str_replace('data:image/png;base64,', '', $img);
51        $img = \str_replace(' ', '+', $img);
52
53        return (string) \base64_decode($img);
54    }
55
56    /**
57     * Calculate the lightness from an RGB value as integer
58     *
59     * @param int $rgb RGB value represented as integer
60     *
61     * @return float
62     *
63     * @since 1.0.0
64     */
65    public static function lightness(int $rgb) : float
66    {
67        $sR = ($rgb >> 16) & 0xFF;
68        $sG = ($rgb >> 8) & 0xFF;
69        $sB = $rgb & 0xFF;
70
71        return self::lightnessFromRgb($sR, $sG, $sB);
72    }
73
74    /**
75     * Calculate lightess from rgb values
76     *
77     * @param int $r Red
78     * @param int $g Green
79     * @param int $b Blue
80     *
81     * @return float
82     *
83     * @since 1.0.0
84     */
85    public static function lightnessFromRgb(int $r, int $g, int $b) : float
86    {
87        $vR = $r / 255.0;
88        $vG = $g / 255.0;
89        $vB = $b / 255.0;
90
91        $lR = $vR <= 0.04045 ? $vR / 12.92 : \pow((($vR + 0.055) / 1.055), 2.4);
92        $lG = $vG <= 0.04045 ? $vG / 12.92 : \pow((($vG + 0.055) / 1.055), 2.4);
93        $lB = $vB <= 0.04045 ? $vB / 12.92 : \pow((($vB + 0.055) / 1.055), 2.4);
94
95        $y     = 0.2126 * $lR + 0.7152 * $lG + 0.0722 * $lB;
96        $lStar = $y <= 216.0 / 24389.0 ? $y * 24389.0 / 27.0 : \pow($y, (1 / 3)) * 116.0 - 16.0;
97
98        return $lStar / 100.0;
99    }
100
101    /**
102     * Resize image file
103     *
104     * @param string $srcPath Source path
105     * @param string $dstPath Destination path
106     * @param int    $width   New width
107     * @param int    $height  New image width
108     * @param bool   $crop    Crop image
109     *
110     * @return void
111     *
112     * @throws \InvalidArgumentException
113     *
114     * @since 1.0.0
115     */
116    public static function resize(string $srcPath, string $dstPath, int $width, int $height, bool $crop = false) : void
117    {
118        if (!\is_file($srcPath)) {
119            return;
120        }
121
122        /** @var array $imageDim */
123        $imageDim = \getimagesize($srcPath);
124
125        if ($imageDim[0] === 0 || $imageDim[1] === 0) {
126            return;
127        }
128
129        $ratio = $imageDim[0] / $imageDim[1];
130        if ($crop) {
131            if ($imageDim[0] > $imageDim[1]) {
132                $imageDim[0] = (int) \ceil($imageDim[0] - ($imageDim[0] * \abs($ratio - $width / $height)));
133            } else {
134                $imageDim[1] = (int) \ceil($imageDim[1] - ($imageDim[1] * \abs($ratio - $width / $height)));
135            }
136        } elseif ($width / $height > $ratio) {
137            $width = (int) ($height * $ratio);
138        } else {
139            $height = (int) ($width / $ratio);
140        }
141
142        $src = null;
143        if (\stripos($srcPath, '.jpg') !== false || \stripos($srcPath, '.jpeg') !== false) {
144            $src = \imagecreatefromjpeg($srcPath);
145        } elseif (\stripos($srcPath, '.png') !== false) {
146            $src = \imagecreatefrompng($srcPath);
147        } elseif (\stripos($srcPath, '.gif') !== false) {
148            $src = \imagecreatefromgif($srcPath);
149        }
150
151        $dst = \imagecreatetruecolor($width, $height);
152
153        if ($src === null || $src === false || $dst === null || $dst === false) {
154            throw new \InvalidArgumentException();
155        }
156
157        if (\stripos($srcPath, '.png')) {
158            \imagealphablending($dst, false);
159            $transparent = \imagecolorallocatealpha($dst, 0, 0, 0, 127);
160
161            if ($transparent === false) {
162                throw new \InvalidArgumentException();
163            }
164
165            \imagefill($dst, 0, 0, $transparent);
166            \imagesavealpha($dst, true);
167        }
168
169        \imagecopyresampled($dst, $src, 0, 0, 0, 0, $width, $height, $imageDim[0], $imageDim[1]);
170
171        if (\stripos($srcPath, '.jpg') || \stripos($srcPath, '.jpeg')) {
172            \imagejpeg($dst, $dstPath);
173        } elseif (\stripos($srcPath, '.png')) {
174            \imagepng($dst, $dstPath);
175        } elseif (\stripos($srcPath, '.gif')) {
176            \imagegif($dst, $dstPath);
177        }
178
179        \imagedestroy($src);
180        \imagedestroy($dst);
181    }
182
183    /**
184     * Get difference between two images
185     *
186     * @param string $img1 Path to first image
187     * @param string $img2 Path to second image
188     * @param string $out  Output path for difference image (empty = no difference image is created)
189     * @param int    $diff Difference image type (0 = only show differences of img2, 1 = make differences red/green colored)
190     *
191     * @return int Amount of pixel differences
192     */
193    public static function difference(string $img1, string $img2, string $out = '', int $diff = 0) : int
194    {
195        $src1 = null;
196        if (\stripos($img1, '.jpg') !== false || \stripos($img1, '.jpeg') !== false) {
197            $src1 = \imagecreatefromjpeg($img1);
198        } elseif (\stripos($img1, '.png') !== false) {
199            $src1 = \imagecreatefrompng($img1);
200        } elseif (\stripos($img1, '.gif') !== false) {
201            $src1 = \imagecreatefromgif($img1);
202        }
203
204        $src2 = null;
205        if (\stripos($img2, '.jpg') !== false || \stripos($img2, '.jpeg') !== false) {
206            $src2 = \imagecreatefromjpeg($img2);
207        } elseif (\stripos($img2, '.png') !== false) {
208            $src2 = \imagecreatefrompng($img2);
209        } elseif (\stripos($img2, '.gif') !== false) {
210            $src2 = \imagecreatefromgif($img2);
211        }
212
213        if ($src1 === null || $src2 === null
214            || $src1 === false || $src2 === false
215        ) {
216            return 0;
217        }
218
219        $imageDim1 = [\imagesx($src1), \imagesy($src1)];
220        $imageDim2 = [\imagesx($src2), \imagesy($src2)];
221
222        $newDim = [\max($imageDim1[0], $imageDim2[0]), \max($imageDim1[1], $imageDim2[1])];
223
224        $diff = empty($out) ? -1 : $diff;
225        $dst  = false;
226
227        $red   = 0;
228        $green = 0;
229
230        if ($diff !== -1) {
231            $dst = $diff === 0
232                ? \imagecreatetruecolor($newDim[0], $newDim[1])
233                : \imagecrop($src2, ['x' => 0, 'y' => 0, 'width' => $imageDim2[0], 'height' => $imageDim2[1]]);
234
235            if ($dst === false) {
236                return 0;
237            }
238
239            $alpha = \imagecolorallocatealpha($dst, 255, 255, 255, 127);
240            if ($alpha === false) {
241                return 0;
242            }
243
244            if ($diff === 0) {
245                \imagefill($dst, 0, 0, $alpha);
246            }
247
248            $red   = \imagecolorallocate($dst, 255, 0, 0);
249            $green = \imagecolorallocate($dst, 0, 255, 0);
250
251            if ($red === false || $green === false) {
252                return 0;
253            }
254        }
255
256        $diffArea   = 5;
257        $difference = 0;
258
259        for ($i = 0; $i < $newDim[0]; ++$i) {
260            for ($j = 0; $j < $newDim[1]; ++$j) {
261                // Dimension difference
262                if ($i >= $imageDim1[0] || $j >= $imageDim1[1]) {
263                    if ($diff === 0) {
264                        /** @var \GdImage $dst */
265                        \imagesetpixel($dst, $i, $j, $green);
266                    } elseif ($diff === 1) {
267                        if ($i >= $imageDim2[0] || $j >= $imageDim2[1]) {
268                            /** @var \GdImage $dst */
269                            \imagesetpixel($dst, $i, $j, $green);
270                        } else {
271                            $color2 = \imagecolorat($src2, $i, $j);
272
273                            if ($color2 === false) {
274                                continue;
275                            }
276
277                            /** @var \GdImage $dst */
278                            \imagesetpixel($dst, $i, $j, $color2);
279                        }
280                    }
281
282                    ++$difference;
283                    continue;
284                }
285
286                // Dimension difference
287                if ($i >= $imageDim2[0] || $j >= $imageDim2[1]) {
288                    if ($diff === 0) {
289                        /** @var \GdImage $dst */
290                        \imagesetpixel($dst, $i, $j, $red);
291                    } elseif ($diff === 1) {
292                        if ($i >= $imageDim1[0] || $j >= $imageDim1[1]) {
293                            /** @var \GdImage $dst */
294                            \imagesetpixel($dst, $i, $j, $red);
295                        } else {
296                            $color1 = \imagecolorat($src1, $i, $j);
297
298                            if ($color1 === false) {
299                                continue;
300                            }
301
302                            /** @var \GdImage $dst */
303                            \imagesetpixel($dst, $i, $j, $color1);
304                        }
305                    }
306
307                    ++$difference;
308                    continue;
309                }
310
311                // Get average color at current pixel position with a 10 pixel area
312                $color1Avg = self::getAverageColor($src1, $i, $j, $imageDim2[0], $imageDim2[1], $diffArea);
313                $color2Avg = self::getAverageColor($src2, $i, $j, $newDim[0], $newDim[1], $diffArea);
314
315                //$color1 = \imagecolorat($src1, $i, $j);
316                $color2 = \imagecolorat($src2, $i, $j);
317
318                if (\abs($color1Avg - $color2Avg) / $color1Avg > 0.05 && $color1Avg > 0 && $color2Avg > 0) {
319                    ++$difference;
320
321                    if ($diff === 0) {
322                        if ($color2 === false) {
323                            continue;
324                        }
325
326                        /** @var \GdImage $dst */
327                        \imagesetpixel($dst, $i, $j, $color2);
328                    } elseif ($diff === 1) {
329                        /** @var \GdImage $dst */
330                        \imagesetpixel($dst, $i, $j, $green);
331                    }
332                }
333            }
334        }
335
336        if ($dst !== false) {
337            if (\stripos($out, '.jpg') || \stripos($out, '.jpeg')) {
338                \imagejpeg($dst, $out);
339            } elseif (\stripos($out, '.png')) {
340                \imagesavealpha($dst, true);
341                \imagepng($dst, $out);
342            } elseif (\stripos($out, '.gif')) {
343                \imagegif($dst, $out);
344            }
345
346            \imagedestroy($src1);
347            \imagedestroy($src2);
348            \imagedestroy($dst);
349        }
350
351        return $difference;
352    }
353
354    /**
355     * Calculate the average color of an image at a specific position
356     *
357     * @param \GdImage $src    Image resource
358     * @param int      $x      X position
359     * @param int      $y      Y position
360     * @param int      $width  Image width
361     * @param int      $height Image height
362     * @param int      $area   Area to calculate average color
363     *
364     * @return int
365     *
366     * @since 1.0.0
367     */
368    private static function getAverageColor($src, $x, $y, $width, $height, $area = 10) : int
369    {
370        $colors = [];
371
372        for ($i = $x - $area; $i < $x + $area; ++$i) {
373            for ($j = $y - $area; $j < $y + $area; ++$j) {
374                if ($i < 0 || $j < 0 || $i >= $width || $j >= $height) {
375                    continue;
376                }
377
378                $color = \imagecolorat($src, $i, $j);
379
380                if ($color === false) {
381                    continue;
382                }
383
384                $colors[] = $color;
385            }
386        }
387
388        return (int) (\array_sum($colors) / \count($colors));
389    }
390}