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

codeigniter4 / CodeIgniter4 / 18390059502

09 Oct 2025 09:49PM UTC coverage: 84.39% (+0.03%) from 84.362%
18390059502

Pull #9751

github

web-flow
Merge 22157a7b0 into d945236b2
Pull Request #9751: refactor(app): Standardize subdomain detection logic

16 of 16 new or added lines in 3 files covered. (100.0%)

43 existing lines in 6 files now uncovered.

21236 of 25164 relevant lines covered (84.39%)

195.78 hits per line

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

88.5
/system/Images/Handlers/ImageMagickHandler.php
1
<?php
2

3
declare(strict_types=1);
4

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

14
namespace CodeIgniter\Images\Handlers;
15

16
use CodeIgniter\Images\Exceptions\ImageException;
17
use Config\Images;
18
use Imagick;
19
use ImagickDraw;
20
use ImagickDrawException;
21
use ImagickException;
22
use ImagickPixel;
23
use ImagickPixelException;
24

25
/**
26
 * Image handler for Imagick extension.
27
 */
28
class ImageMagickHandler extends BaseHandler
29
{
30
    /**
31
     * Stores Imagick instance.
32
     *
33
     * @var Imagick|null
34
     */
35
    protected $resource;
36

37
    /**
38
     * Constructor.
39
     *
40
     * @param Images $config
41
     *
42
     * @throws ImageException
43
     */
44
    public function __construct($config = null)
45
    {
46
        parent::__construct($config);
40✔
47

48
        if (! extension_loaded('imagick')) {
40✔
49
            throw ImageException::forMissingExtension('IMAGICK');  // @codeCoverageIgnore
×
50
        }
51
    }
52

53
    /**
54
     * Loads the image for manipulation.
55
     *
56
     * @return void
57
     *
58
     * @throws ImageException
59
     */
60
    protected function ensureResource()
61
    {
62
        if (! $this->resource instanceof Imagick) {
33✔
63
            // Verify that we have a valid image
64
            $this->image();
33✔
65

66
            try {
67
                $this->resource = new Imagick();
32✔
68
                $this->resource->readImage($this->image()->getPathname());
32✔
69

70
                // Check for valid image
71
                if ($this->resource->getImageWidth() === 0 || $this->resource->getImageHeight() === 0) {
32✔
72
                    throw ImageException::forInvalidImageCreate($this->image()->getPathname());
×
73
                }
74

75
                $this->supportedFormatCheck();
32✔
76
            } catch (ImagickException $e) {
×
77
                throw ImageException::forInvalidImageCreate($e->getMessage());
×
78
            }
79
        }
80
    }
81

82
    /**
83
     * Handles all the grunt work of resizing, etc.
84
     *
85
     * @param string $action  Type of action to perform
86
     * @param int    $quality Quality setting for Imagick operations
87
     *
88
     * @return $this
89
     *
90
     * @throws ImageException
91
     */
92
    protected function process(string $action, int $quality = 100)
93
    {
94
        $this->image();
12✔
95

96
        $this->ensureResource();
12✔
97

98
        try {
99
            switch ($action) {
100
                case 'resize':
12✔
101
                    $this->resource->resizeImage(
6✔
102
                        $this->width,
6✔
103
                        $this->height,
6✔
104
                        Imagick::FILTER_LANCZOS,
6✔
105
                        0,
6✔
106
                    );
6✔
107
                    break;
6✔
108

109
                case 'crop':
10✔
110
                    $width  = $this->width;
10✔
111
                    $height = $this->height;
10✔
112
                    $xAxis  = $this->xAxis ?? 0;
10✔
113
                    $yAxis  = $this->yAxis ?? 0;
10✔
114

115
                    $this->resource->cropImage(
10✔
116
                        $width,
10✔
117
                        $height,
10✔
118
                        $xAxis,
10✔
119
                        $yAxis,
10✔
120
                    );
10✔
121

122
                    // Reset canvas to cropped size
123
                    $this->resource->setImagePage(0, 0, 0, 0);
10✔
124
                    break;
10✔
125
            }
126

127
            // Handle transparency for supported image types
128
            if (in_array($this->image()->imageType, $this->supportTransparency, true)
12✔
129
                && $this->resource->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED) {
12✔
130
                $this->resource->setImageAlphaChannel(Imagick::ALPHACHANNEL_OPAQUE);
12✔
131
            }
132
        } catch (ImagickException) {
×
133
            throw ImageException::forImageProcessFailed();
×
134
        }
135

136
        return $this;
12✔
137
    }
138

139
    /**
140
     * Handles the actual resizing of the image.
141
     *
142
     * @return ImageMagickHandler
143
     *
144
     * @throws ImagickException
145
     */
146
    public function _resize(bool $maintainRatio = false)
147
    {
148
        if ($maintainRatio) {
9✔
149
            // If maintaining a ratio, we need a custom approach
150
            $this->ensureResource();
3✔
151

152
            // Use thumbnailImage which preserves an aspect ratio
153
            $this->resource->thumbnailImage($this->width, $this->height, true);
3✔
154

155
            return $this;
3✔
156
        }
157

158
        // Use the common process() method for normal resizing
159
        return $this->process('resize');
6✔
160
    }
161

162
    /**
163
     * Crops the image.
164
     *
165
     * @return $this
166
     *
167
     * @throws ImagickException
168
     */
169
    public function _crop()
170
    {
171
        // Use the common process() method for cropping
172
        $result = $this->process('crop');
10✔
173

174
        // Handle a case where crop dimensions exceed the original image size
175
        if ($this->resource instanceof Imagick) {
10✔
176
            $imgWidth  = $this->resource->getImageWidth();
10✔
177
            $imgHeight = $this->resource->getImageHeight();
10✔
178

179
            if ($this->xAxis >= $imgWidth || $this->yAxis >= $imgHeight) {
10✔
180
                // Create transparent background
181
                $background = new Imagick();
2✔
182
                $background->newImage($this->width, $this->height, new ImagickPixel('transparent'));
2✔
183
                $background->setImageFormat($this->resource->getImageFormat());
2✔
184

185
                // Composite our image on the background
186
                $background->compositeImage($this->resource, Imagick::COMPOSITE_OVER, 0, 0);
2✔
187

188
                // Replace our resource
189
                $this->resource = $background;
2✔
190
            }
191
        }
192

193
        return $result;
10✔
194
    }
195

196
    /**
197
     * Handles the rotation of an image resource.
198
     * Doesn't save the image, but replaces the current resource.
199
     *
200
     * @return $this
201
     *
202
     * @throws ImagickException
203
     */
204
    protected function _rotate(int $angle)
205
    {
206
        $this->ensureResource();
3✔
207

208
        // Create transparent background
209
        $this->resource->setImageBackgroundColor(new ImagickPixel('transparent'));
3✔
210
        $this->resource->rotateImage(new ImagickPixel('transparent'), $angle);
3✔
211

212
        // Reset canvas dimensions
213
        $this->resource->setImagePage($this->resource->getImageWidth(), $this->resource->getImageHeight(), 0, 0);
3✔
214

215
        return $this;
3✔
216
    }
217

218
    /**
219
     * Flattens transparencies, default white background
220
     *
221
     * @return $this
222
     *
223
     * @throws ImagickException|ImagickPixelException
224
     */
225
    protected function _flatten(int $red = 255, int $green = 255, int $blue = 255)
226
    {
227
        $this->ensureResource();
1✔
228

229
        // Create background
230
        $bg = new ImagickPixel("rgb({$red},{$green},{$blue})");
1✔
231

232
        // Create a new canvas with the background color
233
        $canvas = new Imagick();
1✔
234
        $canvas->newImage(
1✔
235
            $this->resource->getImageWidth(),
1✔
236
            $this->resource->getImageHeight(),
1✔
237
            $bg,
1✔
238
            $this->resource->getImageFormat(),
1✔
239
        );
1✔
240

241
        // Composite our image on the background
242
        $canvas->compositeImage(
1✔
243
            $this->resource,
1✔
244
            Imagick::COMPOSITE_OVER,
1✔
245
            0,
1✔
246
            0,
1✔
247
        );
1✔
248

249
        // Replace our resource with the flattened version
250
        $this->resource->clear();
1✔
251
        $this->resource = $canvas;
1✔
252

253
        return $this;
1✔
254
    }
255

256
    /**
257
     * Flips an image along its vertical or horizontal axis.
258
     *
259
     * @return $this
260
     *
261
     * @throws ImagickException
262
     */
263
    protected function _flip(string $direction)
264
    {
265
        $this->ensureResource();
5✔
266

267
        if ($direction === 'horizontal') {
5✔
268
            $this->resource->flopImage();
3✔
269
        } else {
270
            $this->resource->flipImage();
2✔
271
        }
272

273
        return $this;
5✔
274
    }
275

276
    /**
277
     * Get a driver version
278
     *
279
     * @return string
280
     */
281
    public function getVersion()
282
    {
283
        $version = Imagick::getVersion();
1✔
284

285
        if (preg_match('/ImageMagick\s+(\d+\.\d+\.\d+)/', $version['versionString'], $matches)) {
1✔
286
            return $matches[1];
1✔
287
        }
288

289
        return '';
×
290
    }
291

292
    /**
293
     * Check if a given image format is supported
294
     *
295
     * @return void
296
     *
297
     * @throws ImageException
298
     */
299
    protected function supportedFormatCheck()
300
    {
301
        if (! $this->resource instanceof Imagick) {
32✔
302
            return;
×
303
        }
304

305
        if ($this->image()->imageType === IMAGETYPE_WEBP && ! in_array('WEBP', Imagick::queryFormats(), true)) {
32✔
UNCOV
306
            throw ImageException::forInvalidImageCreate(lang('images.webpNotSupported'));
×
307
        }
308
    }
309

310
    /**
311
     * Saves any changes that have been made to the file. If no new filename is
312
     * provided, the existing image is overwritten; otherwise a copy of the
313
     * file is made at $target.
314
     *
315
     * Example:
316
     *    $image->resize(100, 200, true)
317
     *          ->save();
318
     *
319
     * @param non-empty-string|null $target
320
     *
321
     * @throws ImagickException
322
     */
323
    public function save(?string $target = null, int $quality = 90): bool
324
    {
325
        $original = $target;
8✔
326
        $target   = ($target === null || $target === '') ? $this->image()->getPathname() : $target;
8✔
327

328
        // If no new resource has been created, then we're
329
        // simply copy the existing one.
330
        if (! $this->resource instanceof Imagick && $quality === 100) {
8✔
331
            if ($original === null) {
1✔
332
                return true;
1✔
333
            }
334

UNCOV
335
            $name = basename($target);
×
UNCOV
336
            $path = pathinfo($target, PATHINFO_DIRNAME);
×
337

UNCOV
338
            return $this->image()->copy($path, $name);
×
339
        }
340

341
        $this->ensureResource();
7✔
342

343
        $this->resource->setImageCompressionQuality($quality);
7✔
344

345
        if ($target !== null) {
7✔
346
            $extension = pathinfo($target, PATHINFO_EXTENSION);
7✔
347
            $this->resource->setImageFormat($extension);
7✔
348
        }
349

350
        try {
351
            $result = $this->resource->writeImage($target);
7✔
352

353
            chmod($target, $this->filePermissions);
7✔
354

355
            $this->resource->clear();
7✔
356
            $this->resource = null;
7✔
357

358
            return $result;
7✔
UNCOV
359
        } catch (ImagickException) {
×
UNCOV
360
            throw ImageException::forSaveFailed();
×
361
        }
362
    }
363

364
    /**
365
     * Handler-specific method for overlaying text on an image.
366
     *
367
     * @throws ImagickDrawException|ImagickException|ImagickPixelException
368
     */
369
    protected function _text(string $text, array $options = [])
370
    {
371
        $this->ensureResource();
4✔
372

373
        $draw = new ImagickDraw();
4✔
374

375
        if (isset($options['fontPath'])) {
4✔
UNCOV
376
            $draw->setFont($options['fontPath']);
×
377
        }
378

379
        if (isset($options['fontSize'])) {
4✔
380
            $draw->setFontSize($options['fontSize']);
4✔
381
        }
382

383
        if (isset($options['color'])) {
4✔
384
            $color = $options['color'];
4✔
385

386
            // Shorthand hex, #f00
387
            if (strlen($color) === 3) {
4✔
UNCOV
388
                $color = implode('', array_map(str_repeat(...), str_split($color), [2, 2, 2]));
×
389
            }
390

391
            [$r, $g, $b] = sscanf("#{$color}", '#%02x%02x%02x');
4✔
392
            $opacity     = $options['opacity'] ?? 1.0;
4✔
393
            $draw->setFillColor(new ImagickPixel("rgba({$r},{$g},{$b},{$opacity})"));
4✔
394
        }
395

396
        // Calculate text positioning
397
        $imgWidth  = $this->resource->getImageWidth();
4✔
398
        $imgHeight = $this->resource->getImageHeight();
4✔
399
        $xAxis     = 0;
4✔
400
        $yAxis     = 0;
4✔
401

402
        // Default padding
403
        $padding = $options['padding'] ?? 0;
4✔
404

405
        if (isset($options['hAlign'])) {
4✔
406
            $hOffset = $options['hOffset'] ?? 0;
4✔
407

408
            switch ($options['hAlign']) {
4✔
409
                case 'left':
4✔
UNCOV
410
                    $xAxis = $hOffset + $padding;
×
UNCOV
411
                    $draw->setTextAlignment(Imagick::ALIGN_LEFT);
×
UNCOV
412
                    break;
×
413

414
                case 'center':
4✔
415
                    $xAxis = $imgWidth / 2 + $hOffset;
3✔
416
                    $draw->setTextAlignment(Imagick::ALIGN_CENTER);
3✔
417
                    break;
3✔
418

419
                case 'right':
1✔
420
                    $xAxis = $imgWidth - $hOffset - $padding;
1✔
421
                    $draw->setTextAlignment(Imagick::ALIGN_RIGHT);
1✔
422
                    break;
1✔
423
            }
424
        }
425

426
        if (isset($options['vAlign'])) {
4✔
427
            $vOffset = $options['vOffset'] ?? 0;
4✔
428

429
            switch ($options['vAlign']) {
4✔
430
                case 'top':
4✔
UNCOV
431
                    $yAxis = $vOffset + $padding + ($options['fontSize'] ?? 16);
×
UNCOV
432
                    break;
×
433

434
                case 'middle':
4✔
435
                    $yAxis = $imgHeight / 2 + $vOffset;
1✔
436
                    break;
1✔
437

438
                case 'bottom':
3✔
439
                    // Note: Vertical offset is inverted for bottom alignment as per original implementation
440
                    $yAxis = $vOffset < 0 ? $imgHeight + $vOffset - $padding : $imgHeight - $vOffset - $padding;
3✔
441
                    break;
3✔
442
            }
443
        }
444

445
        if (isset($options['withShadow'])) {
4✔
446
            $shadow = clone $draw;
4✔
447

448
            if (isset($options['shadowColor'])) {
4✔
449
                $shadowColor = $options['shadowColor'];
4✔
450

451
                // Shorthand hex, #f00
452
                if (strlen($shadowColor) === 3) {
4✔
UNCOV
453
                    $shadowColor = implode('', array_map(str_repeat(...), str_split($shadowColor), [2, 2, 2]));
×
454
                }
455

456
                [$sr, $sg, $sb] = sscanf("#{$shadowColor}", '#%02x%02x%02x');
4✔
457
                $shadow->setFillColor(new ImagickPixel("rgb({$sr},{$sg},{$sb})"));
4✔
458
            } else {
UNCOV
459
                $shadow->setFillColor(new ImagickPixel('rgba(0,0,0,0.5)'));
×
460
            }
461

462
            $offset = $options['shadowOffset'] ?? 3;
4✔
463

464
            $this->resource->annotateImage(
4✔
465
                $shadow,
4✔
466
                $xAxis + $offset,
4✔
467
                $yAxis + $offset,
4✔
468
                0,
4✔
469
                $text,
4✔
470
            );
4✔
471
        }
472

473
        // Draw the main text
474
        $this->resource->annotateImage(
4✔
475
            $draw,
4✔
476
            $xAxis,
4✔
477
            $yAxis,
4✔
478
            0,
4✔
479
            $text,
4✔
480
        );
4✔
481
    }
482

483
    /**
484
     * Return the width of an image.
485
     *
486
     * @return int
487
     *
488
     * @throws ImagickException
489
     */
490
    public function _getWidth()
491
    {
492
        $this->ensureResource();
23✔
493

494
        return $this->resource->getImageWidth();
23✔
495
    }
496

497
    /**
498
     * Return the height of an image.
499
     *
500
     * @return int
501
     *
502
     * @throws ImagickException
503
     */
504
    public function _getHeight()
505
    {
506
        $this->ensureResource();
22✔
507

508
        return $this->resource->getImageHeight();
22✔
509
    }
510

511
    /**
512
     * Reads the EXIF information from the image and modifies the orientation
513
     * so that displays correctly in the browser. This is especially an issue
514
     * with images taken by smartphones who always store the image up-right,
515
     * but set the orientation flag to display it correctly.
516
     *
517
     * @param bool $silent If true, will ignore exceptions when PHP doesn't support EXIF.
518
     *
519
     * @return $this
520
     */
521
    public function reorient(bool $silent = false)
522
    {
523
        $orientation = $this->getEXIF('Orientation', $silent);
2✔
524

525
        return match ($orientation) {
2✔
526
            2       => $this->flip('horizontal'),
2✔
527
            3       => $this->rotate(180),
2✔
528
            4       => $this->rotate(180)->flip('horizontal'),
2✔
529
            5       => $this->rotate(90)->flip('horizontal'),
2✔
530
            6       => $this->rotate(90),
2✔
531
            7       => $this->rotate(270)->flip('horizontal'),
2✔
532
            8       => $this->rotate(270),
2✔
533
            default => $this,
2✔
534
        };
2✔
535
    }
536

537
    /**
538
     * Clears metadata from the image.
539
     *
540
     * @return $this
541
     *
542
     * @throws ImagickException
543
     */
544
    public function clearMetadata(): static
545
    {
546
        $this->ensureResource();
3✔
547

548
        $this->resource->stripImage();
2✔
549

550
        return $this;
2✔
551
    }
552
}
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