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

codeigniter4 / CodeIgniter4 / 7293561159

21 Dec 2023 09:55PM UTC coverage: 85.237% (+0.004%) from 85.233%
7293561159

push

github

web-flow
Merge pull request #8355 from paulbalandan/replace

Add `replace` to composer.json

18597 of 21818 relevant lines covered (85.24%)

199.84 hits per line

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

82.56
/system/Images/Handlers/GDHandler.php
1
<?php
2

3
/**
4
 * This file is part of CodeIgniter 4 framework.
5
 *
6
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11

12
namespace CodeIgniter\Images\Handlers;
13

14
use CodeIgniter\Images\Exceptions\ImageException;
15
use Config\Images;
16

17
/**
18
 * Image handler for GD package
19
 */
20
class GDHandler extends BaseHandler
21
{
22
    /**
23
     * Constructor.
24
     *
25
     * @param Images|null $config
26
     *
27
     * @throws ImageException
28
     */
29
    public function __construct($config = null)
30
    {
31
        parent::__construct($config);
46✔
32

33
        if (! extension_loaded('gd')) {
46✔
34
            throw ImageException::forMissingExtension('GD'); // @codeCoverageIgnore
35
        }
36
    }
37

38
    /**
39
     * Handles the rotation of an image resource.
40
     * Doesn't save the image, but replaces the current resource.
41
     */
42
    protected function _rotate(int $angle): bool
43
    {
44
        // Create the image handle
45
        $srcImg = $this->createImage();
3✔
46

47
        // Set the background color
48
        // This won't work with transparent PNG files so we are
49
        // going to have to figure out how to determine the color
50
        // of the alpha channel in a future release.
51

52
        $white = imagecolorallocate($srcImg, 255, 255, 255);
3✔
53

54
        // Rotate it!
55
        $destImg = imagerotate($srcImg, $angle, $white);
3✔
56

57
        // Kill the file handles
58
        imagedestroy($srcImg);
3✔
59

60
        $this->resource = $destImg;
3✔
61

62
        return true;
3✔
63
    }
64

65
    /**
66
     * Flattens transparencies
67
     *
68
     * @return $this
69
     */
70
    protected function _flatten(int $red = 255, int $green = 255, int $blue = 255)
71
    {
72
        $srcImg = $this->createImage();
1✔
73

74
        if (function_exists('imagecreatetruecolor')) {
1✔
75
            $create = 'imagecreatetruecolor';
1✔
76
            $copy   = 'imagecopyresampled';
1✔
77
        } else {
78
            $create = 'imagecreate';
×
79
            $copy   = 'imagecopyresized';
×
80
        }
81
        $dest = $create($this->width, $this->height);
1✔
82

83
        $matte = imagecolorallocate($dest, $red, $green, $blue);
1✔
84

85
        imagefilledrectangle($dest, 0, 0, $this->width, $this->height, $matte);
1✔
86
        imagecopy($dest, $srcImg, 0, 0, 0, 0, $this->width, $this->height);
1✔
87

88
        // Kill the file handles
89
        imagedestroy($srcImg);
1✔
90

91
        $this->resource = $dest;
1✔
92

93
        return $this;
1✔
94
    }
95

96
    /**
97
     * Flips an image along it's vertical or horizontal axis.
98
     *
99
     * @return $this
100
     */
101
    protected function _flip(string $direction)
102
    {
103
        $srcImg = $this->createImage();
5✔
104

105
        $angle = $direction === 'horizontal' ? IMG_FLIP_HORIZONTAL : IMG_FLIP_VERTICAL;
5✔
106

107
        imageflip($srcImg, $angle);
5✔
108

109
        $this->resource = $srcImg;
5✔
110

111
        return $this;
5✔
112
    }
113

114
    /**
115
     * Get GD version
116
     *
117
     * @return mixed
118
     */
119
    public function getVersion()
120
    {
121
        if (function_exists('gd_info')) {
1✔
122
            $gdVersion = @gd_info();
1✔
123

124
            return preg_replace('/\D/', '', $gdVersion['GD Version']);
1✔
125
        }
126

127
        return false;
×
128
    }
129

130
    /**
131
     * Resizes the image.
132
     *
133
     * @return GDHandler
134
     */
135
    public function _resize(bool $maintainRatio = false)
136
    {
137
        return $this->process('resize');
7✔
138
    }
139

140
    /**
141
     * Crops the image.
142
     *
143
     * @return GDHandler
144
     */
145
    public function _crop()
146
    {
147
        return $this->process('crop');
10✔
148
    }
149

150
    /**
151
     * Handles all of the grunt work of resizing, etc.
152
     *
153
     * @return $this
154
     */
155
    protected function process(string $action)
156
    {
157
        $origWidth  = $this->image()->origWidth;
14✔
158
        $origHeight = $this->image()->origHeight;
14✔
159

160
        if ($action === 'crop') {
14✔
161
            // Reassign the source width/height if cropping
162
            $origWidth  = $this->width;
10✔
163
            $origHeight = $this->height;
10✔
164

165
            // Modify the "original" width/height to the new
166
            // values so that methods that come after have the
167
            // correct size to work with.
168
            $this->image()->origHeight = $this->height;
10✔
169
            $this->image()->origWidth  = $this->width;
10✔
170
        }
171

172
        // Create the image handle
173
        $src = $this->createImage();
14✔
174

175
        if (function_exists('imagecreatetruecolor')) {
14✔
176
            $create = 'imagecreatetruecolor';
14✔
177
            $copy   = 'imagecopyresampled';
14✔
178
        } else {
179
            $create = 'imagecreate';
×
180
            $copy   = 'imagecopyresized';
×
181
        }
182

183
        $dest = $create($this->width, $this->height);
14✔
184

185
        // for png and webp we can actually preserve transparency
186
        if (in_array($this->image()->imageType, $this->supportTransparency, true)) {
14✔
187
            imagealphablending($dest, false);
14✔
188
            imagesavealpha($dest, true);
14✔
189
        }
190

191
        $copy($dest, $src, 0, 0, (int) $this->xAxis, (int) $this->yAxis, $this->width, $this->height, $origWidth, $origHeight);
14✔
192

193
        imagedestroy($src);
14✔
194
        $this->resource = $dest;
14✔
195

196
        return $this;
14✔
197
    }
198

199
    /**
200
     * Saves any changes that have been made to file. If no new filename is
201
     * provided, the existing image is overwritten, otherwise a copy of the
202
     * file is made at $target.
203
     *
204
     * Example:
205
     *    $image->resize(100, 200, true)
206
     *          ->save();
207
     *
208
     * @param non-empty-string|null $target
209
     */
210
    public function save(?string $target = null, int $quality = 90): bool
211
    {
212
        $original = $target;
6✔
213
        $target   = ($target === null || $target === '') ? $this->image()->getPathname() : $target;
6✔
214

215
        // If no new resource has been created, then we're
216
        // simply copy the existing one.
217
        if (empty($this->resource) && $quality === 100) {
6✔
218
            if ($original === null) {
1✔
219
                return true;
1✔
220
            }
221

222
            $name = basename($target);
×
223
            $path = pathinfo($target, PATHINFO_DIRNAME);
×
224

225
            return $this->image()->copy($path, $name);
×
226
        }
227

228
        $this->ensureResource();
5✔
229

230
        // for png and webp we can actually preserve transparency
231
        if (in_array($this->image()->imageType, $this->supportTransparency, true)) {
5✔
232
            imagepalettetotruecolor($this->resource);
5✔
233
            imagealphablending($this->resource, false);
5✔
234
            imagesavealpha($this->resource, true);
5✔
235
        }
236

237
        switch ($this->image()->imageType) {
5✔
238
            case IMAGETYPE_GIF:
5✔
239
                if (! function_exists('imagegif')) {
3✔
240
                    throw ImageException::forInvalidImageCreate(lang('Images.gifNotSupported'));
×
241
                }
242

243
                if (! @imagegif($this->resource, $target)) {
3✔
244
                    throw ImageException::forSaveFailed();
×
245
                }
246
                break;
3✔
247

248
            case IMAGETYPE_JPEG:
5✔
249
                if (! function_exists('imagejpeg')) {
3✔
250
                    throw ImageException::forInvalidImageCreate(lang('Images.jpgNotSupported'));
×
251
                }
252

253
                if (! @imagejpeg($this->resource, $target, $quality)) {
3✔
254
                    throw ImageException::forSaveFailed();
×
255
                }
256
                break;
3✔
257

258
            case IMAGETYPE_PNG:
5✔
259
                if (! function_exists('imagepng')) {
4✔
260
                    throw ImageException::forInvalidImageCreate(lang('Images.pngNotSupported'));
×
261
                }
262

263
                if (! @imagepng($this->resource, $target)) {
4✔
264
                    throw ImageException::forSaveFailed();
×
265
                }
266
                break;
4✔
267

268
            case IMAGETYPE_WEBP:
4✔
269
                if (! function_exists('imagewebp')) {
4✔
270
                    throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported'));
×
271
                }
272

273
                if (! @imagewebp($this->resource, $target, $quality)) {
4✔
274
                    throw ImageException::forSaveFailed();
×
275
                }
276
                break;
4✔
277

278
            default:
279
                throw ImageException::forInvalidImageCreate();
×
280
        }
281

282
        imagedestroy($this->resource);
5✔
283

284
        chmod($target, $this->filePermissions);
5✔
285

286
        return true;
5✔
287
    }
288

289
    /**
290
     * Create Image Resource
291
     *
292
     * This simply creates an image resource handle
293
     * based on the type of image being processed
294
     *
295
     * @return bool|resource
296
     */
297
    protected function createImage(string $path = '', string $imageType = '')
298
    {
299
        if ($this->resource !== null) {
24✔
300
            return $this->resource;
7✔
301
        }
302

303
        if ($path === '') {
24✔
304
            $path = $this->image()->getPathname();
24✔
305
        }
306

307
        if ($imageType === '') {
24✔
308
            $imageType = $this->image()->imageType;
24✔
309
        }
310

311
        return $this->getImageResource($path, $imageType);
24✔
312
    }
313

314
    /**
315
     * Make the image resource object if needed
316
     */
317
    protected function ensureResource()
318
    {
319
        if ($this->resource === null) {
7✔
320
            // if valid image type, make corresponding image resource
321
            $this->resource = $this->getImageResource(
7✔
322
                $this->image()->getPathname(),
7✔
323
                $this->image()->imageType
7✔
324
            );
7✔
325
        }
326
    }
327

328
    /**
329
     * Check if image type is supported and return image resource
330
     *
331
     * @param string $path      Image path
332
     * @param int    $imageType Image type
333
     *
334
     * @return bool|resource
335
     *
336
     * @throws ImageException
337
     */
338
    protected function getImageResource(string $path, int $imageType)
339
    {
340
        switch ($imageType) {
341
            case IMAGETYPE_GIF:
29✔
342
                if (! function_exists('imagecreatefromgif')) {
4✔
343
                    throw ImageException::forInvalidImageCreate(lang('Images.gifNotSupported'));
×
344
                }
345

346
                return imagecreatefromgif($path);
4✔
347

348
            case IMAGETYPE_JPEG:
29✔
349
                if (! function_exists('imagecreatefromjpeg')) {
7✔
350
                    throw ImageException::forInvalidImageCreate(lang('Images.jpgNotSupported'));
×
351
                }
352

353
                return imagecreatefromjpeg($path);
7✔
354

355
            case IMAGETYPE_PNG:
26✔
356
                if (! function_exists('imagecreatefrompng')) {
26✔
357
                    throw ImageException::forInvalidImageCreate(lang('Images.pngNotSupported'));
×
358
                }
359

360
                return @imagecreatefrompng($path);
26✔
361

362
            case IMAGETYPE_WEBP:
4✔
363
                if (! function_exists('imagecreatefromwebp')) {
4✔
364
                    throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported'));
×
365
                }
366

367
                return imagecreatefromwebp($path);
4✔
368

369
            default:
370
                throw ImageException::forInvalidImageCreate('Ima');
×
371
        }
372
    }
373

374
    /**
375
     * Add text overlay to an image.
376
     */
377
    protected function _text(string $text, array $options = [])
378
    {
379
        // Reverse the vertical offset
380
        // When the image is positioned at the bottom
381
        // we don't want the vertical offset to push it
382
        // further down. We want the reverse, so we'll
383
        // invert the offset. Note: The horizontal
384
        // offset flips itself automatically
385

386
        if ($options['vAlign'] === 'bottom') {
3✔
387
            $options['vOffset'] *= -1;
2✔
388
        }
389

390
        if ($options['hAlign'] === 'right') {
3✔
391
            $options['hOffset'] *= -1;
1✔
392
        }
393

394
        // Set font width and height
395
        // These are calculated differently depending on
396
        // whether we are using the true type font or not
397
        if (! empty($options['fontPath'])) {
3✔
398
            if (function_exists('imagettfbbox')) {
×
399
                $temp = imagettfbbox($options['fontSize'], 0, $options['fontPath'], $text);
×
400
                $temp = $temp[2] - $temp[0];
×
401

402
                $fontwidth = $temp / strlen($text);
×
403
            } else {
404
                $fontwidth = $options['fontSize'] - ($options['fontSize'] / 4);
×
405
            }
406

407
            $fontheight = $options['fontSize'];
×
408
        } else {
409
            $fontwidth  = imagefontwidth($options['fontSize']);
3✔
410
            $fontheight = imagefontheight($options['fontSize']);
3✔
411
        }
412

413
        $options['fontheight'] = $fontheight;
3✔
414
        $options['fontwidth']  = $fontwidth;
3✔
415

416
        // Set base X and Y axis values
417
        $xAxis = $options['hOffset'] + $options['padding'];
3✔
418
        $yAxis = $options['vOffset'] + $options['padding'];
3✔
419

420
        // Set vertical alignment
421
        if ($options['vAlign'] === 'middle') {
3✔
422
            // Don't apply padding when you're in the middle of the image.
423
            $yAxis += ($this->image()->origHeight / 2) + ($fontheight / 2) - $options['padding'] - $fontheight - $options['shadowOffset'];
1✔
424
        } elseif ($options['vAlign'] === 'bottom') {
2✔
425
            $yAxis = ($this->image()->origHeight - $fontheight - $options['shadowOffset'] - ($fontheight / 2)) - $yAxis;
2✔
426
        }
427

428
        // Set horizontal alignment
429
        if ($options['hAlign'] === 'right') {
3✔
430
            $xAxis += ($this->image()->origWidth - ($fontwidth * strlen($text)) - $options['shadowOffset']) - (2 * $options['padding']);
1✔
431
        } elseif ($options['hAlign'] === 'center') {
2✔
432
            $xAxis += floor(($this->image()->origWidth - ($fontwidth * strlen($text))) / 2);
2✔
433
        }
434

435
        $options['xAxis'] = $xAxis;
3✔
436
        $options['yAxis'] = $yAxis;
3✔
437

438
        if ($options['withShadow']) {
3✔
439
            // Offset from text
440
            $options['xShadow'] = $xAxis + $options['shadowOffset'];
1✔
441
            $options['yShadow'] = $yAxis + $options['shadowOffset'];
1✔
442

443
            $this->textOverlay($text, $options, true);
1✔
444
        }
445

446
        $this->textOverlay($text, $options);
3✔
447
    }
448

449
    /**
450
     * Handler-specific method for overlaying text on an image.
451
     *
452
     * @param bool $isShadow Whether we are drawing the dropshadow or actual text
453
     */
454
    protected function textOverlay(string $text, array $options = [], bool $isShadow = false)
455
    {
456
        $src = $this->createImage();
3✔
457

458
        /* Set RGB values for shadow
459
         *
460
         * Get the rest of the string and split it into 2-length
461
         * hex values:
462
         */
463
        $opacity = (int) ($options['opacity'] * 127);
3✔
464

465
        // Allow opacity to be applied to the text
466
        imagealphablending($src, true);
3✔
467

468
        $color = $isShadow ? $options['shadowColor'] : $options['color'];
3✔
469

470
        // shorthand hex, #f00
471
        if (strlen($color) === 3) {
3✔
472
            $color = implode('', array_map('str_repeat', str_split($color), [2, 2, 2]));
×
473
        }
474

475
        $color = str_split(substr($color, 0, 6), 2);
3✔
476
        $color = imagecolorclosestalpha($src, hexdec($color[0]), hexdec($color[1]), hexdec($color[2]), $opacity);
3✔
477

478
        $xAxis = $isShadow ? $options['xShadow'] : $options['xAxis'];
3✔
479
        $yAxis = $isShadow ? $options['yShadow'] : $options['yAxis'];
3✔
480

481
        // Add the shadow to the source image
482
        if (! empty($options['fontPath'])) {
3✔
483
            // We have to add fontheight because imagettftext locates the bottom left corner, not top-left corner.
484
            imagettftext($src, $options['fontSize'], 0, (int) $xAxis, (int) ($yAxis + $options['fontheight']), $color, $options['fontPath'], $text);
×
485
        } else {
486
            imagestring($src, (int) $options['fontSize'], (int) $xAxis, (int) $yAxis, $text, $color);
3✔
487
        }
488

489
        $this->resource = $src;
3✔
490
    }
491

492
    /**
493
     * Return image width.
494
     *
495
     * @return int
496
     */
497
    public function _getWidth()
498
    {
499
        return imagesx($this->resource);
22✔
500
    }
501

502
    /**
503
     * Return image height.
504
     *
505
     * @return int
506
     */
507
    public function _getHeight()
508
    {
509
        return imagesy($this->resource);
21✔
510
    }
511
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc