Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
82.03% |
105 / 128 |
|
88.89% |
24 / 27 |
CRAP | |
0.00% |
0 / 1 |
Convert | |
82.03% |
105 / 128 |
|
88.89% |
24 / 27 |
59.82 | |
0.00% |
0 / 1 |
in | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
out | |
75.00% |
9 / 12 |
|
0.00% |
0 / 1 |
4.25 | |||
getCommand | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
7 | |||
execute | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
raw | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
gravity | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
density | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
profile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
removeProfile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
changeProfile | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
rotate | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
background | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
resample | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
5.07 | |||
size | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
flatten | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
strip | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
flip | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
flop | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
type | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
layers | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
resize | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
crop | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
quality | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
colorspace | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
sepia | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
polaroid | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
bordercolor | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * Karla ImageMagick wrapper library |
5 | * |
6 | * PHP Version 8.0< |
7 | * |
8 | * @category Utility |
9 | * @author Johannes Skov Frandsen <jsf@greenoak.dk> |
10 | * @license http://www.opensource.org/licenses/mit-license.php MIT |
11 | * @link https://github.com/localgod/karla Karla |
12 | * @since 2012-04-05 |
13 | */ |
14 | |
15 | declare(strict_types=1); |
16 | |
17 | namespace Karla\Program; |
18 | |
19 | use InvalidArgumentException; |
20 | use RuntimeException; |
21 | use Karla\Action\Resize; |
22 | use Karla\Action\Gravity; |
23 | use Karla\Action\Density; |
24 | use Karla\Action\Profile; |
25 | use Karla\Action\Rotate; |
26 | use Karla\Action\Background; |
27 | use Karla\Action\Resample; |
28 | use Karla\Action\Size; |
29 | use Karla\Action\Flatten; |
30 | use Karla\Action\Strip; |
31 | use Karla\Action\Flip; |
32 | use Karla\Action\Flop; |
33 | use Karla\Action\Type; |
34 | use Karla\Action\Layers; |
35 | use Karla\Action\Crop; |
36 | use Karla\Action\Quality; |
37 | use Karla\Action\Colorspace; |
38 | use Karla\Action\Sepia; |
39 | use Karla\Action\Polaroid; |
40 | use Karla\Action\Bordercolor; |
41 | use Karla\Program; |
42 | use Karla\Cache; |
43 | use Karla\Query; |
44 | |
45 | /** |
46 | * Class for wrapping ImageMagicks convert tool |
47 | * |
48 | * @category Utility |
49 | * @author Johannes Skov Frandsen <jsf@greenoak.dk> |
50 | * @license http://www.opensource.org/licenses/mit-license.php MIT |
51 | * @link https://github.com/localgod/karla Karla |
52 | */ |
53 | class Convert extends ImageMagick implements Program |
54 | { |
55 | /** |
56 | * Input file |
57 | * |
58 | * @var string |
59 | */ |
60 | protected $inputFile; |
61 | |
62 | /** |
63 | * Output file |
64 | * |
65 | * @var string |
66 | */ |
67 | protected $outputFile; |
68 | |
69 | /** |
70 | * Add input argument |
71 | * |
72 | * @param string $filePath |
73 | * Input file path |
74 | * |
75 | * @return Convert |
76 | * @throws \InvalidArgumentException |
77 | */ |
78 | public function in(string $filePath): self |
79 | { |
80 | if (! file_exists($filePath)) { |
81 | $message = 'The input file path (' . $filePath . ') is invalid or the file could not be located.'; |
82 | throw new InvalidArgumentException($message); |
83 | } |
84 | |
85 | if (is_writeable($filePath)) { |
86 | $this->inputFile = '"' . $filePath . '"'; |
87 | } |
88 | $this->getQuery()->dirty(); |
89 | |
90 | return $this; |
91 | } |
92 | |
93 | /** |
94 | * Add output argument |
95 | * |
96 | * @param string $filePath |
97 | * Output file path |
98 | * @param boolean $includeOptions |
99 | * Include the used options as part of the filename |
100 | * |
101 | * @return Convert |
102 | * @throws \InvalidArgumentException |
103 | * @todo Implement include options to filename |
104 | */ |
105 | public function out(string $filePath, bool $includeOptions = false): self |
106 | { |
107 | $pathinfo = pathinfo($filePath); |
108 | if (! file_exists($pathinfo['dirname'])) { |
109 | $message = 'The output file path (' . $pathinfo['dirname'] . ') is invalid or could not be located.'; |
110 | throw new InvalidArgumentException($message); |
111 | } |
112 | if (! is_writeable($pathinfo['dirname'])) { |
113 | $message = 'The output file path (' . $pathinfo['dirname'] . ') is not writable.'; |
114 | throw new InvalidArgumentException($message); |
115 | } |
116 | if (! $includeOptions) { |
117 | $this->outputFile = '"' . $pathinfo['dirname'] . '/' . $pathinfo['basename'] . '"'; |
118 | } else { |
119 | // TODO implement this feature |
120 | $this->outputFile = '"' . $pathinfo['dirname'] . '/' . $pathinfo['basename'] . '"'; |
121 | } |
122 | $this->getQuery()->dirty(); |
123 | |
124 | return $this; |
125 | } |
126 | |
127 | /** |
128 | * Get the command to run |
129 | * |
130 | * @see ImageMagick::getCommand() |
131 | * @return string |
132 | */ |
133 | public function getCommand(): string |
134 | { |
135 | if ($this->outputFile == '') { |
136 | throw new RuntimeException('Can not perform convert without an output file'); |
137 | } |
138 | if ($this->inputFile == '') { |
139 | throw new RuntimeException('Can not perform convert without an input file'); |
140 | } |
141 | |
142 | ! is_array($this->getQuery()->getOutputOptions()) ? $this->getQuery()->setOutputOption("") : null; |
143 | ! is_array($this->getQuery()->getInputOptions()) ? $this->getQuery()->setInputOption("") : null; |
144 | $inOptions = $this->getQuery()->prepareOptions($this->getQuery()->getInputOptions()); |
145 | $outOptions = $this->getQuery()->prepareOptions($this->getQuery()->getOutputOptions()); |
146 | |
147 | return $this->binPath . $this->bin . ' ' . ($inOptions == '' ? '' : $inOptions . ' ') . |
148 | $this->inputFile . ' ' . ($outOptions == '' ? '' : $outOptions . ' ') . $this->outputFile; |
149 | } |
150 | |
151 | /** |
152 | * Execute the command |
153 | * |
154 | * @see ImageMagick::execute() |
155 | * @return string |
156 | */ |
157 | public function execute($reset = true): string |
158 | { |
159 | if ($this->cache instanceof Cache) { |
160 | ! is_array($this->getQuery()->getInputOptions()) ? $this->getQuery()->setInputOption("") : null; |
161 | if (! $this->cache->isCached($this->inputFile, $this->outputFile, $this->getQuery()->getInputOptions())) { |
162 | parent::execute(false); |
163 | $this->cache->setCache($this->inputFile, $this->outputFile, $this->getQuery()->getInputOptions()); |
164 | $temp = str_replace('"', '', $this->outputFile); |
165 | shell_exec('rm ' . $temp); |
166 | $out = $this->cache->getCached( |
167 | $this->inputFile, |
168 | $this->outputFile, |
169 | $this->getQuery()->getInputOptions() |
170 | ); |
171 | $this->getQuery()->reset(); |
172 | return $out; |
173 | } |
174 | return $this->cache->getCached($this->inputFile, $this->outputFile, $this->getQuery()->getInputOptions()); |
175 | } else { |
176 | $temp = str_replace('"', '', $this->outputFile); |
177 | parent::execute(); |
178 | shell_exec('chmod 666 ' . $temp); |
179 | return $this->outputFile; |
180 | } |
181 | } |
182 | |
183 | /** |
184 | * Raw arguments directly to ImageMagick |
185 | * |
186 | * @param string $arguments |
187 | * Arguments |
188 | * @param boolean $input |
189 | * Defaults to an input option, use false to use it as an output option |
190 | * |
191 | * @return Convert |
192 | * @see ImageMagick::raw() |
193 | */ |
194 | public function raw(string $arguments, bool $input = true): self |
195 | { |
196 | parent::raw($arguments, $input); |
197 | |
198 | return $this; |
199 | } |
200 | |
201 | /** |
202 | * Set the gravity |
203 | * |
204 | * @param string $gravity |
205 | * Gravity |
206 | * |
207 | * @return Convert |
208 | */ |
209 | public function gravity(string $gravity): self |
210 | { |
211 | $action = new Gravity($this, $gravity); |
212 | $this->setQuery($action->perform($this->getQuery())); |
213 | return $this; |
214 | } |
215 | |
216 | /** |
217 | * Set the density of the output image. |
218 | * |
219 | * @param integer $width |
220 | * The width of the image |
221 | * @param integer $height |
222 | * The height of the image |
223 | * @param boolean $output |
224 | * If output is true density is set for the resulting image |
225 | * If output is false density is used for reading the input image |
226 | * |
227 | * @return Convert |
228 | */ |
229 | public function density(int $width = 72, int $height = 72, bool $output = true): self |
230 | { |
231 | $action = new Density($width, $height, $output); |
232 | $this->setQuery($action->perform($this->getQuery())); |
233 | return $this; |
234 | } |
235 | |
236 | /** |
237 | * Add a profile to the image. |
238 | * |
239 | * @param string $profilePath |
240 | * Profile path |
241 | * @param string $profileName |
242 | * Profile name |
243 | * |
244 | * @return Convert |
245 | */ |
246 | public function profile(string $profilePath = "", string $profileName = ""): self |
247 | { |
248 | $action = new Profile($profilePath, $profileName); |
249 | $this->setQuery($action->perform($this->getQuery())); |
250 | return $this; |
251 | } |
252 | |
253 | /** |
254 | * Remove a profile from the image. |
255 | * |
256 | * @param string $profileName |
257 | * Profile name |
258 | * |
259 | * @return Convert |
260 | * |
261 | * @todo get list of profiles from image (can be done by identify but might be too expensive) |
262 | */ |
263 | public function removeProfile(string $profileName): self |
264 | { |
265 | $action = new Profile('', $profileName, true); |
266 | $this->setQuery($action->perform($this->getQuery())); |
267 | return $this; |
268 | } |
269 | |
270 | /** |
271 | * Change profile on the image. |
272 | * |
273 | * @param string $profilePathFrom |
274 | * Path to the profile |
275 | * @param string $profilePathTo |
276 | * Path to the profile |
277 | * |
278 | * @return Convert |
279 | * @throws \InvalidArgumentException |
280 | */ |
281 | public function changeProfile(string $profilePathFrom, string $profilePathTo): self |
282 | { |
283 | $this->getQuery()->notWith('profile', Query::ARGUMENT_TYPE_OUTPUT); |
284 | try { |
285 | $this->profile($profilePathFrom); |
286 | } catch (InvalidArgumentException $e) { |
287 | $message = $e->getMessage() . ' for input profile'; |
288 | throw new InvalidArgumentException($message); |
289 | } |
290 | try { |
291 | $this->profile($profilePathTo); |
292 | } catch (InvalidArgumentException $e) { |
293 | $message = $e->getMessage() . ' for output profile'; |
294 | throw new InvalidArgumentException($message); |
295 | } |
296 | |
297 | return $this; |
298 | } |
299 | |
300 | /** |
301 | * Rotate image |
302 | * |
303 | * @param integer $degree |
304 | * Degrees to rotate the image |
305 | * @param string $background |
306 | * The background color to apply to empty triangles in the corners, |
307 | * left over from rotating the image |
308 | * |
309 | * @return Convert |
310 | */ |
311 | public function rotate(int $degree, string $background = '#ffffff'): self |
312 | { |
313 | $action = new Rotate($degree); |
314 | $this->setQuery($action->perform($this->getQuery())); |
315 | $this->background($background); |
316 | return $this; |
317 | } |
318 | |
319 | /** |
320 | * Add a background color to a image |
321 | * |
322 | * @param string $color |
323 | * Color |
324 | * |
325 | * @return Convert |
326 | */ |
327 | public function background(string $color): self |
328 | { |
329 | $action = new Background($color); |
330 | $this->setQuery($action->perform($this->getQuery())); |
331 | return $this; |
332 | } |
333 | |
334 | /** |
335 | * Resample the image to a new resolution |
336 | * |
337 | * @param integer $newWidth |
338 | * New image resolution |
339 | * @param integer $newHeight |
340 | * New image resolution |
341 | * @param integer $originalWidth |
342 | * Original image resolution |
343 | * @param integer $originalHeight |
344 | * Original image resolution |
345 | * |
346 | * @return Convert |
347 | */ |
348 | public function resample( |
349 | int $newWidth, |
350 | int|null $newHeight = null, |
351 | int|null $originalWidth = null, |
352 | int|null $originalHeight = null |
353 | ): self { |
354 | if ($originalWidth != null && $originalHeight != null) { |
355 | $this->density($originalWidth, $originalHeight, false); |
356 | } |
357 | if ($originalWidth != null && $originalHeight == null) { |
358 | $this->density($originalWidth, $originalWidth, false); |
359 | } |
360 | |
361 | $action = new Resample($newWidth, $newHeight); |
362 | $this->setQuery($action->perform($this->getQuery())); |
363 | return $this; |
364 | } |
365 | |
366 | /** |
367 | * Size the input image |
368 | * |
369 | * @param integer $width |
370 | * Image width |
371 | * @param integer $height |
372 | * Image height |
373 | * |
374 | * @return Convert |
375 | */ |
376 | public function size(int|null $width, int|null $height): self |
377 | { |
378 | $action = new Size($width, $height); |
379 | $this->setQuery($action->perform($this->getQuery())); |
380 | return $this; |
381 | } |
382 | |
383 | /** |
384 | * Flatten layers in an image. |
385 | * |
386 | * @return Convert |
387 | */ |
388 | public function flatten(): self |
389 | { |
390 | $action = new Flatten(); |
391 | $this->setQuery($action->perform($this->getQuery())); |
392 | return $this; |
393 | } |
394 | |
395 | /** |
396 | * Strip image of any profiles or comments. |
397 | * |
398 | * @return Convert |
399 | */ |
400 | public function strip(): self |
401 | { |
402 | $action = new Strip(); |
403 | $this->setQuery($action->perform($this->getQuery())); |
404 | return $this; |
405 | } |
406 | |
407 | /** |
408 | * Flip image |
409 | * |
410 | * @return Convert |
411 | */ |
412 | public function flip(): self |
413 | { |
414 | $action = new Flip(); |
415 | $this->setQuery($action->perform($this->getQuery())); |
416 | return $this; |
417 | } |
418 | |
419 | /** |
420 | * Flop image |
421 | * |
422 | * @return Convert |
423 | */ |
424 | public function flop(): self |
425 | { |
426 | $action = new Flop(); |
427 | $this->setQuery($action->perform($this->getQuery())); |
428 | return $this; |
429 | } |
430 | |
431 | /** |
432 | * Set output image type |
433 | * |
434 | * @param string $type |
435 | * The output image type |
436 | * |
437 | * @return Convert |
438 | */ |
439 | public function type(string $type): self |
440 | { |
441 | $action = new Type($this, $type); |
442 | $this->setQuery($action->perform($this->getQuery())); |
443 | return $this; |
444 | } |
445 | |
446 | /** |
447 | * Apply a method to layers in images. |
448 | * |
449 | * @param string $method |
450 | * The method to use |
451 | * |
452 | * @return Convert |
453 | */ |
454 | public function layers(string $method): self |
455 | { |
456 | $action = new Layers($this, $method); |
457 | $this->setQuery($action->perform($this->getQuery())); |
458 | return $this; |
459 | } |
460 | |
461 | /** |
462 | * Resize the input image |
463 | * |
464 | * @param integer $width |
465 | * Image width |
466 | * @param integer $height |
467 | * Image height |
468 | * @param boolean $maintainAspectRatio |
469 | * Should we maintain aspect ratio? default is true |
470 | * @param boolean $dontScaleUp |
471 | * Should we prohipped scaling up? default is true |
472 | * @param string $aspect |
473 | * How should we handle aspect ratio? |
474 | * |
475 | * @return Convert |
476 | */ |
477 | public function resize( |
478 | int| null $width, |
479 | int| null $height, |
480 | bool $maintainAspectRatio = true, |
481 | bool $dontScaleUp = true, |
482 | string $aspect = Resize::ASPECT_FIT |
483 | ): self { |
484 | $action = new Resize($width, $height, $maintainAspectRatio, $dontScaleUp, $aspect); |
485 | $this->setQuery($action->perform($this->getQuery())); |
486 | return $this; |
487 | } |
488 | |
489 | /** |
490 | * Resize the input image |
491 | * |
492 | * @param integer $width |
493 | * Image width |
494 | * @param integer $height |
495 | * Image height |
496 | * @param integer $xOffset |
497 | * X offset from upper-left corner |
498 | * @param integer $yOffset |
499 | * Y offset from upper-left corner |
500 | * |
501 | * @return Convert |
502 | */ |
503 | public function crop(int $width, int $height, int $xOffset = 0, int $yOffset = 0): self |
504 | { |
505 | $action = new Crop($width, $height, $xOffset, $yOffset); |
506 | $this->setQuery($action->perform($this->getQuery())); |
507 | return $this; |
508 | } |
509 | |
510 | /** |
511 | * Set the quality of the output image for jpeg an png. |
512 | * |
513 | * @param integer $quality |
514 | * A value between 0 - 100 |
515 | * @param string $format |
516 | * Format to use; default is jpeg |
517 | * |
518 | * @return Convert |
519 | */ |
520 | public function quality(int $quality, string $format = 'jpeg'): self |
521 | { |
522 | $action = new Quality($quality, $format); |
523 | $this->setQuery($action->perform($this->getQuery())); |
524 | return $this; |
525 | } |
526 | |
527 | /** |
528 | * Set the colorspace for the image |
529 | * |
530 | * @param string $colorSpace |
531 | * The colorspace to use |
532 | * |
533 | * @return Convert |
534 | */ |
535 | public function colorspace(string $colorSpace): self |
536 | { |
537 | $action = new Colorspace($this, $colorSpace); |
538 | $this->setQuery($action->perform($this->getQuery())); |
539 | return $this; |
540 | } |
541 | |
542 | /** |
543 | * Sepia tone the image |
544 | * |
545 | * @param integer $threshold |
546 | * The threshold to use |
547 | * |
548 | * @return Convert |
549 | */ |
550 | public function sepia(int $threshold = 80): self |
551 | { |
552 | $action = new Sepia($threshold); |
553 | $this->setQuery($action->perform($this->getQuery())); |
554 | return $this; |
555 | } |
556 | |
557 | /** |
558 | * Add polaroid effect to the image |
559 | * |
560 | * @param integer $angle |
561 | * The threshold to use |
562 | * |
563 | * @return Convert |
564 | */ |
565 | public function polaroid(int $angle = 0): self |
566 | { |
567 | $action = new Polaroid($angle); |
568 | $this->setQuery($action->perform($this->getQuery())); |
569 | return $this; |
570 | } |
571 | |
572 | /** |
573 | * Set the color of the border if border is set |
574 | * |
575 | * @param string $color |
576 | * The color of the border |
577 | * |
578 | * @return Convert |
579 | */ |
580 | public function bordercolor(string $color = '#DFDFDF'): self |
581 | { |
582 | $action = new Bordercolor($color); |
583 | $this->setQuery($action->perform($this->getQuery())); |
584 | return $this; |
585 | } |
586 | } |