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

codeigniter4 / CodeIgniter4 / 7298596047

22 Dec 2023 10:01AM UTC coverage: 85.049% (+0.004%) from 85.045%
7298596047

push

github

web-flow
Merge pull request #8359 from kenjis/docs-FileLocatorInterface

[4.5] fix: merge mistake

1 of 1 new or added line in 1 file covered. (100.0%)

147 existing lines in 11 files now uncovered.

19358 of 22761 relevant lines covered (85.05%)

193.82 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
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
     * Constructor.
38
     *
39
     * @param Images $config
40
     *
41
     * @throws ImageException
42
     */
43
    public function __construct($config = null)
44
    {
45
        parent::__construct($config);
35✔
46

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

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

64
        $escape = '\\';
8✔
65

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

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

74
        $this->process($action);
8✔
75

76
        return $this;
8✔
77
    }
78

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

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

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

98
        $this->process($action);
10✔
99

100
        return $this;
10✔
101
    }
102

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

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

118
        $action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
3✔
119

120
        $this->process($action);
3✔
121

122
        return $this;
3✔
123
    }
124

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

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

139
        $action = ' ' . $flatten . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
1✔
140

141
        $this->process($action);
1✔
142

143
        return $this;
1✔
144
    }
145

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

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

160
        $action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
5✔
161

162
        $this->process($action);
5✔
163

164
        return $this;
5✔
165
    }
166

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

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

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

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

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

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

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

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

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

217
        return $output;
29✔
218
    }
219

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

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

UNCOV
243
            $name = basename($target);
×
244
            $path = pathinfo($target, PATHINFO_DIRNAME);
×
245

UNCOV
246
            return $this->image()->copy($path, $name);
×
247
        }
248

249
        $this->ensureResource();
6✔
250

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

255
        $this->process($action, $quality);
6✔
256

257
        unlink($this->resource);
6✔
258

259
        return true;
6✔
260
    }
261

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

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

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

289
        $this->image()->copy($path, $name);
28✔
290

291
        return $this->resource;
28✔
292
    }
293

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

305
        $this->supportedFormatCheck();
6✔
306
    }
307

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

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

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

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

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

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

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

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

392
            $xAxis = $xAxis >= 0 ? '+' . $xAxis : $xAxis;
3✔
393
            $yAxis = $yAxis >= 0 ? '+' . $yAxis : $yAxis;
3✔
394

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

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

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

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

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

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

416
        $cmd = " '{$source}' {$cmd} '{$destination}'";
3✔
417

418
        $this->process($cmd);
3✔
419
    }
420

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

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

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

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

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

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

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

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

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

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

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