PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.7.0
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.7.0
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / rosell-dk / webp-convert / src / Convert / Converters / Cwebp.php

Cwebp.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.7.0, at vendor/rosell-dk/webp-convert/src/Convert/Converters/Cwebp.php

981 lines 37.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WebPConvert\Convert\Converters;
4
5 use ExecWithFallback\ExecWithFallback;
6 use LocateBinaries\LocateBinaries;
7
8 use WebPConvert\Convert\Converters\AbstractConverter;
9 use WebPConvert\Convert\Converters\BaseTraits\WarningLoggerTrait;
10 use WebPConvert\Convert\Converters\ConverterTraits\EncodingAutoTrait;
11 use WebPConvert\Convert\Converters\ConverterTraits\ExecTrait;
12 use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
13 use WebPConvert\Convert\Exceptions\ConversionFailedException;
14 use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperationalException;
15 use WebPConvert\Options\OptionFactory;
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 /**
36 * Get the options unique for this converter
37 *
38 * @return array Array of options
39 */
40 public function getUniqueOptions($imageType)
41 {
42 $binariesForOS = [];
43 if (isset(self::$suppliedBinariesInfo[PHP_OS])) {
44 foreach (self::$suppliedBinariesInfo[PHP_OS] as $i => list($file, $hash, $version)) {
45 $binariesForOS[] = $file;
46 }
47 }
48
49 return OptionFactory::createOptions([
50 self::niceOption(),
51 ['try-cwebp', 'boolean', [
52 'title' => 'Try plain cwebp command',
53 'description' =>
54 'If set, the converter will try executing "cwebp -version". In case it succeeds, ' .
55 'and the version is higher than those working cwebps found using other methods, ' .
56 'the conversion will be done by executing this cwebp.',
57 'default' => true,
58 'ui' => [
59 'component' => 'checkbox',
60 'advanced' => true
61 ]
62 ]],
63 ['try-discovering-cwebp', 'boolean', [
64 'title' => 'Try discovering cwebp binary',
65 'description' =>
66 'If set, the converter will try to discover installed cwebp binaries using a "which -a cwebp" ' .
67 'command, or in case that fails, a "whereis -b cwebp" command. These commands will find ' .
68 'cwebp binaries residing in PATH',
69 'default' => true,
70 'ui' => [
71 'component' => 'checkbox',
72 'advanced' => true
73 ]
74 ]],
75 ['try-common-system-paths', 'boolean', [
76 'title' => 'Try locating cwebp in common system paths',
77 'description' =>
78 'If set, the converter will look for a cwebp binaries residing in common system locations ' .
79 'such as "/usr/bin/cwebp". If such exist, it is assumed that they are valid cwebp binaries. ' .
80 'A version check will be run on the binaries found (they are executed with the "-version" flag. ' .
81 'The cwebp with the highest version found using this method and the other enabled methods will ' .
82 'be used for the actual conversion.' .
83 'Note: All methods for discovering cwebp binaries are per default enabled. You can save a few ' .
84 'microseconds by disabling some, but it is probably not worth it, as your ' .
85 'setup will then become less resilient to system changes.',
86 'default' => true,
87 'ui' => [
88 'component' => 'checkbox',
89 'advanced' => true
90 ]
91 ]],
92 ['try-supplied-binary-for-os', 'boolean', [
93 'title' => 'Try precompiled cwebp binaries',
94 'description' =>
95 'If set, the converter will try use a precompiled cwebp binary that comes with webp-convert. ' .
96 'But only if it has a higher version that those found by other methods. As the library knows ' .
97 'the versions of its bundled binaries, no additional time is spent executing them with the ' .
98 '"-version" parameter. The binaries are hash-checked before executed. ' .
99 'The library btw. comes with several versions of precompiled cwebps because they have different ' .
100 'dependencies - some works on some systems and others on others.',
101 'default' => true,
102 'ui' => [
103 'component' => 'checkbox',
104 'advanced' => true
105 ]
106 ]],
107 ['skip-these-precompiled-binaries', 'string', [
108 'title' => 'Skip these precompiled binaries',
109 'description' =>
110 '',
111 'default' => '',
112 'ui' => [
113 'component' => 'multi-select',
114 'advanced' => true,
115 'options' => $binariesForOS,
116 'display' => "option('cwebp-try-supplied-binary-for-os') == true"
117 ]
118
119 ]],
120 ['rel-path-to-precompiled-binaries', 'string', [
121 'title' => 'Rel path to precompiled binaries',
122 'description' =>
123 '',
124 'default' => './Binaries',
125 'ui' => [
126 'component' => '',
127 'advanced' => true,
128 'display' => "option('cwebp-try-supplied-binary-for-os') == true"
129 ],
130 'sensitive' => true
131 ]],
132 ['command-line-options', 'string', [
133 'title' => 'Command line options',
134 'description' =>
135 '',
136 'default' => '',
137 'ui' => [
138 'component' => 'input',
139 'advanced' => true,
140 ]
141
142 ]],
143 ]);
144 }
145
146
147 // OS-specific binaries included in this library, along with hashes
148 // If other binaries are going to be added, notice that the first argument is what PHP_OS returns.
149 // (possible values, see here: https://stackoverflow.com/questions/738823/possible-values-for-php-os)
150 // Got the precompiled binaries here: https://developers.google.com/speed/webp/docs/precompiled
151 // Note when changing binaries:
152 // 1: Do NOT use "." in filename. It causes unzipping to fail on some hosts
153 // 2: Set permission to 775. 755 causes unzipping to fail on some hosts
154 private static $suppliedBinariesInfo = [
155 'WINNT' => [
156 ['cwebp-120-windows-x64.exe', '2849fd06012a9eb311b02a4f8918ae4b16775693bc21e95f4cc6a382eac299f9', '1.2.0'],
157
158 // Keep the 1.1.0 version a while, in case some may have problems with the 1.2.0 version
159 ['cwebp-110-windows-x64.exe', '442682869402f92ad2c8b3186c02b0ea6d6da68d2f908df38bf905b3411eb9fb', '1.1.0'],
160 ],
161 'Darwin' => [
162 ['cwebp-110-mac-10_15', 'bfce742da09b959f9f2929ba808fed9ade25c8025530434b6a47d217a6d2ceb5', '1.1.0'],
163 ],
164 'SunOS' => [
165 // Got this from ewww Wordpress plugin, which unfortunately still uses the old 0.6.0 versions
166 // Can you help me get a 1.0.3 version?
167 ['cwebp-060-solaris', '1febaffbb18e52dc2c524cda9eefd00c6db95bc388732868999c0f48deb73b4f', '0.6.0']
168 ],
169 'FreeBSD' => [
170 // Got this from ewww Wordpress plugin, which unfortunately still uses the old 0.6.0 versions
171 // Can you help me get a 1.0.3 version?
172 ['cwebp-060-fbsd', 'e5cbea11c97fadffe221fdf57c093c19af2737e4bbd2cb3cd5e908de64286573', '0.6.0']
173 ],
174 'Linux' => [
175
176 // PS: Some experience the following error with 1.20:
177 // /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found
178 // (see #278)
179
180 ['cwebp-120-linux-x86-64', 'f1b7dc03e95535a6b65852de07c0404be4dba078af48369f434ee39b2abf8f4e', '1.2.0'],
181
182 // As some experience the an error with 1.20 (see #278), we keep the 1.10
183 ['cwebp-110-linux-x86-64', '1603b07b592876dd9fdaa62b44aead800234c9474ff26dc7dd01bc0f4785c9c6', '1.1.0'],
184
185 // Statically linked executable
186 // It may be that it on some systems works, where the dynamically linked does not (see #196)
187 [
188 'cwebp-103-linux-x86-64-static',
189 'ab96f01b49336da8b976c498528080ff614112d5985da69943b48e0cb1c5228a',
190 '1.0.3'
191 ],
192
193 // Old executable for systems in case all of the above fails
194 ['cwebp-061-linux-x86-64', '916623e5e9183237c851374d969aebdb96e0edc0692ab7937b95ea67dc3b2568', '0.6.1'],
195 ]
196 ];
197
198 /**
199 * Check all hashes of the precompiled binaries.
200 *
201 * This isn't used when converting, but can be used as a startup check.
202 */
203 public static function checkAllHashes()
204 {
205 foreach (self::$suppliedBinariesInfo as $os => $arr) {
206 foreach ($arr as $i => list($filename, $expectedHash)) {
207 $actualHash = hash_file("sha256", __DIR__ . '/Binaries/' . $filename);
208 if ($expectedHash != $actualHash) {
209 throw new \Exception(
210 'Hash for ' . $filename . ' is incorrect! ' .
211 'Checksum is: ' . $actualHash . ', ' .
212 ', but expected: ' . $expectedHash .
213 '. Did you transfer with FTP, but not in binary mode? '
214 );
215 }
216 }
217 }
218 }
219
220 public function checkOperationality()
221 {
222 $this->checkOperationalityExecTrait();
223
224 $options = $this->options;
225 if (!$options['try-supplied-binary-for-os'] &&
226 !$options['try-common-system-paths'] &&
227 !$options['try-cwebp'] &&
228 !$options['try-discovering-cwebp']
229 ) {
230 throw new ConverterNotOperationalException(
231 'Configured to neither try pure cwebp command, ' .
232 'nor look for cweb binaries in common system locations and ' .
233 'nor to use one of the supplied precompiled binaries. ' .
234 'But these are the only ways this converter can convert images. No conversion can be made!'
235 );
236 }
237 }
238
239 private function executeBinary($binary, $commandOptions, $useNice)
240 {
241 //$version = $this->detectVersion($binary);
242
243 // Redirect stderr to same place as stdout with "2>&1"
244 // https://www.brianstorti.com/understanding-shell-script-idiom-redirect/
245
246 $command = ($useNice ? 'nice ' : '') . $binary . ' ' . $commandOptions . ' 2>&1';
247
248 //$logger->logLn('command options:' . $commandOptions);
249 $this->logLn('Trying to convert by executing the following command:');
250 $startExecuteBinaryTime = self::startTimer();
251 ;
252 $this->logLn($command);
253 ExecWithFallback::exec($command, $output, $returnCode);
254 $this->logExecOutput($output);
255 $this->logTimeSpent($startExecuteBinaryTime, 'Executing cwebp binary took: ');
256 $this->logLn('');
257 /*
258 if ($returnCode == 255) {
259 if (isset($output[0])) {
260 // Could be an error like 'Error! Cannot open output file' or 'Error! ...preset... '
261 $this->logLn(print_r($output[0], true));
262 }
263 }*/
264 //$logger->logLn(self::msgForExitCode($returnCode));
265 return intval($returnCode);
266 }
267
268 /**
269 * Use "escapeshellarg()" on all arguments in a commandline string of options
270 *
271 * For example, passing '-sharpness 5 -crop 10 10 40 40 -low_memory' will result in:
272 * [
273 * "-sharpness '5'"
274 * "-crop '10' '10' '40' '40'"
275 * "-low_memory"
276 * ]
277 * @param string $commandLineOptions string which can contain multiple commandline options
278 * @return array Array of command options
279 */
280 private static function escapeShellArgOnCommandLineOptions($commandLineOptions)
281 {
282 if (!ctype_print($commandLineOptions)) {
283 throw new ConversionFailedException(
284 'Non-printable characters are not allowed in the extra command line options'
285 );
286 }
287
288 if (preg_match('#[^a-zA-Z0-9_\s\-]#', $commandLineOptions)) {
289 throw new ConversionFailedException('The extra command line options contains inacceptable characters');
290 }
291
292 $cmdOptions = [];
293 $arr = explode(' -', ' ' . $commandLineOptions);
294 foreach ($arr as $cmdOption) {
295 $pos = strpos($cmdOption, ' ');
296 $cName = '';
297 if (!$pos) {
298 $cName = $cmdOption;
299 if ($cName == '') {
300 continue;
301 }
302 $cmdOptions[] = '-' . $cName;
303 } else {
304 $cName = substr($cmdOption, 0, $pos);
305 $cValues = substr($cmdOption, $pos + 1);
306 $cValuesArr = explode(' ', $cValues);
307 foreach ($cValuesArr as &$cArg) {
308 $cArg = escapeshellarg($cArg);
309 }
310 $cValues = implode(' ', $cValuesArr);
311 $cmdOptions[] = '-' . $cName . ' ' . $cValues;
312 }
313 }
314 return $cmdOptions;
315 }
316
317 /**
318 * Build command line options for a given version of cwebp.
319 *
320 * The "-near_lossless" param is not supported on older versions of cwebp, so skip on those.
321 *
322 * @param string $version Version of cwebp (ie "1.0.3")
323 * @return string
324 */
325 private function createCommandLineOptions($version)
326 {
327
328 $this->logLn('Creating command line options for version: ' . $version);
329
330 // we only need two decimal places for version.
331 // convert to number to make it easier to compare
332 $version = preg_match('#^\d+\.\d+#', $version, $matches);
333 $versionNum = 0;
334 if (isset($matches[0])) {
335 $versionNum = floatval($matches[0]);
336 } else {
337 $this->logLn(
338 'Could not extract version number from the following version string: ' . $version,
339 'bold'
340 );
341 }
342
343 //$this->logLn('version:' . strval($versionNum));
344
345 $options = $this->options;
346
347 $cmdOptions = [];
348
349 // Metadata (all, exif, icc, xmp or none (default))
350 // Comma-separated list of existing metadata to copy from input to output
351 if ($versionNum >= 0.3) {
352 $cmdOptions[] = '-metadata ' . $options['metadata'];
353 } else {
354 $this->logLn('Ignoring metadata option (requires cwebp 0.3)', 'italic');
355 }
356
357 // preset. Appears first in the list as recommended in the docs
358 if (!is_null($options['preset'])) {
359 if ($options['preset'] != 'none') {
360 $cmdOptions[] = '-preset ' . escapeshellarg($options['preset']);
361 }
362 }
363
364 // Size
365 $addedSizeOption = false;
366 if (!is_null($options['size-in-percentage'])) {
367 $sizeSource = filesize($this->source);
368 if ($sizeSource !== false) {
369 $targetSize = floor($sizeSource * $options['size-in-percentage'] / 100);
370 $cmdOptions[] = '-size ' . $targetSize;
371 $addedSizeOption = true;
372 }
373 }
374
375 // quality
376 if (!$addedSizeOption) {
377 $cmdOptions[] = '-q ' . $this->getCalculatedQuality();
378 }
379
380 // alpha-quality
381 if ($this->options['alpha-quality'] !== 100) {
382 $cmdOptions[] = '-alpha_q ' . escapeshellarg($this->options['alpha-quality']);
383 }
384
385 // Losless PNG conversion
386 if ($options['encoding'] == 'lossless') {
387 // No need to add -lossless when near-lossless is used (on version >= 0.5)
388 if (($options['near-lossless'] === 100) || ($versionNum < 0.5)) {
389 $cmdOptions[] = '-lossless';
390 }
391 }
392
393 // Near-lossles
394 if ($options['near-lossless'] !== 100) {
395 if ($versionNum < 0.5) {
396 $this->logLn('Ignoring near-lossless option (requires cwebp 0.5)', 'italic');
397 } else {
398 // The "-near_lossless" flag triggers lossless encoding. We don't want that to happen,
399 // we want the "encoding" option to be respected, and we need it to be in order for
400 // encoding=auto to work.
401 // So: Only set when "encoding" is set to "lossless"
402 if ($options['encoding'] == 'lossless') {
403 $cmdOptions[] = '-near_lossless ' . $options['near-lossless'];
404 } else {
405 $this->logLn(
406 'The near-lossless option ignored for lossy'
407 );
408 }
409 }
410 }
411
412 // Autofilter
413 if ($options['auto-filter'] === true) {
414 $cmdOptions[] = '-af';
415 }
416
417 // SharpYUV
418 if ($options['sharp-yuv'] === true) {
419 if ($versionNum >= 0.6) { // #284
420 $cmdOptions[] = '-sharp_yuv';
421 } else {
422 $this->logLn('Ignoring sharp-yuv option (requires cwebp 0.6)', 'italic');
423 }
424 }
425
426
427 // Built-in method option
428 $cmdOptions[] = '-m ' . strval($options['method']);
429
430 // Built-in low memory option
431 if ($options['low-memory']) {
432 $cmdOptions[] = '-low_memory';
433 }
434
435 // command-line-options
436 if ($options['command-line-options']) {
437 /*
438 In some years, we can use the splat instead (requires PHP 5.6)
439 array_push(
440 $cmdOptions,
441 ...self::escapeShellArgOnCommandLineOptions($options['command-line-options'])
442 );
443 */
444 foreach (self::escapeShellArgOnCommandLineOptions($options['command-line-options']) as $cmdLineOption) {
445 array_push($cmdOptions, $cmdLineOption);
446 }
447 }
448
449 // Source file
450 $cmdOptions[] = escapeshellarg($this->source);
451
452 // Output
453 $cmdOptions[] = '-o ' . escapeshellarg($this->destination);
454
455 $commandOptions = implode(' ', $cmdOptions);
456 //$this->logLn('command line options:' . $commandOptions);
457
458 return $commandOptions;
459 }
460
461 private function checkHashForSuppliedBinary($binaryFile, $hash)
462 {
463 // File exists, now generate its hash
464 // hash_file() is normally available, but it is not always
465 // - https://stackoverflow.com/questions/17382712/php-5-3-20-undefined-function-hash
466 // If available, validate that hash is correct.
467
468 if (function_exists('hash_file')) {
469 $this->logLn(
470 'Checking checksum for supplied binary: ' . $binaryFile
471 );
472 $startHashCheckTime = self::startTimer();
473
474 $binaryHash = hash_file('sha256', $binaryFile);
475
476 if ($binaryHash != $hash) {
477 $this->logLn(
478 'Binary checksum of supplied binary is invalid! ' .
479 'Did you transfer with FTP, but not in binary mode? ' .
480 'File:' . $binaryFile . '. ' .
481 'Expected checksum: ' . $hash . '. ' .
482 'Actual checksum:' . $binaryHash . '.',
483 'bold'
484 );
485 return false;
486 ;
487 }
488
489 $this->logTimeSpent($startHashCheckTime, 'Checksum test took: ');
490 }
491 return true;
492 }
493
494 /**
495 * Get supplied binary info for current OS.
496 * paths are made absolute and checked. Missing are removed
497 *
498 * @return array Two arrays.
499 * First array: array of files (absolute paths)
500 * Second array: array of info objects (absolute path, hash and version)
501 */
502 private function getSuppliedBinaryInfoForCurrentOS()
503 {
504 $this->log('Checking if we have a supplied precompiled binary for your OS (' . PHP_OS . ')... ');
505
506 // Try supplied binary (if available for OS, and hash is correct)
507 $options = $this->options;
508 if (!isset(self::$suppliedBinariesInfo[PHP_OS])) {
509 $this->logLn('No we dont - not for that OS');
510 return [];
511 }
512
513 $filesFound = [];
514 $info = [];
515 $files = self::$suppliedBinariesInfo[PHP_OS];
516 if (count($files) == 1) {
517 $this->logLn('We do.');
518 } else {
519 $this->logLn('We do. We in fact have ' . count($files));
520 }
521
522 $skipThese = explode(',', $this->options['skip-these-precompiled-binaries']);
523
524 //$this->logLn('However, skipping' . print_r($skipThese, true));
525
526 foreach ($files as $i => list($file, $hash, $version)) {
527 //$file = $info[0];
528 //$hash = $info[1];
529
530 $binaryFile = __DIR__ . '/' . $options['rel-path-to-precompiled-binaries'] . '/' . $file;
531
532 // Replace "/./" with "/" in path (we could alternatively use realpath)
533 //$binaryFile = preg_replace('#\/\.\/#', '/', $binaryFile);
534 // The file should exist, but may have been removed manually.
535 /*
536 if (!file_exists($binaryFile)) {
537 $this->logLn('Supplied binary not found! It ought to be here:' . $binaryFile, 'italic');
538 return false;
539 }*/
540 if (in_array($file, $skipThese)) {
541 $this->logLn('Skipped: ' . $file . ' (was told to in the "skip-these-precompiled-binaries" option)');
542 continue;
543 }
544
545
546 $realPathResult = realpath($binaryFile);
547 if ($realPathResult === false) {
548 $this->logLn('Supplied binary not found! It ought to be here:' . $binaryFile, 'italic');
549 continue;
550 }
551 $binaryFile = $realPathResult;
552 $filesFound[] = $realPathResult;
553 $info[] = [$realPathResult, $hash, $version, $file];
554 }
555 return [$filesFound, $info];
556 }
557
558 private function who()
559 {
560 ExecWithFallback::exec('whoami 2>&1', $whoOutput, $whoReturnCode);
561 if (($whoReturnCode == 0) && (isset($whoOutput[0]))) {
562 return 'user: "' . $whoOutput[0] . '"';
563 } else {
564 return 'the user that the command was run with';
565 }
566 }
567
568 /**
569 * Detect the version of a cwebp binary.
570 *
571 * @param string $binary The binary to detect version for (path to cwebp or simply "cwebp")
572 *
573 * @return string|int Version string (ie "1.0.2") OR return code, in case of failure
574 */
575 private function detectVersion($binary)
576 {
577 $command = $binary . ' -version 2>&1';
578 $this->log('- Executing: ' . $command);
579 ExecWithFallback::exec($command, $output, $returnCode);
580
581 if ($returnCode == 0) {
582 if (isset($output[0])) {
583 $this->logLn('. Result: version: *' . $output[0] . '*');
584 return $output[0];
585 }
586 } else {
587 $this->log('. Result: ');
588 if ($returnCode == 127) {
589 $this->logLn(
590 '*Exec failed* (the cwebp binary was not found at path: ' . $binary .
591 ', or it had missing library dependencies)'
592 );
593 } else {
594 if ($returnCode == 126) {
595 $this->logLn(
596 '*Exec failed*. ' .
597 'Permission denied (' . $this->who() . ' does not have permission to execute that binary)'
598 );
599 } else {
600 $this->logLn(
601 '*Exec failed* (return code: ' . $returnCode . ')'
602 );
603 $this->logExecOutput($output);
604 }
605 }
606 return $returnCode;
607 }
608 return ''; // Will not happen. Just so phpstan doesn't complain
609 }
610
611 /**
612 * Check versions for an array of binaries.
613 *
614 * @param array $binaries array of binaries to detect the version of
615 *
616 * @return array the "detected" key holds working binaries and their version numbers, the
617 * the "failed" key holds failed binaries and their error codes.
618 */
619 private function detectVersions($binaries)
620 {
621 $binariesWithVersions = [];
622 $binariesWithFailCodes = [];
623
624 foreach ($binaries as $binary) {
625 $versionStringOrFailCode = $this->detectVersion($binary);
626 // $this->logLn($binary . ': ' . $versionString);
627 if (gettype($versionStringOrFailCode) == 'string') {
628 $binariesWithVersions[$binary] = $versionStringOrFailCode;
629 } else {
630 $binariesWithFailCodes[$binary] = $versionStringOrFailCode;
631 }
632 }
633 return ['detected' => $binariesWithVersions, 'failed' => $binariesWithFailCodes];
634 }
635
636 private function logBinariesFound($binaries, $startTime)
637 {
638 if (count($binaries) == 0) {
639 $this->logLn('Found 0 binaries' . self::getTimeStr($startTime));
640 } else {
641 $this->logLn('Found ' . count($binaries) . ' binaries' . self::getTimeStr($startTime));
642 foreach ($binaries as $binary) {
643 $this->logLn('- ' . $binary);
644 }
645 }
646 }
647
648 private function logDiscoverAction($optionName, $description)
649 {
650 if ($this->options[$optionName]) {
651 $this->logLn(
652 'Discovering binaries ' . $description . ' ' .
653 '(to skip this step, disable the "' . $optionName . '" option)'
654 );
655 } else {
656 $this->logLn(
657 'Skipped discovering binaries ' . $description . ' ' .
658 '(enable "' . $optionName . '" if you do not want to skip that step)'
659 );
660 }
661 }
662
663 private static function startTimer()
664 {
665 if (function_exists('microtime')) {
666 return microtime(true);
667 } else {
668 return 0;
669 }
670 }
671
672 private static function readTimer($startTime)
673 {
674 if (function_exists('microtime')) {
675 $endTime = microtime(true);
676 $seconds = ($endTime - $startTime);
677 return round(($seconds * 1000));
678 } else {
679 return 0;
680 }
681 }
682
683 private static function getTimeStr($startTime, $pre = ' (spent ', $post = ')')
684 {
685 if (function_exists('microtime')) {
686 $ms = self::readTimer($startTime);
687 return $pre . $ms . ' ms' . $post;
688 }
689 return '';
690 }
691
692 private function logTimeSpent($startTime, $pre = 'Spent: ')
693 {
694 if (function_exists('microtime')) {
695 $ms = self::readTimer($startTime);
696 $this->logLn($pre . $ms . ' ms');
697 }
698 }
699
700 /**
701 * @return array Two arrays (in an array).
702 * First array: binaries found,
703 * Second array: supplied binaries info for current OS
704 */
705 private function discoverCwebpBinaries()
706 {
707 $this->logLn(
708 'Looking for cwebp binaries.'
709 );
710
711 $startDiscoveryTime = self::startTimer();
712
713 $binaries = [];
714
715 if (defined('WEBPCONVERT_CWEBP_PATH')) {
716 $this->logLn('WEBPCONVERT_CWEBP_PATH was defined, so using that path and ignoring any other');
717 return [[constant('WEBPCONVERT_CWEBP_PATH')],[[], []]];
718 }
719 if (!empty(getenv('WEBPCONVERT_CWEBP_PATH'))) {
720 $this->logLn(
721 'WEBPCONVERT_CWEBP_PATH environment variable was set, so using that path and ignoring any other'
722 );
723 return [[getenv('WEBPCONVERT_CWEBP_PATH')],[[], []]];
724 }
725
726 if ($this->options['try-cwebp']) {
727 $startTime = self::startTimer();
728 $this->logLn(
729 'Discovering if a plain cwebp call works (to skip this step, disable the "try-cwebp" option)'
730 );
731 $result = $this->detectVersion('cwebp');
732 if (gettype($result) == 'string') {
733 $this->logLn('We could get the version, so yes, a plain cwebp call works ' .
734 '(spent ' . self::readTimer($startTime) . ' ms)');
735 $binaries[] = 'cwebp';
736 } else {
737 $this->logLn('Nope a plain cwebp call does not work' . self::getTimeStr($startTime));
738 }
739 } else {
740 $this->logLn(
741 'Skipped discovering if a plain cwebp call works' .
742 ' (enable the "try-cwebp" option if you do not want to skip that step)'
743 );
744 }
745
746 // try-discovering-cwebp
747 $startTime = self::startTimer();
748 $this->logDiscoverAction('try-discovering-cwebp', 'using "which -a cwebp" command.');
749 if ($this->options['try-discovering-cwebp']) {
750 $moreBinaries = LocateBinaries::locateInstalledBinaries('cwebp');
751 $this->logBinariesFound($moreBinaries, $startTime);
752 $binaries = array_merge($binaries, $moreBinaries);
753 }
754
755 // 'try-common-system-paths'
756 $startTime = self::startTimer();
757 $this->logDiscoverAction('try-common-system-paths', 'by peeking in common system paths');
758 if ($this->options['try-common-system-paths']) {
759 $moreBinaries = LocateBinaries::locateInCommonSystemPaths('cwebp');
760 $this->logBinariesFound($moreBinaries, $startTime);
761 $binaries = array_merge($binaries, $moreBinaries);
762 }
763
764 // try-supplied-binary-for-os
765 $suppliedBinariesInfo = [[], []];
766 $startTime = self::startTimer();
767 $this->logDiscoverAction('try-supplied-binary-for-os', 'which are distributed with the webp-convert library');
768 if ($this->options['try-supplied-binary-for-os']) {
769 $suppliedBinariesInfo = $this->getSuppliedBinaryInfoForCurrentOS();
770 $moreBinaries = $suppliedBinariesInfo[0];
771 $this->logBinariesFound($moreBinaries, $startTime);
772 //$binaries = array_merge($binaries, $moreBinaries);
773 }
774
775 $this->logTimeSpent($startDiscoveryTime, 'Discovering cwebp binaries took: ');
776 $this->logLn('');
777
778 return [array_values(array_unique($binaries)), $suppliedBinariesInfo];
779 }
780
781 /**
782 * Try executing a cwebp binary (or command, like: "cwebp")
783 *
784 * @param string $binary
785 * @param string $version Version of cwebp (ie "1.0.3")
786 * @param boolean $useNice Whether to use "nice" command or not
787 *
788 * @return boolean success or not.
789 */
790 private function tryCwebpBinary($binary, $version, $useNice)
791 {
792
793 //$this->logLn('Trying binary: ' . $binary);
794 $commandOptions = $this->createCommandLineOptions($version);
795
796 $returnCode = $this->executeBinary($binary, $commandOptions, $useNice);
797 if ($returnCode == 0) {
798 // It has happened that even with return code 0, there was no file at destination.
799 if (!file_exists($this->destination)) {
800 $this->logLn('executing cweb returned success code - but no file was found at destination!');
801 return false;
802 } else {
803 $this->logLn('Success');
804 return true;
805 }
806 } else {
807 $this->logLn(
808 'Exec failed (return code: ' . $returnCode . ')'
809 );
810 return false;
811 }
812 }
813
814 /**
815 * Helper for composing an error message when no converters are working.
816 *
817 * @param array $versions The array which we get from calling ::detectVersions($binaries)
818 * @return string An informative and to the point error message.
819 */
820 private function composeMeaningfullErrorMessageNoVersionsWorking($versions)
821 {
822 // TODO: Take "supplied" into account
823
824 // PS: array_values() is used to reindex
825 $uniqueFailCodes = array_values(array_unique(array_values($versions['failed'])));
826 $justOne = (count($versions['failed']) == 1);
827
828 if (count($uniqueFailCodes) == 1) {
829 if ($uniqueFailCodes[0] == 127) {
830 return 'No cwebp binaries located. Check the conversion log for details.';
831 }
832 }
833 // If there are more failures than 127, the 127 failures are unintesting.
834 // It is to be expected that some of the common system paths does not contain a cwebp.
835 $uniqueFailCodesBesides127 = array_values(array_diff($uniqueFailCodes, [127]));
836
837 if (count($uniqueFailCodesBesides127) == 1) {
838 if ($uniqueFailCodesBesides127[0] == 126) {
839 return 'No cwebp binaries could be executed (permission denied for ' . $this->who() . ').';
840 }
841 }
842
843 $errorMsg = '';
844 if ($justOne) {
845 $errorMsg .= 'The cwebp file found cannot be can be executed ';
846 } else {
847 $errorMsg .= 'None of the cwebp files can be executed ';
848 }
849 if (count($uniqueFailCodesBesides127) == 1) {
850 $errorMsg .= '(failure code: ' . $uniqueFailCodesBesides127[0] . ')';
851 } else {
852 $errorMsg .= '(failure codes: ' . implode(', ', $uniqueFailCodesBesides127) . ')';
853 }
854 return $errorMsg;
855 }
856
857 protected function doActualConvert()
858 {
859 list($foundBinaries, $suppliedBinariesInfo) = $this->discoverCwebpBinaries();
860 $suppliedBinaries = $suppliedBinariesInfo[0];
861 $allBinaries = array_merge($foundBinaries, $suppliedBinaries);
862
863 //$binaries = $this->discoverCwebpBinaries();
864 if (count($allBinaries) == 0) {
865 $this->logLn('No cwebp binaries found!');
866
867 $discoverOptions = [
868 'try-supplied-binary-for-os',
869 'try-common-system-paths',
870 'try-cwebp',
871 'try-discovering-cwebp'
872 ];
873 $disabledDiscoverOptions = [];
874 foreach ($discoverOptions as $discoverOption) {
875 if (!$this->options[$discoverOption]) {
876 $disabledDiscoverOptions[] = $discoverOption;
877 }
878 }
879 if (count($disabledDiscoverOptions) == 0) {
880 throw new SystemRequirementsNotMetException(
881 'No cwebp binaries found.'
882 );
883 } else {
884 throw new SystemRequirementsNotMetException(
885 'No cwebp binaries found. Try enabling the "' .
886 implode('" option or the "', $disabledDiscoverOptions) . '" option.'
887 );
888 }
889 }
890
891 $detectedVersions = [];
892 if (count($foundBinaries) > 0) {
893 $this->logLn(
894 'Detecting versions of the cwebp binaries found' .
895 (count($suppliedBinaries) > 0 ? ' (except supplied binaries)' : '.')
896 );
897 $startDetectionTime = self::startTimer();
898 $versions = $this->detectVersions($foundBinaries);
899 $detectedVersions = $versions['detected'];
900
901 $this->logTimeSpent($startDetectionTime, 'Detecting versions took: ');
902 }
903
904 //$suppliedVersions = [];
905 $suppliedBinariesHash = [];
906 $suppliedBinariesFilename = [];
907
908 $binaryVersions = $detectedVersions;
909 foreach ($suppliedBinariesInfo[1] as list($path, $hash, $version, $filename)) {
910 $binaryVersions[$path] = $version;
911 $suppliedBinariesHash[$path] = $hash;
912 $suppliedBinariesFilename[$path] = $filename;
913 }
914
915 //$binaryVersions = array_merge($detectedVersions, $suppliedBinariesInfo);
916
917 // TODO: reimplement
918 /*
919 $versions['supplied'] = $suppliedBinariesInfo;
920
921 $binaryVersions = $versions['detected'];
922 if ((count($binaryVersions) == 0) && (count($suppliedBinaries) == 0)) {
923 // No working cwebp binaries found, no supplied binaries found
924
925 throw new SystemRequirementsNotMetException(
926 $this->composeMeaningfullErrorMessageNoVersionsWorking($versions)
927 );
928 }*/
929
930 // Sort binaries so those with highest numbers comes first
931 arsort($binaryVersions);
932 $this->logLn(
933 'Binaries ordered by version number.'
934 );
935 foreach ($binaryVersions as $binary => $version) {
936 $this->logLn('- ' . $binary . ': (version: ' . $version . ')');
937 }
938
939 // Execute!
940 $this->logLn(
941 'Starting conversion, using the first of these. If that should fail, ' .
942 'the next will be tried and so on.'
943 );
944 $useNice = ($this->options['use-nice'] && $this->checkNiceSupport());
945
946 $success = false;
947 foreach ($binaryVersions as $binary => $version) {
948 if (isset($suppliedBinariesHash[$binary])) {
949 if (!$this->checkHashForSuppliedBinary($binary, $suppliedBinariesHash[$binary])) {
950 continue;
951 }
952 }
953 if ($this->tryCwebpBinary($binary, $version, $useNice)) {
954 $success = true;
955 break;
956 } else {
957 if (isset($suppliedBinariesFilename[$binary])) {
958 $this->logLn(
959 'Note: You can prevent trying this precompiled binary, by setting the ' .
960 '"skip-these-precompiled-binaries" option to "' . $suppliedBinariesFilename[$binary] . '"'
961 );
962 }
963 }
964 }
965
966 // cwebp sets file permissions to 664 but instead ..
967 // .. $this->source file permissions should be used
968
969 if ($success) {
970 $fileStatistics = stat($this->source);
971 if ($fileStatistics !== false) {
972 // Apply same permissions as source file, but strip off the executable bits
973 $permissions = $fileStatistics['mode'] & 0000666;
974 chmod($this->destination, $permissions);
975 }
976 } else {
977 throw new SystemRequirementsNotMetException('Failed converting. Check the conversion log for details.');
978 }
979 }
980 }
981