Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.03% covered (warning)
82.03%
105 / 128
88.89% covered (warning)
88.89%
24 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Convert
82.03% covered (warning)
82.03%
105 / 128
88.89% covered (warning)
88.89%
24 / 27
59.82
0.00% covered (danger)
0.00%
0 / 1
 in
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 out
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
4.25
 getCommand
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
7
 execute
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 raw
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 gravity
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 density
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 profile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 removeProfile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 changeProfile
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 rotate
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 background
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 resample
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
5.07
 size
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 flatten
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 strip
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 flip
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 flop
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 type
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 layers
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 resize
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 crop
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 quality
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 colorspace
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 sepia
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 polaroid
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 bordercolor
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
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
15declare(strict_types=1);
16
17namespace Karla\Program;
18
19use InvalidArgumentException;
20use RuntimeException;
21use Karla\Action\Resize;
22use Karla\Action\Gravity;
23use Karla\Action\Density;
24use Karla\Action\Profile;
25use Karla\Action\Rotate;
26use Karla\Action\Background;
27use Karla\Action\Resample;
28use Karla\Action\Size;
29use Karla\Action\Flatten;
30use Karla\Action\Strip;
31use Karla\Action\Flip;
32use Karla\Action\Flop;
33use Karla\Action\Type;
34use Karla\Action\Layers;
35use Karla\Action\Crop;
36use Karla\Action\Quality;
37use Karla\Action\Colorspace;
38use Karla\Action\Sepia;
39use Karla\Action\Polaroid;
40use Karla\Action\Bordercolor;
41use Karla\Program;
42use Karla\Cache;
43use 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 */
53class 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}