1 | <?php |
||
2 | |||
3 | namespace WebPConvert\Convert\Converters; |
||
4 | |||
5 | use WebPConvert\Convert\Converters\AbstractConverter; |
||
6 | use WebPConvert\Convert\Converters\BaseTraits\WarningLoggerTrait; |
||
7 | use WebPConvert\Convert\Converters\ConverterTraits\EncodingAutoTrait; |
||
8 | use WebPConvert\Convert\Converters\ConverterTraits\ExecTrait; |
||
9 | use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException; |
||
10 | use WebPConvert\Convert\Exceptions\ConversionFailedException; |
||
11 | use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException; |
||
12 | use WebPConvert\Helpers\BinaryDiscovery; |
||
13 | use WebPConvert\Options\BooleanOption; |
||
14 | use WebPConvert\Options\SensitiveStringOption; |
||
15 | use WebPConvert\Options\StringOption; |
||
16 | |||
17 | /** |
||
18 | * Convert images to webp by calling cwebp binary. |
||
19 | * |
||
20 | * @package WebPConvert |
||
21 | * @author Bjørn Rosell <[email protected]> |
||
22 | * @since Class available since Release 2.0.0 |
||
23 | */ |
||
24 | class Cwebp extends AbstractConverter |
||
25 | { |
||
26 | |||
27 | use EncodingAutoTrait; |
||
28 | use ExecTrait; |
||
29 | |||
30 | protected function getUnsupportedDefaultOptions() |
||
31 | { |
||
32 | return []; |
||
33 | } |
||
34 | |||
35 | 9 | protected function createOptions() |
|
36 | { |
||
37 | 9 | parent::createOptions(); |
|
38 | |||
39 | 9 | $this->options2->addOptions( |
|
40 | 9 | new StringOption('command-line-options', ''), |
|
41 | 9 | new SensitiveStringOption('rel-path-to-precompiled-binaries', './Binaries'), |
|
42 | 9 | new BooleanOption('try-cwebp', true), |
|
43 | 9 | new BooleanOption('try-common-system-paths', true), |
|
44 | 9 | new BooleanOption('try-discovering-cwebp', true), |
|
45 | 9 | new BooleanOption('try-supplied-binary-for-os', true) |
|
46 | ); |
||
47 | 9 | } |
|
48 | |||
49 | // OS-specific binaries included in this library, along with hashes |
||
50 | // If other binaries are going to be added, notice that the first argument is what PHP_OS returns. |
||
51 | // (possible values, see here: https://stackoverflow.com/questions/738823/possible-values-for-php-os) |
||
52 | // Got the precompiled binaries here: https://developers.google.com/speed/webp/docs/precompiled |
||
53 | private static $suppliedBinariesInfo = [ |
||
54 | 'WINNT' => [ |
||
55 | ['cwebp-1.0.3-windows-x64.exe', 'b3aaab03ca587e887f11f6ae612293d034ee04f4f7f6bc7a175321bb47a10169'], |
||
56 | ], |
||
57 | 'Darwin' => [ |
||
58 | ['cwebp-1.0.3-mac-10.14', '7332ed5f0d4091e2379b1eaa32a764f8c0d51b7926996a1dc8b4ef4e3c441a12'], |
||
59 | ], |
||
60 | 'SunOS' => [ |
||
61 | // Got this from ewww Wordpress plugin, which unfortunately still uses the old 0.6.0 versions |
||
62 | // Can you help me get a 1.0.3 version? |
||
63 | ['cwebp-0.6.0-solaris', '1febaffbb18e52dc2c524cda9eefd00c6db95bc388732868999c0f48deb73b4f'] |
||
64 | ], |
||
65 | 'FreeBSD' => [ |
||
66 | // Got this from ewww Wordpress plugin, which unfortunately still uses the old 0.6.0 versions |
||
67 | // Can you help me get a 1.0.3 version? |
||
68 | ['cwebp-0.6.0-fbsd', 'e5cbea11c97fadffe221fdf57c093c19af2737e4bbd2cb3cd5e908de64286573'] |
||
69 | ], |
||
70 | 'Linux' => [ |
||
71 | // Dynamically linked executable. |
||
72 | // It seems it is slightly faster than the statically linked |
||
73 | ['cwebp-1.0.3-linux-x86-64', 'a663215a46d347f63e1ca641c18527a1ae7a2c9a0ae85ca966a97477ea13dfe0'], |
||
74 | |||
75 | // Statically linked executable |
||
76 | // It may be that it on some systems works, where the dynamically linked does not (see #196) |
||
77 | ['cwebp-1.0.3-linux-x86-64-static', 'ab96f01b49336da8b976c498528080ff614112d5985da69943b48e0cb1c5228a'], |
||
78 | |||
79 | // Old executable for systems in case both of the above fails |
||
80 | ['cwebp-0.6.1-linux-x86-64', '916623e5e9183237c851374d969aebdb96e0edc0692ab7937b95ea67dc3b2568'], |
||
81 | ] |
||
82 | ]; |
||
83 | |||
84 | /** |
||
85 | * Check all hashes of the precompiled binaries. |
||
86 | * |
||
87 | * This isn't used when converting, but can be used as a startup check. |
||
88 | */ |
||
89 | public function checkAllHashes() |
||
90 | { |
||
91 | foreach (self::$suppliedBinariesInfo as $os => $arr) { |
||
92 | foreach ($arr as $i => list($filename, $hash)) { |
||
93 | if ($hash != hash_file("sha256", __DIR__ . '/Binaries/' . $filename)) { |
||
94 | throw new \Exception('Hash for ' . $filename . ' is incorrect!'); |
||
95 | } |
||
96 | } |
||
97 | } |
||
98 | } |
||
99 | |||
100 | 4 | public function checkOperationality() |
|
101 | { |
||
102 | 4 | $this->checkOperationalityExecTrait(); |
|
103 | |||
104 | 4 | $options = $this->options; |
|
105 | 4 | if (!$options['try-supplied-binary-for-os'] && |
|
106 | 4 | !$options['try-common-system-paths'] && |
|
107 | 4 | !$options['try-cwebp'] && |
|
108 | 4 | !$options['try-discovering-cwebp'] |
|
109 | ) { |
||
110 | 1 | throw new ConverterNotOperationalException( |
|
111 | 'Configured to neither try pure cwebp command, ' . |
||
112 | 'nor look for cweb binaries in common system locations and ' . |
||
113 | 'nor to use one of the supplied precompiled binaries. ' . |
||
114 | 1 | 'But these are the only ways this converter can convert images. No conversion can be made!' |
|
115 | ); |
||
116 | } |
||
117 | 3 | } |
|
118 | |||
119 | 2 | private function executeBinary($binary, $commandOptions, $useNice) |
|
120 | { |
||
121 | //$version = $this->detectVersion($binary); |
||
122 | |||
123 | 2 | $command = ($useNice ? 'nice ' : '') . $binary . ' ' . $commandOptions; |
|
124 | |||
125 | //$logger->logLn('command options:' . $commandOptions); |
||
126 | 2 | $this->logLn('Trying to convert by executing the following command:'); |
|
127 | 2 | $this->logLn($command); |
|
128 | 2 | exec($command, $output, $returnCode); |
|
129 | 2 | $this->logExecOutput($output); |
|
130 | /* |
||
131 | if ($returnCode == 255) { |
||
132 | if (isset($output[0])) { |
||
133 | // Could be an error like 'Error! Cannot open output file' or 'Error! ...preset... ' |
||
134 | $this->logLn(print_r($output[0], true)); |
||
135 | } |
||
136 | }*/ |
||
137 | //$logger->logLn(self::msgForExitCode($returnCode)); |
||
138 | 2 | return intval($returnCode); |
|
139 | } |
||
140 | |||
141 | /** |
||
142 | * Use "escapeshellarg()" on all arguments in a commandline string of options |
||
143 | * |
||
144 | * For example, passing '-sharpness 5 -crop 10 10 40 40 -low_memory' will result in: |
||
145 | * [ |
||
146 | * "-sharpness '5'" |
||
147 | * "-crop '10' '10' '40' '40'" |
||
148 | * "-low_memory" |
||
149 | * ] |
||
150 | * @param string $commandLineOptions string which can contain multiple commandline options |
||
151 | * @return array Array of command options |
||
152 | */ |
||
153 | 1 | private static function escapeShellArgOnCommandLineOptions($commandLineOptions) |
|
154 | { |
||
155 | 1 | if (!ctype_print($commandLineOptions)) { |
|
156 | throw new ConversionFailedException( |
||
157 | 'Non-printable characters are not allowed in the extra command line options' |
||
158 | ); |
||
159 | } |
||
160 | |||
161 | 1 | if (preg_match('#[^a-zA-Z0-9_\s\-]#', $commandLineOptions)) { |
|
162 | throw new ConversionFailedException('The extra command line options contains inacceptable characters'); |
||
163 | } |
||
164 | |||
165 | 1 | $cmdOptions = []; |
|
166 | 1 | $arr = explode(' -', ' ' . $commandLineOptions); |
|
167 | 1 | foreach ($arr as $cmdOption) { |
|
168 | 1 | $pos = strpos($cmdOption, ' '); |
|
169 | 1 | $cName = ''; |
|
170 | 1 | if (!$pos) { |
|
171 | 1 | $cName = $cmdOption; |
|
172 | 1 | if ($cName == '') { |
|
173 | 1 | continue; |
|
174 | } |
||
175 | 1 | $cmdOptions[] = '-' . $cName; |
|
176 | } else { |
||
177 | 1 | $cName = substr($cmdOption, 0, $pos); |
|
178 | 1 | $cValues = substr($cmdOption, $pos + 1); |
|
179 | 1 | $cValuesArr = explode(' ', $cValues); |
|
180 | 1 | foreach ($cValuesArr as &$cArg) { |
|
181 | 1 | $cArg = escapeshellarg($cArg); |
|
182 | } |
||
183 | 1 | $cValues = implode(' ', $cValuesArr); |
|
184 | 1 | $cmdOptions[] = '-' . $cName . ' ' . $cValues; |
|
185 | } |
||
186 | } |
||
187 | 1 | return $cmdOptions; |
|
188 | } |
||
189 | |||
190 | /** |
||
191 | * Build command line options for a given version of cwebp. |
||
192 | * |
||
193 | * The "-near_lossless" param is not supported on older versions of cwebp, so skip on those. |
||
194 | * |
||
195 | * @param string $version Version of cwebp (ie "1.0.3") |
||
196 | * @return string |
||
197 | */ |
||
198 | 6 | private function createCommandLineOptions($version) |
|
199 | { |
||
200 | |||
201 | 6 | $this->logLn('Creating command line options for version: ' . $version); |
|
202 | |||
203 | // we only need two decimal places for version. |
||
204 | // convert to number to make it easier to compare |
||
205 | 6 | $version = preg_match('#^\d+\.\d+#', $version, $matches); |
|
206 | 6 | $versionNum = 0; |
|
207 | 6 | if (isset($matches[0])) { |
|
208 | 6 | $versionNum = floatval($matches[0]); |
|
209 | } else { |
||
210 | $this->logLn( |
||
211 | 'Could not extract version number from the following version string: ' . $version, |
||
212 | 'bold' |
||
213 | ); |
||
214 | } |
||
215 | |||
216 | //$this->logLn('version:' . strval($versionNum)); |
||
217 | |||
218 | 6 | $options = $this->options; |
|
219 | |||
220 | 6 | $cmdOptions = []; |
|
221 | |||
222 | // Metadata (all, exif, icc, xmp or none (default)) |
||
223 | // Comma-separated list of existing metadata to copy from input to output |
||
224 | 6 | if ($versionNum >= 0.3) { |
|
225 | 6 | $cmdOptions[] = '-metadata ' . $options['metadata']; |
|
226 | } |
||
227 | |||
228 | // preset. Appears first in the list as recommended in the docs |
||
229 | 6 | if (!is_null($options['preset'])) { |
|
230 | 6 | if ($options['preset'] != 'none') { |
|
231 | 1 | $cmdOptions[] = '-preset ' . $options['preset']; |
|
232 | } |
||
233 | } |
||
234 | |||
235 | // Size |
||
236 | 6 | $addedSizeOption = false; |
|
237 | 6 | if (!is_null($options['size-in-percentage'])) { |
|
238 | 1 | $sizeSource = filesize($this->source); |
|
239 | 1 | if ($sizeSource !== false) { |
|
240 | 1 | $targetSize = floor($sizeSource * $options['size-in-percentage'] / 100); |
|
241 | 1 | $cmdOptions[] = '-size ' . $targetSize; |
|
242 | 1 | $addedSizeOption = true; |
|
243 | } |
||
244 | } |
||
245 | |||
246 | // quality |
||
247 | 6 | if (!$addedSizeOption) { |
|
248 | 5 | $cmdOptions[] = '-q ' . $this->getCalculatedQuality(); |
|
249 | } |
||
250 | |||
251 | // alpha-quality |
||
252 | 6 | if ($this->options['alpha-quality'] !== 100) { |
|
253 | 6 | $cmdOptions[] = '-alpha_q ' . escapeshellarg($this->options['alpha-quality']); |
|
254 | } |
||
255 | |||
256 | // Losless PNG conversion |
||
257 | 6 | if ($options['encoding'] == 'lossless') { |
|
258 | // No need to add -lossless when near-lossless is used (on version >= 0.5) |
||
259 | 4 | if (($options['near-lossless'] === 100) || ($versionNum < 0.5)) { |
|
260 | 1 | $cmdOptions[] = '-lossless'; |
|
261 | } |
||
262 | } |
||
263 | |||
264 | // Near-lossles |
||
265 | 6 | if ($options['near-lossless'] !== 100) { |
|
266 | 5 | if ($versionNum < 0.5) { |
|
267 | $this->logLn( |
||
268 | 'The near-lossless option is not supported on this (rather old) version of cwebp' . |
||
269 | '- skipping it.', |
||
270 | 'italic' |
||
271 | ); |
||
272 | } else { |
||
273 | // We only let near_lossless have effect when encoding is set to "lossless" |
||
274 | // otherwise encoding=auto would not work as expected |
||
275 | |||
276 | 5 | if ($options['encoding'] == 'lossless') { |
|
277 | 3 | $cmdOptions[] ='-near_lossless ' . $options['near-lossless']; |
|
278 | } else { |
||
279 | 4 | $this->logLn( |
|
280 | 4 | 'The near-lossless option ignored for lossy' |
|
281 | ); |
||
282 | } |
||
283 | } |
||
284 | } |
||
285 | |||
286 | 6 | if ($options['auto-filter'] === true) { |
|
287 | 1 | $cmdOptions[] = '-af'; |
|
288 | } |
||
289 | |||
290 | // Built-in method option |
||
291 | 6 | $cmdOptions[] = '-m ' . strval($options['method']); |
|
292 | |||
293 | // Built-in low memory option |
||
294 | 6 | if ($options['low-memory']) { |
|
295 | 1 | $cmdOptions[] = '-low_memory'; |
|
296 | } |
||
297 | |||
298 | // command-line-options |
||
299 | 6 | if ($options['command-line-options']) { |
|
300 | /* |
||
301 | In some years, we can use the splat instead (requires PHP 5.6) |
||
302 | array_push( |
||
303 | $cmdOptions, |
||
304 | ...self::escapeShellArgOnCommandLineOptions($options['command-line-options']) |
||
305 | ); |
||
306 | */ |
||
307 | 1 | foreach (self::escapeShellArgOnCommandLineOptions($options['command-line-options']) as $cmdLineOption) { |
|
308 | 1 | array_push($cmdOptions, $cmdLineOption); |
|
309 | } |
||
310 | } |
||
311 | |||
312 | // Source file |
||
313 | 6 | $cmdOptions[] = escapeshellarg($this->source); |
|
314 | |||
315 | // Output |
||
316 | 6 | $cmdOptions[] = '-o ' . escapeshellarg($this->destination); |
|
317 | |||
318 | // Redirect stderr to same place as stdout |
||
319 | // https://www.brianstorti.com/understanding-shell-script-idiom-redirect/ |
||
320 | 6 | $cmdOptions[] = '2>&1'; |
|
321 | |||
322 | 6 | $commandOptions = implode(' ', $cmdOptions); |
|
323 | //$this->logLn('command line options:' . $commandOptions); |
||
324 | |||
325 | 6 | return $commandOptions; |
|
326 | } |
||
327 | |||
328 | /** |
||
329 | * Get path for supplied binary for current OS - and validate hash. |
||
330 | * |
||
331 | * @return array Array of supplied binaries (which actually exists, and where hash validates) |
||
332 | */ |
||
333 | 2 | private function getSuppliedBinaryPathForOS() |
|
334 | { |
||
335 | 2 | $this->log('Checking if we have a supplied precompiled binary for your OS (' . PHP_OS . ')... '); |
|
336 | |||
337 | // Try supplied binary (if available for OS, and hash is correct) |
||
338 | 2 | $options = $this->options; |
|
339 | 2 | if (!isset(self::$suppliedBinariesInfo[PHP_OS])) { |
|
340 | $this->logLn('No we dont - not for that OS'); |
||
341 | return []; |
||
342 | } |
||
343 | |||
344 | 2 | $result = []; |
|
345 | 2 | $files = self::$suppliedBinariesInfo[PHP_OS]; |
|
346 | 2 | if (count($files) == 1) { |
|
347 | $this->logLn('We do.'); |
||
348 | } else { |
||
349 | 2 | $this->logLn('We do. We in fact have ' . count($files)); |
|
350 | } |
||
351 | |||
352 | 2 | foreach ($files as $i => list($file, $hash)) { |
|
353 | //$file = $info[0]; |
||
354 | //$hash = $info[1]; |
||
355 | |||
356 | 2 | $binaryFile = __DIR__ . '/' . $options['rel-path-to-precompiled-binaries'] . '/' . $file; |
|
357 | |||
358 | // Replace "/./" with "/" in path (we could alternatively use realpath) |
||
359 | //$binaryFile = preg_replace('#\/\.\/#', '/', $binaryFile); |
||
360 | // The file should exist, but may have been removed manually. |
||
361 | /* |
||
362 | if (!file_exists($binaryFile)) { |
||
363 | $this->logLn('Supplied binary not found! It ought to be here:' . $binaryFile, 'italic'); |
||
364 | return false; |
||
365 | }*/ |
||
366 | |||
367 | 2 | $realPathResult = realpath($binaryFile); |
|
368 | 2 | if ($realPathResult === false) { |
|
369 | $this->logLn('Supplied binary not found! It ought to be here:' . $binaryFile, 'italic'); |
||
370 | continue; |
||
371 | } |
||
372 | 2 | $binaryFile = $realPathResult; |
|
373 | |||
374 | // File exists, now generate its hash |
||
375 | // hash_file() is normally available, but it is not always |
||
376 | // - https://stackoverflow.com/questions/17382712/php-5-3-20-undefined-function-hash |
||
377 | // If available, validate that hash is correct. |
||
378 | |||
379 | 2 | if (function_exists('hash_file')) { |
|
380 | 2 | $binaryHash = hash_file('sha256', $binaryFile); |
|
381 | |||
382 | 2 | if ($binaryHash != $hash) { |
|
383 | $this->logLn( |
||
384 | 'Binary checksum of supplied binary is invalid! ' . |
||
385 | 'Did you transfer with FTP, but not in binary mode? ' . |
||
386 | 'File:' . $binaryFile . '. ' . |
||
387 | 'Expected checksum: ' . $hash . '. ' . |
||
388 | 'Actual checksum:' . $binaryHash . '.', |
||
389 | 'bold' |
||
390 | ); |
||
391 | continue; |
||
392 | } |
||
393 | } |
||
394 | 2 | $result[] = $binaryFile; |
|
395 | } |
||
396 | 2 | return $result; |
|
397 | } |
||
398 | |||
399 | private function who() |
||
400 | { |
||
401 | exec('whoami', $whoOutput, $whoReturnCode); |
||
402 | if (($whoReturnCode == 0) && (isset($whoOutput[0]))) { |
||
403 | return 'user: "' . $whoOutput[0] . '"'; |
||
404 | } else { |
||
405 | return 'the user that the command was run with'; |
||
406 | } |
||
407 | } |
||
408 | |||
409 | /** |
||
410 | * |
||
411 | * @return string|int Version string (ie "1.0.2") OR return code, in case of failure |
||
412 | */ |
||
413 | 2 | private function detectVersion($binary) |
|
414 | { |
||
415 | 2 | $command = $binary . ' -version'; |
|
416 | 2 | $this->log('- Executing: ' . $command); |
|
417 | 2 | exec($command, $output, $returnCode); |
|
418 | |||
419 | 2 | if ($returnCode == 0) { |
|
420 | 2 | if (isset($output[0])) { |
|
421 | 2 | $this->logLn('. Result: version: *' . $output[0] . '*'); |
|
422 | 2 | return $output[0]; |
|
423 | } |
||
424 | } else { |
||
425 | 1 | $this->log('. Result: '); |
|
426 | 1 | if ($returnCode == 127) { |
|
427 | 1 | $this->logLn('*Exec failed* (the cwebp binary was not found at path: ' . $binary. ')'); |
|
428 | } else { |
||
429 | if ($returnCode == 126) { |
||
430 | $this->logLn( |
||
431 | '*Exec failed*. ' . |
||
432 | 'Permission denied (' . $this->who() . ' does not have permission to execute that binary)' |
||
433 | ); |
||
434 | } else { |
||
435 | $this->logLn( |
||
436 | '*Exec failed* (return code: ' . $returnCode . ')' |
||
437 | ); |
||
438 | $this->logExecOutput($output); |
||
439 | } |
||
440 | } |
||
441 | 1 | return $returnCode; |
|
442 | } |
||
443 | } |
||
444 | |||
445 | /** |
||
446 | * Check versions for an array of binaries. |
||
447 | * |
||
448 | * @return array the "detected" key holds working binaries and their version numbers, the |
||
449 | * the "failed" key holds failed binaries and their error codes. |
||
450 | */ |
||
451 | 2 | private function detectVersions($binaries) |
|
452 | { |
||
453 | 2 | $binariesWithVersions = []; |
|
454 | 2 | $binariesWithFailCodes = []; |
|
455 | |||
456 | 2 | foreach ($binaries as $binary) { |
|
457 | 2 | $versionStringOrFailCode = $this->detectVersion($binary); |
|
458 | // $this->logLn($binary . ': ' . $versionString); |
||
459 | 2 | if (gettype($versionStringOrFailCode) == 'string') { |
|
460 | 2 | $binariesWithVersions[$binary] = $versionStringOrFailCode; |
|
461 | } else { |
||
462 | 2 | $binariesWithFailCodes[$binary] = $versionStringOrFailCode; |
|
463 | } |
||
464 | } |
||
465 | 2 | return ['detected' => $binariesWithVersions, 'failed' => $binariesWithFailCodes]; |
|
466 | } |
||
467 | |||
468 | 3 | private function logBinariesFound($binaries) |
|
469 | { |
||
470 | 3 | if (count($binaries) == 0) { |
|
471 | 2 | $this->logLn('Found 0 binaries'); |
|
472 | } else { |
||
473 | 2 | $this->logLn('Found ' . count($binaries) . ' binaries: '); |
|
474 | 2 | foreach ($binaries as $binary) { |
|
475 | 2 | $this->logLn('- ' . $binary); |
|
476 | } |
||
477 | } |
||
478 | 3 | } |
|
479 | |||
480 | 3 | private function logDiscoverAction($optionName, $description) |
|
481 | { |
||
482 | 3 | if ($this->options[$optionName]) { |
|
483 | 3 | $this->logLn( |
|
484 | 3 | 'Discovering binaries ' . $description . ' ' . |
|
485 | 3 | '(to skip this step, disable the "' . $optionName . '" option)' |
|
486 | ); |
||
487 | } else { |
||
488 | 2 | $this->logLn( |
|
489 | 2 | 'Skipped discovering binaries ' . $description . ' ' . |
|
490 | 2 | '(enable "' . $optionName . '" if you do not want to skip that step)' |
|
491 | ); |
||
492 | } |
||
493 | 3 | } |
|
494 | |||
495 | 3 | private function discoverCwebpBinaries() |
|
496 | { |
||
497 | 3 | $this->logLn( |
|
498 | 3 | 'Looking for cwebp binaries.' |
|
499 | ); |
||
500 | 3 | $binaries = []; |
|
501 | |||
502 | 3 | if (defined('WEBPCONVERT_CWEBP_PATH')) { |
|
503 | $this->logLn('WEBPCONVERT_CWEBP_PATH was defined, so using that path and ignoring any other'); |
||
504 | return [constant('WEBPCONVERT_CWEBP_PATH')]; |
||
505 | } |
||
506 | 3 | if (!empty(getenv('WEBPCONVERT_CWEBP_PATH'))) { |
|
507 | $this->logLn( |
||
508 | 'WEBPCONVERT_CWEBP_PATH environment variable was set, so using that path and ignoring any other' |
||
509 | ); |
||
510 | return [getenv('WEBPCONVERT_CWEBP_PATH')]; |
||
511 | } |
||
512 | |||
513 | 3 | if ($this->options['try-cwebp']) { |
|
514 | 1 | $this->logLn( |
|
515 | 1 | 'Discovering if a plain cwebp call works (to skip this step, disable the "try-cwebp" option)' |
|
516 | ); |
||
517 | 1 | $result = $this->detectVersion('cwebp'); |
|
518 | 1 | if (gettype($result) == 'string') { |
|
519 | $this->logLn('We could get the version, so yes, a plain cwebp call works'); |
||
520 | $binaries[] = 'cwebp'; |
||
521 | } else { |
||
522 | 1 | $this->logLn('Nope a plain cwebp call does not work'); |
|
523 | } |
||
524 | } else { |
||
525 | 2 | $this->logLn( |
|
526 | 'Skipped discovering if a plain cwebp call works' . |
||
527 | 2 | ' (enable the "try-cwebp" option if you do not want to skip that step)' |
|
528 | ); |
||
529 | } |
||
530 | |||
531 | // try-discovering-cwebp |
||
532 | 3 | $this->logDiscoverAction('try-discovering-cwebp', 'using "which -a cwebp" command.'); |
|
533 | 3 | if ($this->options['try-discovering-cwebp']) { |
|
534 | 1 | $moreBinaries = BinaryDiscovery::discoverInstalledBinaries('cwebp'); |
|
535 | 1 | $this->logBinariesFound($moreBinaries); |
|
536 | 1 | $binaries = array_merge($binaries, $moreBinaries); |
|
537 | } |
||
538 | |||
539 | // 'try-common-system-paths' |
||
540 | 3 | $this->logDiscoverAction('try-common-system-paths', 'by peeking in common system paths'); |
|
541 | 3 | if ($this->options['try-common-system-paths']) { |
|
542 | 2 | $moreBinaries = BinaryDiscovery::discoverInCommonSystemPaths('cwebp'); |
|
543 | 2 | $this->logBinariesFound($moreBinaries); |
|
544 | 2 | $binaries = array_merge($binaries, $moreBinaries); |
|
545 | } |
||
546 | |||
547 | // try-supplied-binary-for-os |
||
548 | 3 | $this->logDiscoverAction('try-supplied-binary-for-os', 'which are distributed with the webp-convert library'); |
|
549 | 3 | if ($this->options['try-supplied-binary-for-os']) { |
|
550 | 2 | $moreBinaries = $this->getSuppliedBinaryPathForOS(); |
|
551 | 2 | $this->logBinariesFound($moreBinaries); |
|
552 | 2 | $binaries = array_merge($binaries, $moreBinaries); |
|
553 | } |
||
554 | |||
555 | 3 | return $binaries = array_values(array_unique($binaries)); |
|
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
556 | } |
||
557 | |||
558 | /** |
||
559 | * Try executing a cwebp binary (or command, like: "cwebp") |
||
560 | * |
||
561 | * @param string $binary |
||
562 | * @param string $version Version of cwebp (ie "1.0.3") |
||
563 | * @param boolean $useNice Whether to use "nice" command or not |
||
564 | * |
||
565 | * @return boolean success or not. |
||
566 | */ |
||
567 | 2 | private function tryCwebpBinary($binary, $version, $useNice) |
|
568 | { |
||
569 | |||
570 | //$this->logLn('Trying binary: ' . $binary); |
||
571 | 2 | $commandOptions = $this->createCommandLineOptions($version); |
|
572 | |||
573 | 2 | $returnCode = $this->executeBinary($binary, $commandOptions, $useNice); |
|
574 | 2 | if ($returnCode == 0) { |
|
575 | // It has happened that even with return code 0, there was no file at destination. |
||
576 | 2 | if (!file_exists($this->destination)) { |
|
577 | $this->logLn('executing cweb returned success code - but no file was found at destination!'); |
||
578 | return false; |
||
579 | } else { |
||
580 | 2 | $this->logLn('Success'); |
|
581 | 2 | return true; |
|
582 | } |
||
583 | } else { |
||
584 | $this->logLn( |
||
585 | 'Exec failed (return code: ' . $returnCode . ')' |
||
586 | ); |
||
587 | return false; |
||
588 | } |
||
589 | } |
||
590 | |||
591 | /** |
||
592 | * Helper for composing an error message when no converters are working. |
||
593 | * |
||
594 | * @param array $versions The array which we get from calling ::detectVersions($binaries) |
||
595 | * @return string An informative and to the point error message. |
||
596 | */ |
||
597 | private function composeMeaningfullErrorMessageNoVersionsWorking($versions) |
||
598 | { |
||
599 | |||
600 | // PS: array_values() is used to reindex |
||
601 | $uniqueFailCodes = array_values(array_unique(array_values($versions['failed']))); |
||
602 | $justOne = (count($versions['failed']) == 1); |
||
603 | |||
604 | if (count($uniqueFailCodes) == 1) { |
||
605 | if ($uniqueFailCodes[0] == 127) { |
||
606 | return 'No cwebp binaries located. Check the conversion log for details.'; |
||
607 | } |
||
608 | } |
||
609 | // If there are more failures than 127, the 127 failures are unintesting. |
||
610 | // It is to be expected that some of the common system paths does not contain a cwebp. |
||
611 | $uniqueFailCodesBesides127 = array_values(array_diff($uniqueFailCodes, [127])); |
||
612 | |||
613 | if (count($uniqueFailCodesBesides127) == 1) { |
||
614 | if ($uniqueFailCodesBesides127[0] == 126) { |
||
615 | return 'No cwebp binaries could be executed (permission denied for ' . $this->who() . ').'; |
||
616 | } |
||
617 | } |
||
618 | |||
619 | $errorMsg = ''; |
||
620 | if ($justOne) { |
||
621 | $errorMsg .= 'The cwebp file found cannot be can be executed '; |
||
622 | } else { |
||
623 | $errorMsg .= 'None of the cwebp files can be executed '; |
||
624 | } |
||
625 | if (count($uniqueFailCodesBesides127) == 1) { |
||
626 | $errorMsg .= '(failure code: ' . $uniqueFailCodesBesides127[0] . ')'; |
||
627 | } else { |
||
628 | $errorMsg .= '(failure codes: ' . implode(', ', $uniqueFailCodesBesides127) . ')'; |
||
629 | } |
||
630 | return $errorMsg; |
||
631 | } |
||
632 | |||
633 | 3 | protected function doActualConvert() |
|
634 | { |
||
635 | 3 | $binaries = $this->discoverCwebpBinaries(); |
|
636 | 3 | if (count($binaries) == 0) { |
|
637 | 1 | $this->logLn('No cwebp binaries found!'); |
|
638 | |||
639 | $discoverOptions = [ |
||
640 | 1 | 'try-supplied-binary-for-os', |
|
641 | 'try-common-system-paths', |
||
642 | 'try-cwebp', |
||
643 | 'try-discovering-cwebp' |
||
644 | ]; |
||
645 | 1 | $disabledDiscoverOptions = []; |
|
646 | 1 | foreach ($discoverOptions as $discoverOption) { |
|
647 | 1 | if (!$this->options[$discoverOption]) { |
|
648 | 1 | $disabledDiscoverOptions[] = $discoverOption; |
|
649 | } |
||
650 | } |
||
651 | 1 | if (count($disabledDiscoverOptions) == 0) { |
|
652 | throw new SystemRequirementsNotMetException( |
||
653 | 'No cwebp binaries found.' |
||
654 | ); |
||
655 | } else { |
||
656 | 1 | throw new SystemRequirementsNotMetException( |
|
657 | 'No cwebp binaries found. Try enabling the "' . |
||
658 | 1 | implode('" option or the "', $disabledDiscoverOptions) . '" option.' |
|
659 | ); |
||
660 | } |
||
661 | } |
||
662 | 2 | $this->logLn( |
|
663 | 2 | 'Detecting versions of the cwebp binaries found' |
|
664 | ); |
||
665 | 2 | $versions = $this->detectVersions($binaries); |
|
666 | |||
667 | 2 | $binaryVersions = $versions['detected']; |
|
668 | 2 | if (count($binaryVersions) == 0) { |
|
669 | // No working cwebp binaries found. |
||
670 | |||
671 | throw new SystemRequirementsNotMetException( |
||
672 | $this->composeMeaningfullErrorMessageNoVersionsWorking($versions) |
||
673 | ); |
||
674 | } |
||
675 | |||
676 | // Sort binaries so those with highest numbers comes first |
||
677 | 2 | arsort($binaryVersions); |
|
678 | 2 | $this->logLn( |
|
679 | 2 | 'Binaries ordered by version number.' |
|
680 | ); |
||
681 | 2 | foreach ($binaryVersions as $binary => $version) { |
|
682 | 2 | $this->logLn('- ' . $binary . ': (version: ' . $version .')'); |
|
683 | } |
||
684 | |||
685 | // Execute! |
||
686 | 2 | $this->logLn( |
|
687 | 2 | 'Trying the first of these. If that should fail (it should not), the next will be tried and so on.' |
|
688 | ); |
||
689 | 2 | $useNice = (($this->options['use-nice']) && self::hasNiceSupport()); |
|
690 | 2 | $success = false; |
|
691 | 2 | foreach ($binaryVersions as $binary => $version) { |
|
692 | 2 | if ($this->tryCwebpBinary($binary, $version, $useNice)) { |
|
693 | 2 | $success = true; |
|
694 | 2 | break; |
|
695 | } |
||
696 | } |
||
697 | |||
698 | // cwebp sets file permissions to 664 but instead .. |
||
699 | // .. $destination's parent folder's permissions should be used (except executable bits) |
||
700 | // (or perhaps the current umask instead? https://www.php.net/umask) |
||
701 | |||
702 | 2 | if ($success) { |
|
703 | 2 | $destinationParent = dirname($this->destination); |
|
704 | 2 | $fileStatistics = stat($destinationParent); |
|
705 | 2 | if ($fileStatistics !== false) { |
|
706 | // Apply same permissions as parent folder but strip off the executable bits |
||
707 | 2 | $permissions = $fileStatistics['mode'] & 0000666; |
|
708 | 2 | chmod($this->destination, $permissions); |
|
709 | } |
||
710 | } else { |
||
711 | throw new SystemRequirementsNotMetException('Failed converting. Check the conversion log for details.'); |
||
712 | } |
||
713 | 2 | } |
|
714 | } |
||
715 |