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) |
|
|
|
|
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) |
|
|
|
|
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() |
|
|
|
|
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() |
|
|
|
|
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)) { |
|
|
|
|
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)) { |
|
|
|
|
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)) { |
|
|
|
|
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)) { |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
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.