Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.36% covered (warning)
86.36%
57 / 66
45.45% covered (danger)
45.45%
5 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageMagick
86.36% covered (warning)
86.36%
57 / 66
45.45% covered (danger)
45.45%
5 / 11
28.85
0.00% covered (danger)
0.00%
0 / 1
 __construct
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
3.07
 getQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __clone
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getBinary
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 getCommand
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
5.93
 executeCommand
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
 execute
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 executeWithResult
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 raw
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 validProgram
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
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 Karla\Cache;
20use Karla\CommandResult;
21use Karla\Query;
22
23/**
24 * Class for wrapping ImageMagick arguments used by all tools
25 *
26 * @category Utility
27 * @author   Johannes Skov Frandsen <jsf@greenoak.dk>
28 * @license  http://www.opensource.org/licenses/mit-license.php MIT
29 * @link     https://github.com/localgod/karla Karla
30 */
31abstract class ImageMagick implements \Karla\Program
32{
33    /**
34     * ImageMagick tool animate
35     *
36     * @var string
37     */
38    public const IMAGEMAGICK_ANIMATE = 'animate';
39
40    /**
41     * ImageMagick tool compare
42     *
43     * @var string
44     */
45    public const IMAGEMAGICK_COMPARE = 'compare';
46
47    /**
48     * ImageMagick tool composite
49     *
50     * @var string
51     */
52    public const IMAGEMAGICK_COMPOSITE = 'composite';
53
54    /**
55     * ImageMagick tool
56     *
57     * @var string
58     */
59    public const IMAGEMAGICK_CONJURE = 'conjure';
60
61    /**
62     * ImageMagick tool conjure
63     *
64     * @var string
65     */
66    public const IMAGEMAGICK_CONVERT = 'convert';
67
68    /**
69     * ImageMagick tool display
70     *
71     * @var string
72     */
73    public const IMAGEMAGICK_DISPLAY = 'display';
74
75    /**
76     * ImageMagick tool identify
77     *
78     * @var string
79     */
80    public const IMAGEMAGICK_IDENTIFY = 'identify';
81
82    /**
83     * ImageMagick tool import
84     *
85     * @var string
86     */
87    public const IMAGEMAGICK_IMPORT = 'import';
88
89    /**
90     * ImageMagick tool mogrify
91     *
92     * @var string
93     */
94    public const IMAGEMAGICK_MOGRIFY = 'mogrify';
95
96    /**
97     * ImageMagick tool montage
98     *
99     * @var string
100     */
101    public const IMAGEMAGICK_MONTAGE = 'montage';
102
103    /**
104     * ImageMagick tool stream
105     *
106     * @var string
107     */
108    public const IMAGEMAGICK_STREAM = 'stream';
109
110    /**
111     * ImageMagick unified command (v7+)
112     *
113     * @var string
114     */
115    public const IMAGEMAGICK_MAGICK = 'magick';
116
117    /**
118     * Path to binaries
119     *
120     * @var string
121     */
122    public string $binPath;
123
124    /**
125     * ImageMagick major version
126     *
127     * @var int|null
128     */
129    protected int|null $version = null;
130
131    /**
132     * Name of binary
133     *
134     * @var string
135     */
136    protected string $bin;
137
138    /**
139     * Cache controller
140     *
141     * @var Cache
142     */
143    protected Cache|null $cache;
144
145    /**
146     * The current query
147     *
148     * @var Query
149     */
150    private Query $query;
151
152    /**
153     * Contructs a new program
154     *
155     * @param string $binPath
156     *            Path to binaries
157     * @param string $bin
158     *            Binary
159     * @param \Karla\Cache|null $cache
160     *            Cache controller (default null = no cache)
161     * @param int|null $version
162     *            ImageMagick major version (6 or 7)
163     *
164     * @throws \InvalidArgumentException
165     */
166    final public function __construct(
167        string $binPath,
168        string $bin,
169        \Karla\Cache|null $cache = null,
170        int|null $version = null
171    ) {
172        if ($binPath == '') {
173            throw new \InvalidArgumentException('Invalid bin path');
174        }
175        if ($bin == '') {
176            throw new \InvalidArgumentException('Invalid bin');
177        }
178        $this->binPath = $binPath;
179        $this->bin = $bin;
180        $this->cache = $cache;
181        $this->version = $version;
182        $this->query = new Query();
183        $this->getQuery()->reset();
184    }
185
186    /**
187     * Get the current query
188     */
189    public function getQuery(): Query
190    {
191        return $this->query;
192    }
193
194    /**
195     * Set the current query
196     *
197     * @param Query $query Query to set
198     */
199    public function setQuery(Query $query): void
200    {
201        $this->query = $query;
202    }
203
204    /**
205     * Prevent cloning to avoid shared mutable Query state.
206     *
207     * PHP's default clone behavior is a shallow copy: scalar properties are copied by value,
208     * but object-typed properties keep referring to the same underlying objects unless they
209     * are explicitly cloned inside __clone().
210     * Since this class holds a mutable Query object that accumulates command options,
211     * cloning would cause both the original and the clone to share the same Query instance.
212     * Any modification of the Query in one instance (e.g. adding options) would affect the
213     * other instance as well, leading to command options leaking between instances and
214     * causing hard-to-diagnose bugs.
215     *
216     * Example of the problem if cloning were allowed:
217     * <code>
218     * $convert1 = $karla->convert()->input('file.jpg')->resize(100, 100);
219     * $convert2 = clone $convert1;  // Would share the same Query object!
220     * $convert2->crop(50, 50);      // Modifies shared Query
221     * $convert1->getCommand();      // Contains BOTH resize AND crop (bug!)
222     * </code>
223     *
224     * @throws \BadMethodCallException Always - cloning is not supported
225     */
226    final public function __clone(): void
227    {
228        throw new \BadMethodCallException(
229            'Cloning ImageMagick command builders is not supported due to shared mutable state. ' .
230            'Create a new instance instead: $karla->convert() or $karla->identify()'
231        );
232    }
233
234    /**
235     * Get the binary executable path (for use in -list commands, etc.)
236     * This returns just the base binary without full command setup
237     */
238    public function getBinary(): string
239    {
240        // For ImageMagick 7+, use 'magick' instead of separate tools
241        if ($this->version !== null && $this->version >= 7) {
242            $magickBin = \Karla\Platform::getBinary(self::IMAGEMAGICK_MAGICK);
243            return $this->binPath . $magickBin;
244        }
245
246        return $this->binPath . $this->bin;
247    }
248
249    /**
250     * Get the command to run
251     */
252    public function getCommand(): string
253    {
254        // For ImageMagick 7+, use 'magick' instead of separate tools
255        if ($this->version !== null && $this->version >= 7) {
256            // Get the base command name without .exe extension
257            $command = str_replace('.exe', '', $this->bin);
258
259            // For convert, just use 'magick' (ImageMagick 7 combines convert functionality into magick)
260            // For identify and composite, use 'magick <subcommand>'
261            if ($command === self::IMAGEMAGICK_CONVERT) {
262                $magickBin = \Karla\Platform::getBinary(self::IMAGEMAGICK_MAGICK);
263                return $this->binPath . $magickBin;
264            } elseif (in_array($command, [self::IMAGEMAGICK_IDENTIFY, self::IMAGEMAGICK_COMPOSITE])) {
265                $magickBin = \Karla\Platform::getBinary(self::IMAGEMAGICK_MAGICK);
266                return $this->binPath . $magickBin . ' ' . $command;
267            }
268        }
269
270        return $this->binPath . $this->bin;
271    }
272
273    /**
274     * Execute a shell command using proc_open and return a CommandResult
275     *
276     * @param string $command The full command string to execute
277     *
278     * @throws \RuntimeException When the process cannot be started
279     */
280    protected function executeCommand(string $command): CommandResult
281    {
282        $descriptors = [
283            0 => ['pipe', 'r'],
284            1 => ['pipe', 'w'],
285            2 => ['pipe', 'w'],
286        ];
287
288        $process = proc_open($command, $descriptors, $pipes);
289
290        if (!is_resource($process)) {
291            throw new \RuntimeException("Failed to execute command: {$command}");
292        }
293
294        fclose($pipes[0]);
295        $output = stream_get_contents($pipes[1]);
296        $error  = stream_get_contents($pipes[2]);
297        fclose($pipes[1]);
298        fclose($pipes[2]);
299
300        $exitCode = proc_close($process);
301
302        return new CommandResult(
303            $output !== false ? $output : '',
304            $error !== false ? $error : '',
305            $exitCode
306        );
307    }
308
309    /**
310     * Execute the command and return its output
311     *
312     * @param bool $reset Reset the query after execution
313     *
314     * @deprecated Since 1.2.0, use executeWithResult() for better error handling. Will be removed in 2.0.0.
315     */
316    public function execute(bool $reset = true): string|object
317    {
318        $result = $this->executeCommand($this->getCommand());
319        if ($reset) {
320            $this->getQuery()->reset();
321        }
322
323        return $result->output;
324    }
325
326    /**
327     * Execute the command and return a CommandResult with output, error and exit code
328     *
329     * @param bool $reset Reset the query after execution
330     */
331    public function executeWithResult(bool $reset = true): CommandResult
332    {
333        $result = $this->executeCommand($this->getCommand());
334        if ($reset) {
335            $this->getQuery()->reset();
336        }
337
338        return $result;
339    }
340
341    /**
342     * Raw arguments directly to ImageMagick
343     *
344     * @param string $arguments Arguments
345     * @param bool $input Defaults to an input option, use false to use it as an output option
346     */
347    public function raw(string $arguments, bool $input = true): self
348    {
349        if ($input) {
350            $this->getQuery()->setInputOption($arguments);
351        } else {
352            $this->getQuery()->setOutputOption($arguments);
353        }
354        return $this;
355    }
356
357    /**
358     * Check if the input is a valid ImageMagick program
359     *
360     * @param string $program Program name
361     */
362    final public static function validProgram(string $program): bool
363    {
364        $class = new \ReflectionClass(__CLASS__);
365        $constants = $class->getConstants();
366        foreach ($constants as $constant) {
367            if ($constant == $program) {
368                return true;
369            }
370        }
371
372        return false;
373    }
374}