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