• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JBZoo / Utils / 29770052588

20 Jul 2026 06:55PM UTC coverage: 92.619% (-0.1%) from 92.722%
29770052588

push

github

web-flow
Merge pull request #57 from JBZoo/release/8.0

8.0.0 — PHP 8.3+ floor & lock-step major

15 of 16 new or added lines in 7 files covered. (93.75%)

106 existing lines in 13 files now uncovered.

1669 of 1802 relevant lines covered (92.62%)

41.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.4
/src/Image.php
1
<?php
2

3
/**
4
 * JBZoo Toolbox - Utils.
5
 *
6
 * This file is part of the JBZoo Toolbox project.
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license    MIT
11
 * @copyright  Copyright (C) JBZoo.com, All rights reserved.
12
 * @see        https://github.com/JBZoo/Utils
13
 */
14

15
declare(strict_types=1);
16

17
namespace JBZoo\Utils;
18

19
/**
20
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
21
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
22
 * @psalm-suppress UnusedClass
23
 */
24
final class Image
25
{
26
    public const TOP_LEFT     = 'tl';
27
    public const LEFT         = 'l';
28
    public const BOTTOM_LEFT  = 'bl';
29
    public const TOP          = 't';
30
    public const CENTER       = 'c';
31
    public const BOTTOM       = 'b';
32
    public const TOP_RIGHT    = 'tr';
33
    public const RIGHT        = 'r';
34
    public const BOTTOM_RIGHT = 'bt';
35

36
    private const BASE_PARTS     = 3;
37
    private const EXTENDS_PARTS  = 4;
38
    private const DETAILED_PARTS = 6;
39

40
    /**
41
     * Check if GD library is enabled on server.
42
     */
43
    public static function checkGD(bool $throwException = true): bool
44
    {
45
        $isGd = \extension_loaded('gd');
6✔
46

47
        // Require GD library
48
        if ($throwException && !$isGd) {
6✔
UNCOV
49
            throw new Exception('Required extension GD is not loaded.');
×
50
        }
51

52
        return $isGd;
6✔
53
    }
54

55
    /**
56
     * Checks if image has JPEG/JPG format.
57
     */
58
    public static function isJpeg(?string $format = null): bool
59
    {
60
        if (isStrEmpty($format)) {
12✔
UNCOV
61
            return false;
×
62
        }
63

64
        $format = \strtolower((string)$format);
12✔
65

66
        return $format === 'image/jpg' || $format === 'jpg' || $format === 'image/jpeg' || $format === 'jpeg';
12✔
67
    }
68

69
    /**
70
     * Checks if image has GIF format.
71
     */
72
    public static function isGif(?string $format = null): bool
73
    {
74
        if (isStrEmpty($format)) {
12✔
UNCOV
75
            return false;
×
76
        }
77

78
        $format = \strtolower((string)$format);
12✔
79

80
        return $format === 'image/gif' || $format === 'gif';
12✔
81
    }
82

83
    /**
84
     * Checks if image has PNG format.
85
     */
86
    public static function isPng(?string $format = null): bool
87
    {
88
        if (isStrEmpty($format)) {
12✔
UNCOV
89
            return false;
×
90
        }
91

92
        $format = \strtolower((string)$format);
12✔
93

94
        return $format === 'image/png' || $format === 'png';
12✔
95
    }
96

97
    /**
98
     * Checks if image has WEBP format.
99
     */
100
    public static function isWebp(?string $format = null): bool
101
    {
102
        if (isStrEmpty($format)) {
6✔
UNCOV
103
            return false;
×
104
        }
105

106
        $format = \strtolower((string)$format);
6✔
107

108
        return $format === 'image/webp' || $format === 'webp';
6✔
109
    }
110

111
    /**
112
     * Converts a hex color value to its RGB equivalent.
113
     * @param  array|string $origColor Hex color string, array(red, green, blue) or array(red, green, blue, alpha).
114
     *                                 Where red, green, blue - integers 0-255, alpha - integer 0-127
115
     * @return int[]
116
     * @SuppressWarnings(PHPMD.DevelopmentCodeFragment)
117
     */
118
    public static function normalizeColor(array|string $origColor): array
119
    {
120
        $result = [];
6✔
121

122
        if (\is_string($origColor)) {
6✔
123
            $result = self::normalizeColorString($origColor);
6✔
124
        } elseif (\count($origColor) === self::BASE_PARTS || \count($origColor) === self::EXTENDS_PARTS) {
6✔
125
            $result = self::normalizeColorArray($origColor);
6✔
126
        }
127

128
        if (\count($result) !== self::EXTENDS_PARTS) {
6✔
UNCOV
129
            throw new Exception('Undefined color format (string): ' . \print_r($origColor, true));
×
130
        }
131

132
        return $result;
6✔
133
    }
134

135
    /**
136
     * Same as PHP's imagecopymerge() function, except preserves alpha-transparency in 24-bit PNGs.
137
     * @see http://www.php.net/manual/en/function.imagecopymerge.php#88456
138
     * @param \GdImage $dstImg   Dist image resource
139
     * @param \GdImage $srcImg   Source image resource
140
     * @param array    $dist     Left and Top offset of dist
141
     * @param array    $src      Left and Top offset of source
142
     * @param array    $srcSizes Width and Height  of source
143
     */
144
    public static function imageCopyMergeAlpha(
145
        \GdImage $dstImg,
146
        \GdImage $srcImg,
147
        array $dist,
148
        array $src,
149
        array $srcSizes,
150
        int $opacity,
151
    ): void {
152
        [$dstX, $dstY]          = $dist;
6✔
153
        [$srcX, $srcY]          = $src;
6✔
154
        [$srcWidth, $srcHeight] = $srcSizes;
6✔
155

156
        // Get image width and height and percentage
157
        $opacity /= 100;
6✔
158
        $width  = \imagesx($srcImg);
6✔
159
        $height = \imagesy($srcImg);
6✔
160

161
        // Turn alpha blending off
162
        self::addAlpha($srcImg, false);
6✔
163

164
        // Find the most opaque pixel in the image (the one with the smallest alpha value)
165
        $minBaseAlpha = 127;
6✔
166
        $minAlpha     = $minBaseAlpha;
6✔
167

168
        for ($x = 0; $x < $width; $x++) {
6✔
169
            for ($y = 0; $y < $height; $y++) {
6✔
170
                $alpha = (\imagecolorat($srcImg, $x, $y) >> 24) & 0xFF;
6✔
171
                if ($alpha < $minAlpha) {
6✔
172
                    $minAlpha = $alpha;
6✔
173
                }
174
            }
175
        }
176

177
        // Loop through image pixels and modify alpha for each
178
        for ($x = 0; $x < $width; $x++) {
6✔
179
            for ($y = 0; $y < $height; $y++) {
6✔
180
                // Get current alpha value (represents the TRANSPARENCY!)
181
                $colorXY = \imagecolorat($srcImg, $x, $y);
6✔
182
                $alpha   = ($colorXY >> 24) & 0xFF;
6✔
183

184
                // Calculate new alpha
185
                if ($minAlpha !== $minBaseAlpha) {
6✔
186
                    $alpha = $minBaseAlpha + $minBaseAlpha * $opacity * ($alpha - $minBaseAlpha)
6✔
187
                        / ($minBaseAlpha - $minAlpha);
6✔
188
                } else {
UNCOV
189
                    $alpha += $minBaseAlpha * $opacity;
×
190
                }
191

192
                // Get the color index with new alpha
193
                $alphaColorXY = \imagecolorallocatealpha(
6✔
194
                    $srcImg,
6✔
195
                    ($colorXY >> 16) & 0xFF,
6✔
196
                    ($colorXY >> 8) & 0xFF,
6✔
197
                    $colorXY & 0xFF,
6✔
198
                    (int)$alpha, // @phpstan-ignore-line
6✔
199
                );
6✔
200

201
                // Set pixel with the new color + opacity (imagesetpixel() always returns true on a GdImage)
202
                \imagesetpixel($srcImg, $x, $y, (int)$alphaColorXY);
6✔
203
            }
204
        }
205

206
        // Copy it
207
        self::addAlpha($srcImg);
6✔
208
        self::addAlpha($dstImg);
6✔
209
        \imagecopy($dstImg, $srcImg, $dstX, $dstY, $srcX, $srcY, $srcWidth, $srcHeight);
6✔
210
    }
211

212
    /**
213
     * Check opacity value.
214
     */
215
    public static function opacity(float $opacity): int
216
    {
217
        if ($opacity <= 1) {
12✔
218
            $opacity *= 100;
12✔
219
        }
220

221
        $opacity = Filter::int($opacity);
12✔
222

223
        return Vars::limit($opacity, 0, 100);
12✔
224
    }
225

226
    /**
227
     * Convert opacity value to alpha.
228
     */
229
    public static function opacity2Alpha(float $opacity): int
230
    {
231
        $opacity = self::opacity($opacity);
6✔
232
        $opacity /= 100;
6✔
233

234
        $alpha = 127 - (127 * $opacity);
6✔
235

236
        return self::alpha((int)$alpha);
6✔
237
    }
238

239
    /**
240
     * Returns valid value to change color segment of a image (0..255).
241
     */
242
    public static function color(float $color): int
243
    {
244
        return Vars::range($color, 0, 255);
6✔
245
    }
246

247
    /**
248
     * Returns valid value of alpha-channel.
249
     */
250
    public static function alpha(float $color): int
251
    {
252
        return Vars::range($color, 0, 127);
12✔
253
    }
254

255
    /**
256
     * Returns valid value of image rotation (-360..360).
257
     */
258
    public static function rotate(float $color): int
259
    {
260
        return Vars::range($color, -360, 360);
6✔
261
    }
262

263
    /**
264
     * Returns valid value to make image bright (-255..255).
265
     */
266
    public static function brightness(float $brightness): int
267
    {
268
        return Vars::range($brightness, -255, 255);
6✔
269
    }
270

271
    /**
272
     * Returns valid value to change contrast of a image (-100..100).
273
     */
274
    public static function contrast(float $contrast): int
275
    {
276
        return Vars::range($contrast, -100, 100);
6✔
277
    }
278

279
    /**
280
     * Returns valid value to change color segment of a image (-255..255).
281
     */
282
    public static function colorize(float $colorize): int
283
    {
284
        return Vars::range($colorize, -255, 255);
6✔
285
    }
286

287
    /**
288
     * Returns valid value to change smoothness of a image (1..10).
289
     */
290
    public static function smooth(float $smooth): int
291
    {
292
        return Vars::range($smooth, 1, 10);
6✔
293
    }
294

295
    /**
296
     * Returns valid value of image direction: 'x', 'y', 'xy', 'yx'.
297
     */
298
    public static function direction(string $direction): string
299
    {
300
        $direction = \strtolower(\trim($direction));
6✔
301

302
        if (\in_array($direction, ['x', 'y', 'xy', 'yx'], true)) {
6✔
303
            return $direction;
6✔
304
        }
305

306
        return 'x';
6✔
307
    }
308

309
    /**
310
     * Return valid value to blur image (1-10).
311
     */
312
    public static function blur(float $blur): int
313
    {
314
        return Vars::range($blur, 1, 10);
6✔
315
    }
316

317
    /**
318
     * Return valid value of percent (0-100).
319
     */
320
    public static function percent(float $percent): int
321
    {
322
        return Vars::range($percent, 0, 100);
6✔
323
    }
324

325
    /**
326
     * Returns valid value of image quality (0..100).
327
     */
328
    public static function quality(float $percent): int
329
    {
330
        return Vars::range($percent, 0, 100);
6✔
331
    }
332

333
    /**
334
     * Convert string to binary data.
335
     */
336
    public static function strToBin(string $imageString): ?string
337
    {
338
        $cleanedString = \str_replace(
6✔
339
            ' ',
6✔
340
            '+',
6✔
341
            (string)\preg_replace('#^data:image/[^;]+;base64,#', '', $imageString),
6✔
342
        );
6✔
343

344
        $result = \base64_decode($cleanedString, true);
6✔
345

346
        if (isStrEmpty($result)) {
6✔
347
            $result = $imageString;
6✔
348
        }
349

350
        return $result === false ? null : $result;
6✔
351
    }
352

353
    /**
354
     * Check is format supported by lib.
355
     */
356
    public static function isSupportedFormat(string $format): bool
357
    {
358
        if (!isStrEmpty($format)) {
6✔
359
            return self::isJpeg($format) || self::isPng($format) || self::isGif($format) || self::isWebp($format);
6✔
360
        }
361

362
        return false;
6✔
363
    }
364

365
    /**
366
     * Check position name.
367
     * @SuppressWarnings(PHPMD.NPathComplexity)
368
     */
369
    public static function position(string $position): string
370
    {
371
        $position = \strtolower(\trim($position));
18✔
372
        $position = \str_replace(['-', '_'], ' ', $position);
18✔
373

374
        if (\in_array($position, [self::TOP, 'top', 't'], true)) {
18✔
375
            return self::TOP;
12✔
376
        }
377

378
        if (\in_array($position, [self::TOP_RIGHT, 'top right', 'right top', 'tr', 'rt'], true)) {
18✔
379
            return self::TOP_RIGHT;
18✔
380
        }
381

382
        if (\in_array($position, [self::RIGHT, 'right', 'r'], true)) {
12✔
383
            return self::RIGHT;
12✔
384
        }
385

386
        if (\in_array($position, [self::BOTTOM_RIGHT, 'bottom right', 'right bottom', 'br', 'rb'], true)) {
12✔
387
            return self::BOTTOM_RIGHT;
12✔
388
        }
389

390
        if (\in_array($position, [self::BOTTOM, 'bottom', 'b'], true)) {
12✔
391
            return self::BOTTOM;
12✔
392
        }
393

394
        if (\in_array($position, [self::BOTTOM_LEFT, 'bottom left', 'left bottom', 'bl', 'lb'], true)) {
12✔
395
            return self::BOTTOM_LEFT;
12✔
396
        }
397

398
        if (\in_array($position, [self::LEFT, 'left', 'l'], true)) {
12✔
399
            return self::LEFT;
12✔
400
        }
401

402
        if (\in_array($position, [self::TOP_LEFT, 'top left', 'left top', 'tl', 'lt'], true)) {
12✔
403
            return self::TOP_LEFT;
12✔
404
        }
405

406
        return self::CENTER;
12✔
407
    }
408

409
    /**
410
     * Determine position. Returns array with X and Y coordinates.
411
     * @param string $position Position name or code
412
     * @param array  $canvas   Width and Height of canvas
413
     * @param array  $box      Width and Height of box that will be located on canvas
414
     * @param array  $offset   Forced offset X, Y
415
     * @SuppressWarnings(PHPMD.NPathComplexity)
416
     */
417
    public static function getInnerCoords(string $position, array $canvas, array $box, array $offset = [0, 0]): array
418
    {
419
        $positionCode        = self::position($position);
6✔
420
        [$canvasW, $canvasH] = $canvas;
6✔
421
        [$boxW, $boxH]       = $box;
6✔
422
        [$offsetX, $offsetY] = $offset;
6✔
423

424
        // Coords map:
425
        // 00  10  20  =>  tl  t   tr
426
        // 01  11  21  =>  l   c   r
427
        // 02  12  22  =>  bl  b   br
428

429
        // X coord
430
        $xCord0 = $offsetX + 0;                             //  bottom-left     left        top-left
6✔
431
        $xCord1 = $offsetX + ($canvasW / 2) - ($boxW / 2);  //  bottom          center      top
6✔
432
        $xCord2 = $offsetX + $canvasW - $boxW;              //  bottom-right    right       top-right
6✔
433

434
        // Y coord
435
        $yCord0 = $offsetY + 0;                             //  top-left        top         top-right
6✔
436
        $yCord1 = $offsetY + ($canvasH / 2) - ($boxH / 2);  //  left            center      right
6✔
437
        $yCord2 = $offsetY + $canvasH - $boxH;              //  bottom-left     bottom      bottom-right
6✔
438

439
        if ($positionCode === self::TOP_LEFT) {
6✔
440
            return [$xCord0, $yCord0];
6✔
441
        }
442

443
        if ($positionCode === self::LEFT) {
6✔
444
            return [$xCord0, $yCord1];
6✔
445
        }
446

447
        if ($positionCode === self::BOTTOM_LEFT) {
6✔
448
            return [$xCord0, $yCord2];
6✔
449
        }
450

451
        if ($positionCode === self::TOP) {
6✔
452
            return [$xCord1, $yCord0];
6✔
453
        }
454

455
        if ($positionCode === self::BOTTOM) {
6✔
456
            return [$xCord1, $yCord2];
6✔
457
        }
458

459
        if ($positionCode === self::TOP_RIGHT) {
6✔
460
            return [$xCord2, $yCord0];
6✔
461
        }
462

463
        if ($positionCode === self::RIGHT) {
6✔
464
            return [$xCord2, $yCord1];
6✔
465
        }
466

467
        if ($positionCode === self::BOTTOM_RIGHT) {
6✔
468
            return [$xCord2, $yCord2];
6✔
469
        }
470

471
        return [$xCord1, $yCord1];
6✔
472
    }
473

474
    /**
475
     * Add alpha chanel to image resource.
476
     * @param \GdImage $image   Image GD resource
477
     * @param bool     $isBlend Add alpha blending
478
     */
479
    public static function addAlpha(\GdImage $image, bool $isBlend = true): void
480
    {
481
        \imagesavealpha($image, true);
12✔
482
        \imagealphablending($image, $isBlend);
12✔
483
    }
484

485
    /**
486
     * Normalize color from string.
487
     */
488
    private static function normalizeColorString(string $origColor): array
489
    {
490
        $color = \trim($origColor, '#');
6✔
491
        $color = \trim($color);
6✔
492

493
        if (\strlen($color) === self::DETAILED_PARTS) {
6✔
494
            [$red, $green, $blue] = [$color[0] . $color[1], $color[2] . $color[3], $color[4] . $color[5]];
6✔
495
        } elseif (\strlen($color) === self::BASE_PARTS) {
6✔
496
            [$red, $green, $blue] = [$color[0] . $color[0], $color[1] . $color[1], $color[2] . $color[2]];
6✔
497
        } else {
UNCOV
498
            throw new Exception('Undefined color format (string): ' . $origColor);
×
499
        }
500

501
        $red   = \hexdec($red);
6✔
502
        $green = \hexdec($green);
6✔
503
        $blue  = \hexdec($blue);
6✔
504

505
        return [$red, $green, $blue, 0];
6✔
506
    }
507

508
    /**
509
     * Normalize color from array.
510
     * @return int[]
511
     */
512
    private static function normalizeColorArray(array $origColor): array
513
    {
514
        $result = [];
6✔
515

516
        if (
517
            \array_key_exists('r', $origColor)
6✔
518
            && \array_key_exists('g', $origColor)
6✔
519
            && \array_key_exists('b', $origColor)
6✔
520
        ) {
521
            $result = [
6✔
522
                self::color((int)$origColor['r']),
6✔
523
                self::color((int)$origColor['g']),
6✔
524
                self::color((int)$origColor['b']),
6✔
525
                self::alpha((float)($origColor['a'] ?? 0)),
6✔
526
            ];
6✔
527
        } elseif (
528
            \array_key_exists('0', $origColor)
6✔
529
            && \array_key_exists(1, $origColor)
6✔
530
            && \array_key_exists(2, $origColor)
6✔
531
        ) {
532
            $result = [
6✔
533
                self::color((int)$origColor[0]),
6✔
534
                self::color((int)$origColor[1]),
6✔
535
                self::color((int)$origColor[2]),
6✔
536
                self::alpha((float)($origColor[3] ?? 0)),
6✔
537
            ];
6✔
538
        }
539

540
        return $result;
6✔
541
    }
542
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc