Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.08% covered (warning)
77.08%
37 / 48
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Karla
77.08% covered (warning)
77.08%
37 / 48
42.86% covered (danger)
42.86%
3 / 7
35.77
0.00% covered (danger)
0.00%
0 / 1
 perform
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 __construct
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 detectVersion
71.43% covered (warning)
71.43%
15 / 21
0.00% covered (danger)
0.00%
0 / 1
15.36
 raw
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
6.40
 convert
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 identify
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 composite
100.00% covered (success)
100.00%
2 / 2
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    2010-06-05
13 */
14
15declare(strict_types=1);
16
17namespace Karla;
18
19/**
20 * Karla core class
21 *
22 * @category Utility
23 * @author   Johannes Skov Frandsen <jsf@greenoak.dk>
24 * @license  http://www.opensource.org/licenses/mit-license.php MIT
25 * @link     https://github.com/localgod/karla Karla
26 */
27class Karla
28{
29    /**
30     * Path to imagemagick binary
31     *
32     * @var string $binPath imagemagick binary
33     */
34    private string $binPath;
35
36    /**
37     * Cache controller
38     *
39     * @var Cache
40     */
41    private Cache|null $cache;
42
43    /**
44     * ImageMagick major version
45     *
46     * @var int|null
47     */
48    private int|null $version = null;
49
50    /**
51     * Get an instance of Karla.
52     *
53     * @param string $binPath Path to ImageMagick binaries (optional)
54     * @param Cache|null $cache Cache controller (optional)
55     * @param int|null $version ImageMagick major version (optional, skips auto-detection when provided)
56     *
57     * @deprecated Since 1.2.0, use `new Karla($binPath, $cache, $version)` instead. Will be removed in 2.0.0.
58     *
59     * @throws \RuntimeException
60     */
61    public static function perform(
62        string $binPath = '/opt/local/bin/',
63        Cache|null $cache = null,
64        int|null $version = null
65    ): Karla {
66        try {
67            return new self($binPath, $cache, $version);
68        } catch (\InvalidArgumentException $e) {
69            throw new \RuntimeException($e->getMessage() . ' (' . $binPath . ')', 0, $e);
70        }
71    }
72
73    /**
74     * Construct a new Karla object.
75     *
76     * @param string $binPath Path to ImageMagick binaries
77     * @param Cache|null $cache Cache controller
78     * @param int|null $version ImageMagick major version (optional, skips auto-detection when provided)
79     *
80     * @throws \InvalidArgumentException if path for ImageMagick is invalid
81     */
82    public function __construct(string $binPath, Cache|null $cache = null, int|null $version = null)
83    {
84        if (! file_exists($binPath)) {
85            throw new \InvalidArgumentException('Bin path not found');
86        }
87
88        if ($version !== null) {
89            $this->version = $version;
90        } else {
91            $this->detectVersion($binPath);
92        }
93
94        // Set binPath appropriately for the OS
95        if (Platform::isWindows()) {
96            // On Windows, just use the direct path
97            $this->binPath = rtrim($binPath, '/\\') . '/';
98        } else {
99            // On Unix, use export PATH
100            $this->binPath = 'export PATH=$PATH:' . $binPath . ';';
101        }
102
103        $this->cache = $cache;
104    }
105
106    /**
107     * Detect the installed ImageMagick version and set $this->version.
108     *
109     * Results are cached statically per bin path to avoid repeated shell_exec calls.
110     *
111     * @param string $binPath Path to ImageMagick binaries
112     *
113     * @throws \InvalidArgumentException if ImageMagick cannot be located at the given path
114     */
115    private function detectVersion(string $binPath): void
116    {
117        /** @var array<string, int|null> $versionCache */
118        static $versionCache = [];
119
120        if (isset($versionCache[$binPath])) {
121            $this->version = $versionCache[$binPath];
122            return;
123        }
124
125        $magickBin = Platform::getBinary('magick');
126        $convertBin = Platform::getBinary('convert');
127
128        $versionOutput = '';
129
130        // Try ImageMagick 7 first (magick command)
131        if (file_exists($binPath . $magickBin)) {
132            $versionOutput = shell_exec(escapeshellarg($binPath . $magickBin) . ' -version');
133            if ($versionOutput && stripos($versionOutput, 'ImageMagick') !== false) {
134                // Extract major version (e.g., "Version: ImageMagick 7.1.2" -> 7)
135                if (preg_match('/ImageMagick (\d+)/', $versionOutput, $matches)) {
136                    $this->version = (int)$matches[1];
137                }
138            }
139        }
140
141        // Fallback to ImageMagick 6 (convert command)
142        if (empty($versionOutput) && file_exists($binPath . $convertBin)) {
143            $versionOutput = shell_exec(escapeshellarg($binPath . $convertBin) . ' -version');
144            if ($versionOutput && stripos($versionOutput, 'ImageMagick') !== false) {
145                if (preg_match('/ImageMagick (\d+)/', $versionOutput, $matches)) {
146                    $this->version = (int)$matches[1];
147                } else {
148                    $this->version = 6; // Assume v6 if version not found but convert exists
149                }
150            }
151        }
152
153        if (empty($versionOutput)) {
154            throw new \InvalidArgumentException('ImageMagick could not be located at specified path');
155        }
156
157        $versionCache[$binPath] = $this->version;
158    }
159
160    /**
161     * Run a raw ImageMagick command
162     *
163     * Karla was never intended to wrap all of the functionality of ImageMagick
164     * and likely never will, you will from time to time need to write arguments
165     * to ImageMagick like you would have done directly in the console.
166     *
167     * @param string $program ImageMagick tool to use
168     * @param string $arguments Arguments for the tool
169     *
170     * @throws \InvalidArgumentException if you try to run a non ImageMagick program
171     */
172    public function raw(string $program, string $arguments = ""): string
173    {
174        if (! Program\ImageMagick::validProgram($program)) {
175            throw new \InvalidArgumentException('ImageMagick could not be located at specified path');
176        }
177        $bin = Platform::getBinary($program);
178        // For ImageMagick 7+, prepend with magick command
179        if ($this->version !== null && $this->version >= 7) {
180            $magickBin = Platform::getBinary(Program\ImageMagick::IMAGEMAGICK_MAGICK);
181            $magick = Platform::getExecutable($this->binPath, $magickBin);
182            $result = shell_exec($magick . ' ' . $program . ' ' . $arguments);
183        } else {
184            $result = shell_exec(Platform::getExecutable($this->binPath, $bin) . ' ' . $arguments);
185        }
186
187        return $result !== null && $result !== false ? $result : '';
188    }
189
190    /**
191     * Start a convert operation
192     */
193    public function convert(): Program\Convert
194    {
195        $bin = Platform::getBinary(Program\ImageMagick::IMAGEMAGICK_CONVERT);
196
197        return new Program\Convert($this->binPath, $bin, $this->cache, $this->version);
198    }
199
200    /**
201     * Start an identify operation
202     */
203    public function identify(): Program\Identify
204    {
205        $bin = Platform::getBinary(Program\ImageMagick::IMAGEMAGICK_IDENTIFY);
206
207        return new Program\Identify($this->binPath, $bin, $this->cache, $this->version);
208    }
209
210    /**
211     * Start a composite operation
212     */
213    public function composite(): Program\Composite
214    {
215        $bin = Platform::getBinary(Program\ImageMagick::IMAGEMAGICK_COMPOSITE);
216
217        return new Program\Composite($this->binPath, $bin, $this->cache, $this->version);
218    }
219}