Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.40% covered (warning)
82.40%
103 / 125
88.89% covered (warning)
88.89%
24 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Convert
82.40% covered (warning)
82.40%
103 / 125
88.89% covered (warning)
88.89%
24 / 27
54.55
0.00% covered (danger)
0.00%
0 / 1
 in
100.00% covered (success)
100.00%
6 / 6
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%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 execute
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 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 string $inputFile = '';
61
62    /**
63     * Output file
64     *
65     * @var string
66     */
67    protected string $outputFile = '';
68
69    /**
70     * Add input argument
71     *
72     * @param string $filePath Input file path
73     *
74     * @throws \InvalidArgumentException
75     */
76    public function in(string $filePath): self
77    {
78        if (! file_exists($filePath)) {
79            $message = 'The input file path (' . $filePath . ') is invalid or the file could not be located.';
80            throw new InvalidArgumentException($message);
81        }
82
83        if (is_writeable($filePath)) {
84            $this->inputFile = '"' . $filePath . '"';
85        }
86
87        return $this;
88    }
89
90    /**
91     * Add output argument
92     *
93     * @param string $filePath Output file path
94     * @param bool $includeOptions Include the used options as part of the filename
95     *
96     * @throws \InvalidArgumentException
97     * @todo Implement include options to filename
98     */
99    public function out(string $filePath, bool $includeOptions = false): self
100    {
101        $pathinfo = pathinfo($filePath);
102        $dirname = $pathinfo['dirname'] ?? '.';
103        if (! file_exists($dirname)) {
104            $message = 'The output file path (' . $dirname . ') is invalid or could not be located.';
105            throw new InvalidArgumentException($message);
106        }
107        if (! is_writeable($dirname)) {
108            $message = 'The output file path (' . $dirname . ') is not writable.';
109            throw new InvalidArgumentException($message);
110        }
111        if (! $includeOptions) {
112            $this->outputFile = '"' . $dirname . '/' . $pathinfo['basename'] . '"';
113        } else {
114            // TODO implement this feature
115            $this->outputFile = '"' . $dirname . '/' . $pathinfo['basename'] . '"';
116        }
117
118        return $this;
119    }
120
121    /**
122     * Get the command to run
123     *
124     * @see ImageMagick::getCommand()
125     */
126    public function getCommand(): string
127    {
128        if ($this->outputFile == '') {
129            throw new RuntimeException('Can not perform convert without an output file');
130        }
131        if ($this->inputFile == '') {
132            throw new RuntimeException('Can not perform convert without an input file');
133        }
134
135        $inOptions = $this->getQuery()->prepareOptions($this->getQuery()->getInputOptions());
136        $outOptions = $this->getQuery()->prepareOptions($this->getQuery()->getOutputOptions());
137
138        // Get the base command (handles ImageMagick 7 vs 6 automatically)
139        $baseCommand = parent::getCommand();
140
141        return $baseCommand . ' ' . ($inOptions == '' ? '' : $inOptions . ' ') .
142               $this->inputFile . ' ' . ($outOptions == '' ? '' : $outOptions . ' ') . $this->outputFile;
143    }
144
145    /**
146     * Execute the command
147     *
148     * @param bool $reset Reset after execution
149     *
150     * @see ImageMagick::execute()
151     */
152    public function execute(bool $reset = true): string
153    {
154        if ($this->cache instanceof Cache) {
155            if (! $this->cache->isCached($this->inputFile, $this->outputFile, $this->getQuery()->getInputOptions())) {
156                parent::execute(false);
157                $this->cache->setCache($this->inputFile, $this->outputFile, $this->getQuery()->getInputOptions());
158                $temp = str_replace('"', '', $this->outputFile);
159                shell_exec('rm ' . $temp);
160                $out = $this->cache->getCached(
161                    $this->inputFile,
162                    $this->outputFile,
163                    $this->getQuery()->getInputOptions()
164                );
165                $this->getQuery()->reset();
166                return $out;
167            }
168            return $this->cache->getCached($this->inputFile, $this->outputFile, $this->getQuery()->getInputOptions());
169        } else {
170            $temp = str_replace('"', '', $this->outputFile);
171            parent::execute();
172            shell_exec('chmod 666 ' . $temp);
173            return $this->outputFile;
174        }
175    }
176
177    /**
178     * Raw arguments directly to ImageMagick
179     *
180     * @param string $arguments Arguments
181     * @param bool $input Defaults to an input option, use false to use it as an output option
182     *
183     * @see ImageMagick::raw()
184     */
185    public function raw(string $arguments, bool $input = true): self
186    {
187        parent::raw($arguments, $input);
188
189        return $this;
190    }
191
192    /**
193     * Set the gravity
194     *
195     * @param string $gravity Gravity
196     */
197    public function gravity(string $gravity): self
198    {
199        $action = new Gravity($this, $gravity);
200        $this->setQuery($action->perform($this->getQuery()));
201        return $this;
202    }
203
204    /**
205     * Set the density of the output image.
206     *
207     * @param int $width The width of the image
208     * @param int $height The height of the image
209     * @param bool $output If output is true density is set for the resulting image
210     *                     If output is false density is used for reading the input image
211     */
212    public function density(int $width = 72, int $height = 72, bool $output = true): self
213    {
214        $action = new Density($width, $height, $output);
215        $this->setQuery($action->perform($this->getQuery()));
216        return $this;
217    }
218
219    /**
220     * Add a profile to the image.
221     *
222     * @param string $profilePath Profile path
223     * @param string $profileName Profile name
224     */
225    public function profile(string $profilePath = "", string $profileName = ""): self
226    {
227        $action = new Profile($profilePath, $profileName);
228        $this->setQuery($action->perform($this->getQuery()));
229        return $this;
230    }
231
232    /**
233     * Remove a profile from the image.
234     *
235     * @param string $profileName Profile name
236     *
237     * @todo get list of profiles from image (can be done by identify but might be too expensive)
238     */
239    public function removeProfile(string $profileName): self
240    {
241        $action = new Profile('', $profileName, true);
242        $this->setQuery($action->perform($this->getQuery()));
243        return $this;
244    }
245
246    /**
247     * Change profile on the image.
248     *
249     * @param string $profilePathFrom Path to the profile
250     * @param string $profilePathTo Path to the profile
251     *
252     * @throws \InvalidArgumentException
253     */
254    public function changeProfile(string $profilePathFrom, string $profilePathTo): self
255    {
256        $this->getQuery()->notWith('profile', Query::ARGUMENT_TYPE_OUTPUT);
257        try {
258            $this->profile($profilePathFrom);
259        } catch (InvalidArgumentException $e) {
260            $message = $e->getMessage() . ' for input profile';
261            throw new InvalidArgumentException($message);
262        }
263        try {
264            $this->profile($profilePathTo);
265        } catch (InvalidArgumentException $e) {
266            $message = $e->getMessage() . ' for output profile';
267            throw new InvalidArgumentException($message);
268        }
269
270        return $this;
271    }
272
273    /**
274     * Rotate image
275     *
276     * @param int $degree Degrees to rotate the image
277     * @param string $background The background color to apply to empty triangles in the corners,
278     *                           left over from rotating the image
279     */
280    public function rotate(int $degree, string $background = '#ffffff'): self
281    {
282        $action = new Rotate($degree);
283        $this->setQuery($action->perform($this->getQuery()));
284        $this->background($background);
285        return $this;
286    }
287
288    /**
289     * Add a background color to a image
290     *
291     * @param string $color Color
292     */
293    public function background(string $color): self
294    {
295        $action = new Background($color);
296        $this->setQuery($action->perform($this->getQuery()));
297        return $this;
298    }
299
300    /**
301     * Resample the image to a new resolution
302     *
303     * @param int $newWidth New image resolution
304     * @param int|null $newHeight New image resolution
305     * @param int|null $originalWidth Original image resolution
306     * @param int|null $originalHeight Original image resolution
307     */
308    public function resample(
309        int $newWidth,
310        int|null $newHeight = null,
311        int|null $originalWidth = null,
312        int|null $originalHeight = null
313    ): self {
314        if ($originalWidth != null && $originalHeight != null) {
315            $this->density($originalWidth, $originalHeight, false);
316        }
317        if ($originalWidth != null && $originalHeight == null) {
318            $this->density($originalWidth, $originalWidth, false);
319        }
320
321        $action = new Resample($newWidth, $newHeight);
322        $this->setQuery($action->perform($this->getQuery()));
323        return $this;
324    }
325
326    /**
327     * Size the input image
328     *
329     * @param int|null $width Image width
330     * @param int|null $height Image height
331     */
332    public function size(int|null $width, int|null $height): self
333    {
334        $action = new Size($width, $height);
335        $this->setQuery($action->perform($this->getQuery()));
336        return $this;
337    }
338
339    /**
340     * Flatten layers in an image.
341     */
342    public function flatten(): self
343    {
344        $action = new Flatten();
345        $this->setQuery($action->perform($this->getQuery()));
346        return $this;
347    }
348
349    /**
350     * Strip image of any profiles or comments.
351     */
352    public function strip(): self
353    {
354        $action = new Strip();
355        $this->setQuery($action->perform($this->getQuery()));
356        return $this;
357    }
358
359    /**
360     * Flip image
361     */
362    public function flip(): self
363    {
364        $action = new Flip();
365        $this->setQuery($action->perform($this->getQuery()));
366        return $this;
367    }
368
369    /**
370     * Flop image
371     */
372    public function flop(): self
373    {
374        $action = new Flop();
375        $this->setQuery($action->perform($this->getQuery()));
376        return $this;
377    }
378
379    /**
380     * Set output image type
381     *
382     * @param string $type The output image type
383     */
384    public function type(string $type): self
385    {
386        $action = new Type($this, $type);
387        $this->setQuery($action->perform($this->getQuery()));
388        return $this;
389    }
390
391    /**
392     * Apply a method to layers in images.
393     *
394     * @param string $method The method to use
395     */
396    public function layers(string $method): self
397    {
398        $action = new Layers($this, $method);
399        $this->setQuery($action->perform($this->getQuery()));
400        return $this;
401    }
402
403    /**
404     * Resize the input image
405     *
406     * @param int|null $width Image width
407     * @param int|null $height Image height
408     * @param bool $maintainAspectRatio Should we maintain aspect ratio? default is true
409     * @param bool $dontScaleUp Should we prohibit scaling up? default is true
410     * @param string $aspect How should we handle aspect ratio?
411     */
412    public function resize(
413        int| null $width,
414        int| null $height,
415        bool $maintainAspectRatio = true,
416        bool $dontScaleUp = true,
417        string $aspect = Resize::ASPECT_FIT
418    ): self {
419        $action = new Resize($width, $height, $maintainAspectRatio, $dontScaleUp, $aspect);
420        $this->setQuery($action->perform($this->getQuery()));
421        return $this;
422    }
423
424    /**
425     * Resize the input image
426     *
427     * @param int $width Image width
428     * @param int $height Image height
429     * @param int $xOffset X offset from upper-left corner
430     * @param int $yOffset Y offset from upper-left corner
431     */
432    public function crop(int $width, int $height, int $xOffset = 0, int $yOffset = 0): self
433    {
434        $action = new Crop($width, $height, $xOffset, $yOffset);
435        $this->setQuery($action->perform($this->getQuery()));
436        return $this;
437    }
438
439    /**
440     * Set the quality of the output image for jpeg an png.
441     *
442     * @param int $quality A value between 0 - 100
443     * @param string $format Format to use; default is jpeg
444     */
445    public function quality(int $quality, string $format = 'jpeg'): self
446    {
447        $action = new Quality($quality, $format);
448        $this->setQuery($action->perform($this->getQuery()));
449        return $this;
450    }
451
452    /**
453     * Set the colorspace for the image
454     *
455     * @param string $colorSpace The colorspace to use
456     */
457    public function colorspace(string $colorSpace): self
458    {
459        $action = new Colorspace($this, $colorSpace);
460        $this->setQuery($action->perform($this->getQuery()));
461        return $this;
462    }
463
464    /**
465     * Sepia tone the image
466     *
467     * @param int $threshold The threshold to use
468     */
469    public function sepia(int $threshold = 80): self
470    {
471        $action = new Sepia($threshold);
472        $this->setQuery($action->perform($this->getQuery()));
473        return $this;
474    }
475
476    /**
477     * Add polaroid effect to the image
478     *
479     * @param int $angle The threshold to use
480     */
481    public function polaroid(int $angle = 0): self
482    {
483        $action = new Polaroid($angle);
484        $this->setQuery($action->perform($this->getQuery()));
485        return $this;
486    }
487
488    /**
489     * Set the color of the border if border is set
490     *
491     * @param string $color The color of the border
492     */
493    public function bordercolor(string $color = '#DFDFDF'): self
494    {
495        $action = new Bordercolor($color);
496        $this->setQuery($action->perform($this->getQuery()));
497        return $this;
498    }
499}