PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.3
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.3
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 3.2.3, at vendor/rosell-dk/webp-convert/src/Convert/Converters/Cwebp.php

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