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

codeigniter4 / CodeIgniter4 / 17697332145

13 Sep 2025 01:37PM UTC coverage: 84.31% (+0.005%) from 84.305%
17697332145

Pull #9719

github

web-flow
Merge aab617945 into d771d5819
Pull Request #9719: fix: Add missing translation

11 of 12 new or added lines in 5 files covered. (91.67%)

1 existing line in 1 file now uncovered.

20870 of 24754 relevant lines covered (84.31%)

194.33 hits per line

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

90.0
/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\I18n\Time;
17
use CodeIgniter\Images\Exceptions\ImageException;
18
use Config\Images;
19
use Exception;
20
use Imagick;
21

22
/**
23
 * Class ImageMagickHandler
24
 *
25
 * FIXME - This needs conversion & unit testing, to use the imagick extension
26
 */
27
class ImageMagickHandler extends BaseHandler
28
{
29
    /**
30
     * Stores image resource in memory.
31
     *
32
     * @var string|null
33
     */
34
    protected $resource;
35

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

45
        if (! extension_loaded('imagick') && ! class_exists(Imagick::class)) {
40✔
46
            throw ImageException::forMissingExtension('IMAGICK'); // @codeCoverageIgnore
×
47
        }
48

49
        $cmd = $this->config->libraryPath;
40✔
50

51
        if ($cmd === '') {
40✔
52
            throw ImageException::forInvalidImageLibraryPath($cmd);
1✔
53
        }
54

55
        if (preg_match('/convert$/i', $cmd) !== 1) {
40✔
56
            $cmd = rtrim($cmd, '\/') . '/convert';
1✔
57

58
            $this->config->libraryPath = $cmd;
1✔
59
        }
60

61
        if (! is_file($cmd)) {
40✔
62
            throw ImageException::forInvalidImageLibraryPath($cmd);
2✔
63
        }
64
    }
65

66
    /**
67
     * Handles the actual resizing of the image.
68
     *
69
     * @return ImageMagickHandler
70
     *
71
     * @throws Exception
72
     */
73
    public function _resize(bool $maintainRatio = false)
74
    {
75
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
9✔
76
        $destination = $this->getResourcePath();
9✔
77

78
        $escape = '\\';
9✔
79

80
        if (PHP_OS_FAMILY === 'Windows') {
9✔
81
            $escape = '';
×
82
        }
83

84
        $action = $maintainRatio
9✔
85
            ? ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination)
3✔
86
            : ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . "{$escape}! " . escapeshellarg($source) . ' ' . escapeshellarg($destination);
6✔
87

88
        $this->process($action);
9✔
89

90
        return $this;
9✔
91
    }
92

93
    /**
94
     * Crops the image.
95
     *
96
     * @return bool|ImageMagickHandler
97
     *
98
     * @throws Exception
99
     */
100
    public function _crop()
101
    {
102
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
10✔
103
        $destination = $this->getResourcePath();
10✔
104

105
        $extent = ' ';
10✔
106
        if ($this->xAxis >= $this->width || $this->yAxis > $this->height) {
10✔
107
            $extent = ' -background transparent -extent ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' ';
2✔
108
        }
109

110
        $action = ' -crop ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . '+' . ($this->xAxis ?? 0) . '+' . ($this->yAxis ?? 0) . $extent . escapeshellarg($source) . ' ' . escapeshellarg($destination);
10✔
111

112
        $this->process($action);
10✔
113

114
        return $this;
10✔
115
    }
116

117
    /**
118
     * Handles the rotation of an image resource.
119
     * Doesn't save the image, but replaces the current resource.
120
     *
121
     * @return $this
122
     *
123
     * @throws Exception
124
     */
125
    protected function _rotate(int $angle)
126
    {
127
        $angle = '-rotate ' . $angle;
3✔
128

129
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
3✔
130
        $destination = $this->getResourcePath();
3✔
131

132
        $action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
3✔
133

134
        $this->process($action);
3✔
135

136
        return $this;
3✔
137
    }
138

139
    /**
140
     * Flattens transparencies, default white background
141
     *
142
     * @return $this
143
     *
144
     * @throws Exception
145
     */
146
    protected function _flatten(int $red = 255, int $green = 255, int $blue = 255)
147
    {
148
        $flatten = "-background 'rgb({$red},{$green},{$blue})' -flatten";
1✔
149

150
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
1✔
151
        $destination = $this->getResourcePath();
1✔
152

153
        $action = ' ' . $flatten . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
1✔
154

155
        $this->process($action);
1✔
156

157
        return $this;
1✔
158
    }
159

160
    /**
161
     * Flips an image along it's vertical or horizontal axis.
162
     *
163
     * @return $this
164
     *
165
     * @throws Exception
166
     */
167
    protected function _flip(string $direction)
168
    {
169
        $angle = $direction === 'horizontal' ? '-flop' : '-flip';
5✔
170

171
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
5✔
172
        $destination = $this->getResourcePath();
5✔
173

174
        $action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
5✔
175

176
        $this->process($action);
5✔
177

178
        return $this;
5✔
179
    }
180

181
    /**
182
     * Get driver version
183
     */
184
    public function getVersion(): string
185
    {
186
        $versionString = $this->process('-version')[0];
1✔
187
        preg_match('/ImageMagick\s(?P<version>[\S]+)/', $versionString, $matches);
1✔
188

189
        return $matches['version'];
1✔
190
    }
191

192
    /**
193
     * Handles all of the grunt work of resizing, etc.
194
     *
195
     * @return array Lines of output from shell command
196
     *
197
     * @throws Exception
198
     */
199
    protected function process(string $action, int $quality = 100): array
200
    {
201
        if ($action !== '-version') {
31✔
202
            $this->supportedFormatCheck();
30✔
203
        }
204

205
        $cmd = $this->config->libraryPath;
31✔
206
        $cmd .= $action === '-version' ? ' ' . $action : ' -quality ' . $quality . ' ' . $action;
31✔
207

208
        $retval = 1;
31✔
209
        $output = [];
31✔
210
        // exec() might be disabled
211
        if (function_usable('exec')) {
31✔
212
            @exec($cmd, $output, $retval);
31✔
213
        }
214

215
        // Did it work?
216
        if ($retval > 0) {
31✔
217
            throw ImageException::forImageProcessFailed();
×
218
        }
219

220
        return $output;
31✔
221
    }
222

223
    /**
224
     * Saves any changes that have been made to file. If no new filename is
225
     * provided, the existing image is overwritten, otherwise a copy of the
226
     * file is made at $target.
227
     *
228
     * Example:
229
     *    $image->resize(100, 200, true)
230
     *          ->save();
231
     *
232
     * @param non-empty-string|null $target
233
     */
234
    public function save(?string $target = null, int $quality = 90): bool
235
    {
236
        $original = $target;
7✔
237
        $target   = ($target === null || $target === '') ? $this->image()->getPathname() : $target;
7✔
238

239
        // If no new resource has been created, then we're
240
        // simply copy the existing one.
241
        if (empty($this->resource) && $quality === 100) {
7✔
242
            if ($original === null) {
1✔
243
                return true;
1✔
244
            }
245

246
            $name = basename($target);
×
247
            $path = pathinfo($target, PATHINFO_DIRNAME);
×
248

249
            return $this->image()->copy($path, $name);
×
250
        }
251

252
        $this->ensureResource();
6✔
253

254
        // Copy the file through ImageMagick so that it has
255
        // a chance to convert file format.
256
        $action = escapeshellarg($this->resource) . ' ' . escapeshellarg($target);
6✔
257

258
        $this->process($action, $quality);
6✔
259

260
        unlink($this->resource);
6✔
261

262
        return true;
6✔
263
    }
264

265
    /**
266
     * Get Image Resource
267
     *
268
     * This simply creates an image resource handle
269
     * based on the type of image being processed.
270
     * Since ImageMagick is used on the cli, we need to
271
     * ensure we have a temporary file on the server
272
     * that we can use.
273
     *
274
     * To ensure we can use all features, like transparency,
275
     * during the process, we'll use a PNG as the temp file type.
276
     *
277
     * @return string
278
     *
279
     * @throws Exception
280
     */
281
    protected function getResourcePath()
282
    {
283
        if ($this->resource !== null) {
30✔
284
            return $this->resource;
10✔
285
        }
286

287
        $this->resource = WRITEPATH . 'cache/' . Time::now()->getTimestamp() . '_' . bin2hex(random_bytes(10)) . '.png';
30✔
288

289
        $name = basename($this->resource);
30✔
290
        $path = pathinfo($this->resource, PATHINFO_DIRNAME);
30✔
291

292
        $this->image()->copy($path, $name);
30✔
293

294
        return $this->resource;
30✔
295
    }
296

297
    /**
298
     * Make the image resource object if needed
299
     *
300
     * @return void
301
     *
302
     * @throws Exception
303
     */
304
    protected function ensureResource()
305
    {
306
        $this->getResourcePath();
6✔
307

308
        $this->supportedFormatCheck();
6✔
309
    }
310

311
    /**
312
     * Check if given image format is supported
313
     *
314
     * @return void
315
     *
316
     * @throws ImageException
317
     */
318
    protected function supportedFormatCheck()
319
    {
320
        switch ($this->image()->imageType) {
30✔
321
            case IMAGETYPE_WEBP:
30✔
322
                if (! in_array('WEBP', Imagick::queryFormats(), true)) {
4✔
NEW
323
                    throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported'));
×
324
                }
325
                break;
4✔
326
        }
327
    }
328

329
    /**
330
     * Handler-specific method for overlaying text on an image.
331
     *
332
     * @throws Exception
333
     */
334
    protected function _text(string $text, array $options = [])
335
    {
336
        $xAxis   = 0;
4✔
337
        $yAxis   = 0;
4✔
338
        $gravity = '';
4✔
339
        $cmd     = '';
4✔
340

341
        // Reverse the vertical offset
342
        // When the image is positioned at the bottom
343
        // we don't want the vertical offset to push it
344
        // further down. We want the reverse, so we'll
345
        // invert the offset. Note: The horizontal
346
        // offset flips itself automatically
347
        if ($options['vAlign'] === 'bottom') {
4✔
348
            $options['vOffset'] *= -1;
3✔
349
        }
350

351
        if ($options['hAlign'] === 'right') {
4✔
352
            $options['hOffset'] *= -1;
1✔
353
        }
354

355
        // Font
356
        if (! empty($options['fontPath'])) {
4✔
357
            $cmd .= ' -font ' . escapeshellarg($options['fontPath']);
×
358
        }
359

360
        if (isset($options['hAlign'], $options['vAlign'])) {
4✔
361
            switch ($options['hAlign']) {
4✔
362
                case 'left':
4✔
363
                    $xAxis   = $options['hOffset'] + $options['padding'];
×
364
                    $yAxis   = $options['vOffset'] + $options['padding'];
×
365
                    $gravity = $options['vAlign'] === 'top' ? 'NorthWest' : 'West';
×
366
                    if ($options['vAlign'] === 'bottom') {
×
367
                        $gravity = 'SouthWest';
×
368
                        $yAxis   = $options['vOffset'] - $options['padding'];
×
369
                    }
370
                    break;
×
371

372
                case 'center':
4✔
373
                    $xAxis   = $options['hOffset'] + $options['padding'];
3✔
374
                    $yAxis   = $options['vOffset'] + $options['padding'];
3✔
375
                    $gravity = $options['vAlign'] === 'top' ? 'North' : 'Center';
3✔
376
                    if ($options['vAlign'] === 'bottom') {
3✔
377
                        $yAxis   = $options['vOffset'] - $options['padding'];
2✔
378
                        $gravity = 'South';
2✔
379
                    }
380
                    break;
3✔
381

382
                case 'right':
1✔
383
                    $xAxis   = $options['hOffset'] - $options['padding'];
1✔
384
                    $yAxis   = $options['vOffset'] + $options['padding'];
1✔
385
                    $gravity = $options['vAlign'] === 'top' ? 'NorthEast' : 'East';
1✔
386
                    if ($options['vAlign'] === 'bottom') {
1✔
387
                        $gravity = 'SouthEast';
1✔
388
                        $yAxis   = $options['vOffset'] - $options['padding'];
1✔
389
                    }
390
                    break;
1✔
391
            }
392

393
            $xAxis = $xAxis >= 0 ? '+' . $xAxis : $xAxis;
4✔
394
            $yAxis = $yAxis >= 0 ? '+' . $yAxis : $yAxis;
4✔
395

396
            $cmd .= ' -gravity ' . escapeshellarg($gravity) . ' -geometry ' . escapeshellarg("{$xAxis}{$yAxis}");
4✔
397
        }
398

399
        // Color
400
        if (isset($options['color'])) {
4✔
401
            [$r, $g, $b] = sscanf("#{$options['color']}", '#%02x%02x%02x');
4✔
402

403
            $cmd .= ' -fill ' . escapeshellarg("rgba({$r},{$g},{$b},{$options['opacity']})");
4✔
404
        }
405

406
        // Font Size - use points....
407
        if (isset($options['fontSize'])) {
4✔
408
            $cmd .= ' -pointsize ' . escapeshellarg((string) $options['fontSize']);
4✔
409
        }
410

411
        // Text
412
        $cmd .= ' -annotate 0 ' . escapeshellarg($text);
4✔
413

414
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
4✔
415
        $destination = $this->getResourcePath();
4✔
416

417
        $cmd = ' ' . escapeshellarg($source) . ' ' . $cmd . ' ' . escapeshellarg($destination);
4✔
418

419
        $this->process($cmd);
4✔
420
    }
421

422
    /**
423
     * Return the width of an image.
424
     *
425
     * @return int
426
     */
427
    public function _getWidth()
428
    {
429
        return imagesx(imagecreatefromstring(file_get_contents($this->resource)));
23✔
430
    }
431

432
    /**
433
     * Return the height of an image.
434
     *
435
     * @return int
436
     */
437
    public function _getHeight()
438
    {
439
        return imagesy(imagecreatefromstring(file_get_contents($this->resource)));
22✔
440
    }
441

442
    /**
443
     * Reads the EXIF information from the image and modifies the orientation
444
     * so that displays correctly in the browser. This is especially an issue
445
     * with images taken by smartphones who always store the image up-right,
446
     * but set the orientation flag to display it correctly.
447
     *
448
     * @param bool $silent If true, will ignore exceptions when PHP doesn't support EXIF.
449
     *
450
     * @return $this
451
     */
452
    public function reorient(bool $silent = false)
453
    {
454
        $orientation = $this->getEXIF('Orientation', $silent);
2✔
455

456
        return match ($orientation) {
2✔
457
            2       => $this->flip('horizontal'),
2✔
458
            3       => $this->rotate(180),
2✔
459
            4       => $this->rotate(180)->flip('horizontal'),
2✔
460
            5       => $this->rotate(90)->flip('horizontal'),
2✔
461
            6       => $this->rotate(90),
2✔
462
            7       => $this->rotate(270)->flip('horizontal'),
2✔
463
            8       => $this->rotate(270),
2✔
464
            default => $this,
2✔
465
        };
2✔
466
    }
467
}
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