• 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

89.33
/system/Images/Handlers/ImageMagickHandler.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\I18n\Time;
15
use CodeIgniter\Images\Exceptions\ImageException;
16
use Config\Images;
17
use Exception;
18
use Imagick;
19

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

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

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

50
    /**
51
     * Handles the actual resizing of the image.
52
     *
53
     * @return ImageMagickHandler
54
     *
55
     * @throws Exception
56
     */
57
    public function _resize(bool $maintainRatio = false)
58
    {
59
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
8✔
60
        $destination = $this->getResourcePath();
8✔
61

62
        $escape = '\\';
8✔
63

64
        if (PHP_OS_FAMILY === 'Windows') {
8✔
65
            $escape = '';
×
66
        }
67

68
        $action = $maintainRatio === true
8✔
69
            ? ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' "' . $source . '" "' . $destination . '"'
3✔
70
            : ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . "{$escape}! \"" . $source . '" "' . $destination . '"';
5✔
71

72
        $this->process($action);
8✔
73

74
        return $this;
8✔
75
    }
76

77
    /**
78
     * Crops the image.
79
     *
80
     * @return bool|\CodeIgniter\Images\Handlers\ImageMagickHandler
81
     *
82
     * @throws Exception
83
     */
84
    public function _crop()
85
    {
86
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
10✔
87
        $destination = $this->getResourcePath();
10✔
88

89
        $extent = ' ';
10✔
90
        if ($this->xAxis >= $this->width || $this->yAxis > $this->height) {
10✔
91
            $extent = ' -background transparent -extent ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' ';
2✔
92
        }
93

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

96
        $this->process($action);
10✔
97

98
        return $this;
10✔
99
    }
100

101
    /**
102
     * Handles the rotation of an image resource.
103
     * Doesn't save the image, but replaces the current resource.
104
     *
105
     * @return $this
106
     *
107
     * @throws Exception
108
     */
109
    protected function _rotate(int $angle)
110
    {
111
        $angle = '-rotate ' . $angle;
3✔
112

113
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
3✔
114
        $destination = $this->getResourcePath();
3✔
115

116
        $action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
3✔
117

118
        $this->process($action);
3✔
119

120
        return $this;
3✔
121
    }
122

123
    /**
124
     * Flattens transparencies, default white background
125
     *
126
     * @return $this
127
     *
128
     * @throws Exception
129
     */
130
    protected function _flatten(int $red = 255, int $green = 255, int $blue = 255)
131
    {
132
        $flatten = "-background 'rgb({$red},{$green},{$blue})' -flatten";
1✔
133

134
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
1✔
135
        $destination = $this->getResourcePath();
1✔
136

137
        $action = ' ' . $flatten . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
1✔
138

139
        $this->process($action);
1✔
140

141
        return $this;
1✔
142
    }
143

144
    /**
145
     * Flips an image along it's vertical or horizontal axis.
146
     *
147
     * @return $this
148
     *
149
     * @throws Exception
150
     */
151
    protected function _flip(string $direction)
152
    {
153
        $angle = $direction === 'horizontal' ? '-flop' : '-flip';
5✔
154

155
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
5✔
156
        $destination = $this->getResourcePath();
5✔
157

158
        $action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
5✔
159

160
        $this->process($action);
5✔
161

162
        return $this;
5✔
163
    }
164

165
    /**
166
     * Get driver version
167
     */
168
    public function getVersion(): string
169
    {
170
        $result = $this->process('-version');
1✔
171

172
        // The first line has the version in it...
173
        preg_match('/(ImageMagick\s[\S]+)/', $result[0], $matches);
1✔
174

175
        return str_replace('ImageMagick ', '', $matches[0]);
1✔
176
    }
177

178
    /**
179
     * Handles all of the grunt work of resizing, etc.
180
     *
181
     * @return array Lines of output from shell command
182
     *
183
     * @throws Exception
184
     */
185
    protected function process(string $action, int $quality = 100): array
186
    {
187
        // Do we have a vaild library path?
188
        if (empty($this->config->libraryPath)) {
29✔
189
            throw ImageException::forInvalidImageLibraryPath($this->config->libraryPath);
×
190
        }
191

192
        if ($action !== '-version') {
29✔
193
            $this->supportedFormatCheck();
28✔
194
        }
195

196
        if (! preg_match('/convert$/i', $this->config->libraryPath)) {
29✔
197
            $this->config->libraryPath = rtrim($this->config->libraryPath, '/') . '/convert';
×
198
        }
199

200
        $cmd = $this->config->libraryPath;
29✔
201
        $cmd .= $action === '-version' ? ' ' . $action : ' -quality ' . $quality . ' ' . $action;
29✔
202

203
        $retval = 1;
29✔
204
        $output = [];
29✔
205
        // exec() might be disabled
206
        if (function_usable('exec')) {
29✔
207
            @exec($cmd, $output, $retval);
29✔
208
        }
209

210
        // Did it work?
211
        if ($retval > 0) {
29✔
212
            throw ImageException::forImageProcessFailed();
×
213
        }
214

215
        return $output;
29✔
216
    }
217

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

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

241
            $name = basename($target);
×
242
            $path = pathinfo($target, PATHINFO_DIRNAME);
×
243

244
            return $this->image()->copy($path, $name);
×
245
        }
246

247
        $this->ensureResource();
6✔
248

249
        // Copy the file through ImageMagick so that it has
250
        // a chance to convert file format.
251
        $action = escapeshellarg($this->resource) . ' ' . escapeshellarg($target);
6✔
252

253
        $this->process($action, $quality);
6✔
254

255
        unlink($this->resource);
6✔
256

257
        return true;
6✔
258
    }
259

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

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

284
        $name = basename($this->resource);
28✔
285
        $path = pathinfo($this->resource, PATHINFO_DIRNAME);
28✔
286

287
        $this->image()->copy($path, $name);
28✔
288

289
        return $this->resource;
28✔
290
    }
291

292
    /**
293
     * Make the image resource object if needed
294
     *
295
     * @return void
296
     *
297
     * @throws Exception
298
     */
299
    protected function ensureResource()
300
    {
301
        $this->getResourcePath();
6✔
302

303
        $this->supportedFormatCheck();
6✔
304
    }
305

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

324
    /**
325
     * Handler-specific method for overlaying text on an image.
326
     *
327
     * @return void
328
     *
329
     * @throws Exception
330
     */
331
    protected function _text(string $text, array $options = [])
332
    {
333
        $xAxis   = 0;
3✔
334
        $yAxis   = 0;
3✔
335
        $gravity = '';
3✔
336
        $cmd     = '';
3✔
337

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

348
        if ($options['hAlign'] === 'right') {
3✔
349
            $options['hOffset'] *= -1;
1✔
350
        }
351

352
        // Font
353
        if (! empty($options['fontPath'])) {
3✔
354
            $cmd .= " -font '{$options['fontPath']}'";
×
355
        }
356

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

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

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

390
            $xAxis = $xAxis >= 0 ? '+' . $xAxis : $xAxis;
3✔
391
            $yAxis = $yAxis >= 0 ? '+' . $yAxis : $yAxis;
3✔
392

393
            $cmd .= " -gravity {$gravity} -geometry {$xAxis}{$yAxis}";
3✔
394
        }
395

396
        // Color
397
        if (isset($options['color'])) {
3✔
398
            [$r, $g, $b] = sscanf("#{$options['color']}", '#%02x%02x%02x');
3✔
399

400
            $cmd .= " -fill 'rgba({$r},{$g},{$b},{$options['opacity']})'";
3✔
401
        }
402

403
        // Font Size - use points....
404
        if (isset($options['fontSize'])) {
3✔
405
            $cmd .= " -pointsize {$options['fontSize']}";
3✔
406
        }
407

408
        // Text
409
        $cmd .= " -annotate 0 '{$text}'";
3✔
410

411
        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
3✔
412
        $destination = $this->getResourcePath();
3✔
413

414
        $cmd = " '{$source}' {$cmd} '{$destination}'";
3✔
415

416
        $this->process($cmd);
3✔
417
    }
418

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

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

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

453
        switch ($orientation) {
454
            case 2:
2✔
455
                return $this->flip('horizontal');
2✔
456

457
            case 3:
2✔
458
                return $this->rotate(180);
2✔
459

460
            case 4:
2✔
461
                return $this->rotate(180)->flip('horizontal');
2✔
462

463
            case 5:
2✔
464
                return $this->rotate(90)->flip('horizontal');
2✔
465

466
            case 6:
2✔
467
                return $this->rotate(90);
2✔
468

469
            case 7:
2✔
470
                return $this->rotate(270)->flip('horizontal');
2✔
471

472
            case 8:
2✔
473
                return $this->rotate(270);
2✔
474

475
            default:
476
                return $this;
2✔
477
        }
478
    }
479
}
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