Completed
Push — master ( 2a21fa...5cdfb5 )
by Bjørn
03:03
created

Cwebp::doActualConvert()   F

Complexity

Conditions 19
Paths 550

Size

Total Lines 116
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 42.6629

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 19
eloc 66
c 3
b 0
f 0
nc 550
nop 0
dl 0
loc 116
ccs 37
cts 62
cp 0.5968
crap 42.6629
rs 0.9749

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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