Completed
Push — master ( 32257f...b97b6f )
by
unknown
05:21
created

ImagemagickImage::availableCommands()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace Charcoal\Image\Imagemagick;
4
5
use \Exception;
6
use \InvalidArgumentException;
7
use \OutOfBoundsException;
8
9
use \Charcoal\Image\AbstractImage;
10
11
/**
12
 * The Imagemagick image driver.
13
 *
14
 * Run from the binary imagemagick scripts.
15
 * (`mogrify`, `convert` and `identify`)
16
 */
17
class ImagemagickImage extends AbstractImage
18
{
19
    /**
20
     * The temporary file location
21
     * @var string $tmpFile
22
     */
23
    private $tmpFile;
24
25
    /**
26
     * @var string $mogrifyCmd
27
     */
28
    private $mogrifyCmd;
29
30
    /**
31
     * @var string $convertCmd
32
     */
33
    private $convertCmd;
34
35
    /**
36
     * @var string $compositeCmd
37
     */
38
    private $compositeCmd;
39
40
    /**
41
     * @var string $identifyCmd
42
     */
43
    private $identifyCmd;
44
45
    /**
46
     * Set up the commands.
47
     */
48
    public function __construct()
49
    {
50
        // This will throw exception if the binaris are not found.
51
        $this->mogrifyCmd = $this->mogrifyCmd();
52
        $this->convertCmd = $this->convertCmd();
53
    }
54
55
    /**
56
     * Clean up the tmp file, if necessary
57
     */
58
    public function __destruct()
59
    {
60
        $this->resetTmp();
61
    }
62
63
    /**
64
     * @return string
65
     */
66
    public function driverType()
67
    {
68
        return 'imagemagick';
69
    }
70
71
    /**
72
     * Create a blank canvas of a given size, with a given background color.
73
     *
74
     * @param integer $width  Image size, in pixels.
75
     * @param integer $height Image height, in pixels.
76
     * @param string  $color  Default to transparent.
77
     * @throws InvalidArgumentException If the size arguments are not valid.
78
     * @return Image Chainable
79
     */
80
    public function create($width, $height, $color = 'rgb(100%, 100%, 100%, 0)')
81
    {
82
        if (!is_numeric($width) || $width < 1) {
83
            throw new InvalidArgumentException(
84
                'Width must be an integer of at least 1 pixel'
85
            );
86
        }
87
        if (!is_numeric($height) || $height < 1) {
88
            throw new InvalidArgumentException(
89
                'Height must be an integer of at least 1 pixel'
90
            );
91
        }
92
93
        $this->resetTmp();
94
        touch($this->tmp());
95
        $this->exec($this->convertCmd().' -size '.(int)$width.'x'.(int)$height.' canvas:"'.$color.'" '.$this->tmp());
96
        return $this;
97
    }
98
99
    /**
100
     * Open an image file
101
     *
102
     * @param string $source The source path / filename.
103
     * @throws Exception If the source file does not exist.
104
     * @throws InvalidArgumentException If the source argument is not a string.
105
     * @return Image Chainable
106
     */
107 View Code Duplication
    public function open($source = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
108
    {
109
        if ($source !== null && !is_string($source)) {
110
            throw new InvalidArgumentException(
111
                'Source must be a string (file path)'
112
            );
113
        }
114
        $source = ($source) ? $source : $this->source();
115
        $this->resetTmp();
116
        if (!file_exists($source)) {
117
            throw new Exception(
118
                sprintf('File "%s" does not exist', $source)
119
            );
120
        }
121
        $tmp = $this->tmp();
122
        copy($source, $tmp);
123
        return $this;
124
    }
125
126
    /**
127
     * Save an image to a target.
128
     * If no target is set, the original source will be owerwritten
129
     *
130
     * @param string $target The target path / filename.
131
     * @throws Exception If the target file does not exist or is not writeable.
132
     * @throws InvalidArgumentException If the target argument is not a string.
133
     * @return Image Chainable
134
     */
135 View Code Duplication
    public function save($target = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
136
    {
137
        if ($target !== null && !is_string($target)) {
138
            throw new InvalidArgumentException(
139
                'Target must be a string (file path)'
140
            );
141
        }
142
        $target = ($target) ? $target : $this->target();
143
        if (!is_writable(dirname($target))) {
144
            throw new Exception(
145
                sprintf('Target "%s" is not writable', $target)
146
            );
147
        }
148
        copy($this->tmp(), $target);
149
        return $this;
150
    }
151
152
    /**
153
     * Get the image's width, in pixels
154
     *
155
     * @return integer
156
     */
157 View Code Duplication
    public function width()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
158
    {
159
        if (!file_exists($this->tmp())) {
160
            return 0;
161
        }
162
        $cmd = $this->identifyCmd().' -format "%w" '.$this->tmp();
163
        return (int)trim($this->exec($cmd));
164
    }
165
166
    /**
167
     * Get the image's height, in pixels
168
     *
169
     * @return integer
170
     */
171 View Code Duplication
    public function height()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
172
    {
173
        if (!file_exists($this->tmp())) {
174
            return 0;
175
        }
176
        $cmd = $this->identifyCmd().' -format "%h" '.$this->tmp();
177
        return (int)trim($this->exec($cmd));
178
    }
179
180
    /**
181
     * @param string $channel The channel name to convert.
182
     * @return string
183
     */
184
    public function convertChannel($channel)
185
    {
186
        return ucfirst($channel);
187
    }
188
189
    /**
190
     * Find a command.
191
     *
192
     * Try (as best as possible) to find a command name:
193
     *
194
     * - With `type -p`
195
     * - Or else, with `where`
196
     * - Or else, with `which`
197
     *
198
     * @param  string $cmdName The command name to find.
199
     * @throws InvalidArgumentException If the given command name is not a string.
200
     * @throws OutOfBoundsException If the command is unsupported or can not be found.
201
     * @return string
202
     */
203
    protected function findCmd($cmdName)
204
    {
205 View Code Duplication
        if (!is_string($cmdName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
206
            throw new InvalidArgumentException(sprintf(
207
                'Target image must be a string, received %s',
208
                (is_object($cmdName) ? get_class($cmdName) : gettype($cmdName))
209
            ));
210
        }
211
212
        if (!in_array($cmdName, $this->availableCommands())) {
213 View Code Duplication
            if (!is_string($cmdName)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
214
                $cmdName = (is_object($cmdName) ? get_class($cmdName) : gettype($cmdName));
215
            }
216
            throw new OutOfBoundsException(sprintf(
217
                'Unsupported command "%s" provided',
218
                $cmdName
219
            ));
220
        }
221
222
        $cmd = exec('type -p '.$cmdName);
223
        $cmd = str_replace($cmdName.' is ', '', $cmd);
224
225
        if (!$cmd) {
226
            $cmd = exec('where '.$cmdName);
227
        }
228
229
        if (!$cmd) {
230
            $cmd = exec('which '.$cmdName);
231
        }
232
233
        if (!$cmd) {
234
            throw new OutOfBoundsException(sprintf(
235
                'Can not find ImageMagick\'s "%s" command.',
236
                $cmdName
237
            ));
238
        }
239
240
        return $cmd;
241
    }
242
243
    /**
244
     * Retrieve the list of available commands.
245
     *
246
     * @return array
247
     */
248
    public function availableCommands()
249
    {
250
        return [ 'mogrify', 'convert', 'composite', 'identify' ];
251
    }
252
253
    /**
254
     * Retrieve the list of available commands.
255
     *
256
     * @param  string $name The name of an available command.
257
     * @throws InvalidArgumentException If the name is not a string.
258
     * @throws OutOfBoundsException If the name is unsupported.
259
     * @return array
260
     */
261
    public function cmd($name)
262
    {
263 View Code Duplication
        if (!is_string($cmdName)) {
0 ignored issues
show
Bug introduced by
The variable $cmdName does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
264
            throw new InvalidArgumentException(sprintf(
265
                'Target image must be a string, received %s',
266
                (is_object($cmdName) ? get_class($cmdName) : gettype($cmdName))
267
            ));
268
        }
269
270
        switch ($name) {
271
            case 'mogrify':
272
                return $this->mogrifyCmd();
273
274
            case 'convert':
275
                return $this->convertCmd();
276
277
            case 'composite':
278
                return $this->compositeCmd();
279
280
            case 'identify':
281
                return $this->identifyCmd();
282
283
            default:
284 View Code Duplication
                if (!is_string($name)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
285
                    $name = (is_object($name) ? get_class($name) : gettype($name));
286
                }
287
                throw new OutOfBoundsException(sprintf(
288
                    'Unsupported command "%s" provided',
289
                    $name
290
                ));
291
        }
292
    }
293
294
    /**
295
     * @return string The full path of the mogrify command.
296
     */
297
    public function mogrifyCmd()
298
    {
299
        if ($this->mogrifyCmd !== null) {
300
            return $this->mogrifyCmd;
301
        }
302
        $this->mogrifyCmd = $this->findCmd('mogrify');
303
        return $this->mogrifyCmd;
304
    }
305
306
    /**
307
     * @return string The full path of the convert command.
308
     */
309
    public function convertCmd()
310
    {
311
        if ($this->convertCmd !== null) {
312
            return $this->convertCmd;
313
        }
314
        $this->convertCmd = $this->findCmd('convert');
315
        return $this->convertCmd;
316
    }
317
318
    /**
319
     * @return string The full path of the composite command.
320
     */
321
    public function compositeCmd()
322
    {
323
        if ($this->compositeCmd !== null) {
324
            return $this->compositeCmd;
325
        }
326
        $this->compositeCmd = $this->findCmd('composite');
327
        return $this->compositeCmd;
328
    }
329
330
    /**
331
     * @return string The full path of the identify comand.
332
     */
333
    public function identifyCmd()
334
    {
335
        if ($this->identifyCmd !== null) {
336
            return $this->identifyCmd;
337
        }
338
        $this->identifyCmd = $this->findCmd('identify');
339
        return $this->identifyCmd;
340
    }
341
342
    /**
343
     * Generate a temporary file, to apply effects on.
344
     *
345
     * @return string
346
     */
347
    public function tmp()
348
    {
349
        if ($this->tmpFile !== null) {
350
            return $this->tmpFile;
351
        }
352
353
        $this->tmpFile = sys_get_temp_dir().'/'.uniqid().'.png';
354
        return $this->tmpFile;
355
    }
356
357
    /**
358
     * @return ImagemagickImage Chainable
359
     */
360
    public function resetTmp()
361
    {
362
        if (file_exists($this->tmpFile)) {
363
            unlink($this->tmpFile);
364
        }
365
        $this->tmpFile = null;
366
        return $this;
367
    }
368
369
    /**
370
     * Exec a command, either with `proc_open()` or `shell_exec()`
371
     *
372
     * The `proc_open()` method is preferred, as it allows to catch errors in
373
     * the STDERR buffer (and throw Exception) but it might be disabled in some
374
     * systems for security reasons.
375
     *
376
     * @param string $cmd The command to execute.
377
     * @throws Exception If the command fails.
378
     * @return string
379
     */
380
    public function exec($cmd)
381
    {
382
        if (function_exists('proc_open')) {
383
            $proc = proc_open(
384
                $cmd,
385
                [
386
                    1 => ['pipe','w'],
387
                    2 => ['pipe','w'],
388
                ],
389
                $pipes
390
            );
391
            $out = stream_get_contents($pipes[1]);
392
            fclose($pipes[1]);
393
            $err = stream_get_contents($pipes[2]);
394
            fclose($pipes[2]);
395
            proc_close($proc);
396
397
            if ($err) {
398
                throw new Exception(
399
                    sprintf('Error executing command "%s": %s', $cmd, $err)
400
                );
401
            }
402
403
            return $out;
404
        } else {
405
            $ret = shell_exec($cmd, $out);
0 ignored issues
show
Bug introduced by
The variable $out seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
406
407
            return $ret;
408
        }
409
    }
410
411
    /**
412
     * @param  string      $params The $cmd's arguments and options.
413
     * @param  string|null $cmd    The command to run.
414
     * @throws Exception If the tmp file was not properly set.
415
     * @return ImagemagickImage Chainable
416
     */
417
    public function applyCmd($params, $cmd = null)
418
    {
419
        if ($cmd === null) {
420
            $cmd = $this->mogrifyCmd();
421
        } else {
422
            $cmd = $this->cmd($cmd);
423
        }
424
425
        if (!file_exists($this->tmp())) {
426
            throw new Exception(
427
                'No file currently set as tmp file, commands can not be executed.'
428
            );
429
        }
430
431
        $this->exec($cmd.' '.$params.' '.$this->tmp());
432
433
        return $this;
434
    }
435
436
    /**
437
     * Convert a gravity name (string) to an `Imagick::GRAVITY_*` constant (integer)
438
     *
439
     * @param string $gravity The standard gravity name.
440
     * @throws InvalidArgumentException If the gravity argument is not a valid gravity.
441
     * @return integer
442
     */
443
    public function imagemagickGravity($gravity)
444
    {
445
        $gravityMap = [
446
            'center'    => 'center',
447
            'n'         => 'north',
448
            's'         => 'south',
449
            'e'         => 'east',
450
            'w'         => 'west',
451
            'ne'        => 'northeast',
452
            'nw'        => 'northwest',
453
            'se'        => 'southeast',
454
            'sw'        => 'southwest'
455
        ];
456
        if (!isset($gravityMap[$gravity])) {
457
            throw new InvalidArgumentException(
458
                'Invalid gravity. Possible values are: center, n, s, e, w, ne, nw, se, sw.'
459
            );
460
        }
461
        return $gravityMap[$gravity];
462
    }
463
}
464