Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
CommandBuilder
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
5 / 5
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInput
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setOutput
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 build
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getBaseCommand
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
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    2026-03-26
13 */
14
15declare(strict_types=1);
16
17namespace Karla;
18
19/**
20 * Centralises ImageMagick command construction.
21 *
22 * Each Program subclass (Convert, Identify, …) delegates to this class from
23 * its getCommand() method, keeping quoting/escaping logic and the IM6-vs-IM7
24 * binary-selection logic in a single, easily testable place.
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 */
31class CommandBuilder
32{
33    /**
34     * Escaped input file path
35     *
36     * @var string
37     */
38    private string $inputFile = '';
39
40    /**
41     * Escaped output file path
42     *
43     * @var string
44     */
45    private string $outputFile = '';
46
47    /**
48     * @param Query       $query     The current query (holds accumulated options)
49     * @param string      $binPath   Path prefix for binaries (may include PATH export)
50     * @param string      $binary    Platform-appropriate binary name (e.g. "convert")
51     * @param int|null    $imVersion Detected ImageMagick major version (6 or 7)
52     */
53    public function __construct(
54        private Query $query,
55        private string $binPath,
56        private string $binary,
57        private ?int $imVersion
58    ) {
59    }
60
61    /**
62     * Set the (already-escaped) input file path.
63     *
64     * The caller is responsible for escaping the path with escapeshellarg()
65     * before passing it here; this method stores the value as-is so that
66     * escaping is never applied twice.
67     *
68     * @param string $escapedFilePath Shell-safe file path
69     */
70    public function setInput(string $escapedFilePath): self
71    {
72        $this->inputFile = $escapedFilePath;
73        return $this;
74    }
75
76    /**
77     * Set the (already-escaped) output file path.
78     *
79     * @param string $escapedFilePath Shell-safe file path
80     */
81    public function setOutput(string $escapedFilePath): self
82    {
83        $this->outputFile = $escapedFilePath;
84        return $this;
85    }
86
87    /**
88     * Assemble and return the complete command string.
89     *
90     * Parts are joined with a single space; empty optional parts (options or
91     * files that were not set) are simply omitted, so no extra whitespace is
92     * introduced.
93     */
94    public function build(): string
95    {
96        $parts = [$this->getBaseCommand()];
97
98        $inOptions = $this->query->prepareOptions($this->query->getInputOptions());
99        if ($inOptions !== '') {
100            $parts[] = $inOptions;
101        }
102
103        if ($this->inputFile !== '') {
104            $parts[] = $this->inputFile;
105        }
106
107        $outOptions = $this->query->prepareOptions($this->query->getOutputOptions());
108        if ($outOptions !== '') {
109            $parts[] = $outOptions;
110        }
111
112        if ($this->outputFile !== '') {
113            $parts[] = $this->outputFile;
114        }
115
116        return implode(' ', $parts);
117    }
118
119    /**
120     * Return the base binary invocation, handling ImageMagick 7 vs 6.
121     *
122     * ImageMagick 7 consolidates all tools under the unified `magick` command:
123     * - `convert` becomes `magick`
124     * - `identify` / `composite` become `magick identify` / `magick composite`
125     *
126     * ImageMagick 6 keeps the individual tool binaries as-is.
127     */
128    private function getBaseCommand(): string
129    {
130        if ($this->imVersion !== null && $this->imVersion >= 7) {
131            // Strip the .exe suffix (if any) before comparing the command name
132            $command = str_replace('.exe', '', $this->binary);
133            $magickBin = Platform::getBinary('magick');
134
135            if ($command === 'convert') {
136                return Platform::getExecutable($this->binPath, $magickBin);
137            }
138
139            if (in_array($command, ['identify', 'composite'])) {
140                return Platform::getExecutable($this->binPath, $magickBin) . ' ' . $command;
141            }
142        }
143
144        return Platform::getExecutable($this->binPath, $this->binary);
145    }
146}