Completed
Push — master ( c79d57...cdca2b )
by Bjørn
02:46
created

src/Convert/Converters/Cwebp.php (1 issue)

Severity
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
The assignment to $binaries is dead and can be removed.
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