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