PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.7.1
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.7.1
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 / Gd.php

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

537 lines 20.0 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\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
7 use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInputException;
8 use WebPConvert\Convert\Exceptions\ConversionFailedException;
9
10 /**
11 * Convert images to webp using gd extension.
12 *
13 * @package WebPConvert
14 * @author Bjørn Rosell <[email protected]>
15 * @since Class available since Release 2.0.0
16 */
17 class Gd extends AbstractConverter
18 {
19 public function supportsLossless()
20 {
21 return false;
22 }
23
24 protected function getUnsupportedDefaultOptions()
25 {
26 return [
27 'alpha-quality',
28 'auto-filter',
29 'encoding',
30 'low-memory',
31 'metadata',
32 'method',
33 'near-lossless',
34 'preset',
35 'sharp-yuv',
36 'size-in-percentage',
37 ];
38 }
39
40 private $errorMessageWhileCreating = '';
41 private $errorNumberWhileCreating;
42
43 /**
44 * Check (general) operationality of Gd converter.
45 *
46 * @throws SystemRequirementsNotMetException if system requirements are not met
47 */
48 public function checkOperationality()
49 {
50 if (!extension_loaded('gd')) {
51 throw new SystemRequirementsNotMetException('Required Gd extension is not available.');
52 }
53
54 if (!function_exists('imagewebp')) {
55 throw new SystemRequirementsNotMetException(
56 'Gd has been compiled without webp support.'
57 );
58 }
59
60 if (!function_exists('imagepalettetotruecolor')) {
61 if (!self::functionsExist([
62 'imagecreatetruecolor', 'imagealphablending', 'imagecolorallocatealpha',
63 'imagefilledrectangle', 'imagecopy', 'imagedestroy', 'imagesx', 'imagesy'
64 ])) {
65 throw new SystemRequirementsNotMetException(
66 'Gd cannot convert palette color images to RGB. ' .
67 'Even though it would be possible to convert RGB images to webp with Gd, ' .
68 'we refuse to do it. A partial working converter causes more trouble than ' .
69 'a non-working. To make this converter work, you need the imagepalettetotruecolor() ' .
70 'function to be enabled on your system.'
71 );
72 }
73 }
74 }
75
76 /**
77 * Check if specific file is convertable with current converter / converter settings.
78 *
79 * @throws SystemRequirementsNotMetException if Gd has been compiled without support for image type
80 */
81 public function checkConvertability()
82 {
83 $mimeType = $this->getMimeTypeOfSource();
84 switch ($mimeType) {
85 case 'image/png':
86 if (!function_exists('imagecreatefrompng')) {
87 throw new SystemRequirementsNotMetException(
88 'Gd has been compiled without PNG support and can therefore not convert this PNG image.'
89 );
90 }
91 break;
92
93 case 'image/jpeg':
94 if (!function_exists('imagecreatefromjpeg')) {
95 throw new SystemRequirementsNotMetException(
96 'Gd has been compiled without Jpeg support and can therefore not convert this jpeg image.'
97 );
98 }
99 }
100 }
101
102 /**
103 * Find out if all functions exists.
104 *
105 * @return boolean
106 */
107 private static function functionsExist($functionNamesArr)
108 {
109 foreach ($functionNamesArr as $functionName) {
110 if (!function_exists($functionName)) {
111 return false;
112 }
113 }
114 return true;
115 }
116
117 /**
118 * Try to convert image pallette to true color on older systems that does not have imagepalettetotruecolor().
119 *
120 * The aim is to function as imagepalettetotruecolor, but for older systems.
121 * So, if the image is already rgb, nothing will be done, and true will be returned
122 * PS: Got the workaround here: https://secure.php.net/manual/en/function.imagepalettetotruecolor.php
123 *
124 * @param resource|\GdImage $image
125 * @return boolean TRUE if the convertion was complete, or if the source image already is a true color image,
126 * otherwise FALSE is returned.
127 */
128 private function makeTrueColorUsingWorkaround(&$image)
129 {
130 //return $this->makeTrueColor($image);
131 /*
132 if (function_exists('imageistruecolor') && imageistruecolor($image)) {
133 return true;
134 }*/
135 if (self::functionsExist(['imagecreatetruecolor', 'imagealphablending', 'imagecolorallocatealpha',
136 'imagefilledrectangle', 'imagecopy', 'imagedestroy', 'imagesx', 'imagesy'])) {
137 $dst = imagecreatetruecolor(imagesx($image), imagesy($image));
138
139 if ($dst === false) {
140 return false;
141 }
142
143 $success = false;
144
145 //prevent blending with default black
146 if (imagealphablending($dst, false) !== false) {
147 //change the RGB values if you need, but leave alpha at 127
148 $transparent = imagecolorallocatealpha($dst, 255, 255, 255, 127);
149
150 if ($transparent !== false) {
151 //simpler than flood fill
152 if (imagefilledrectangle($dst, 0, 0, imagesx($image), imagesy($image), $transparent) !== false) {
153 //restore default blending
154 if (imagealphablending($dst, true) !== false) {
155 if (imagecopy($dst, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)) !== false) {
156 $success = true;
157 }
158 };
159 }
160 }
161 }
162 if ($success) {
163 imagedestroy($image);
164 $image = $dst;
165 } else {
166 imagedestroy($dst);
167 }
168 return $success;
169 } else {
170 // The necessary methods for converting color palette are not avalaible
171 return false;
172 }
173 }
174
175 /**
176 * Try to convert image pallette to true color.
177 *
178 * Try to convert image pallette to true color. If imagepalettetotruecolor() exists, that is used (available from
179 * PHP >= 5.5.0). Otherwise using workaround found on the net.
180 *
181 * @param resource|\GdImage $image
182 * @return boolean TRUE if the convertion was complete, or if the source image already is a true color image,
183 * otherwise FALSE is returned.
184 */
185 private function makeTrueColor(&$image)
186 {
187 if (function_exists('imagepalettetotruecolor')) {
188 return imagepalettetotruecolor($image);
189 } else {
190 $this->logLn(
191 'imagepalettetotruecolor() is not available on this system - using custom implementation instead'
192 );
193 return $this->makeTrueColorUsingWorkaround($image);
194 }
195 }
196
197 /**
198 * Create Gd image resource from source
199 *
200 * @throws InvalidInputException if mime type is unsupported or could not be detected
201 * @throws ConversionFailedException if imagecreatefrompng or imagecreatefromjpeg fails
202 * @return resource|\GdImage $image The created image
203 */
204 private function createImageResource()
205 {
206 $mimeType = $this->getMimeTypeOfSource();
207
208 switch ($mimeType) {
209 case 'image/png':
210 $image = imagecreatefrompng($this->source);
211 if ($image === false) {
212 throw new ConversionFailedException(
213 'Gd failed when trying to load/create image (imagecreatefrompng() failed)'
214 );
215 }
216 return $image;
217
218 case 'image/jpeg':
219 $image = imagecreatefromjpeg($this->source);
220 if ($image === false) {
221 throw new ConversionFailedException(
222 'Gd failed when trying to load/create image (imagecreatefromjpeg() failed)'
223 );
224 }
225 return $image;
226 }
227
228 throw new InvalidInputException('Unsupported mime type');
229 }
230
231 /**
232 * Try to make image resource true color if it is not already.
233 *
234 * @param resource|\GdImage $image The image to work on
235 * @return boolean|null True if it is now true color. False if it is NOT true color. null, if we cannot tell
236 */
237 protected function tryToMakeTrueColorIfNot(&$image)
238 {
239 $whatIsItNow = null;
240 $mustMakeTrueColor = false;
241 if (function_exists('imageistruecolor')) {
242 if (imageistruecolor($image)) {
243 $this->logLn('image is true color');
244 $whatIsItNow = true;
245 } else {
246 $this->logLn('image is not true color');
247 $mustMakeTrueColor = true;
248 $whatIsItNow = false;
249 }
250 } else {
251 $this->logLn('It can not be determined if image is true color');
252 $mustMakeTrueColor = true;
253 }
254
255 if ($mustMakeTrueColor) {
256 $this->logLn('converting color palette to true color');
257 $success = $this->makeTrueColor($image);
258 if ($success) {
259 return true;
260 } else {
261 $this->logLn(
262 'FAILED converting color palette to true color. '
263 );
264 }
265 }
266 return $whatIsItNow;
267 }
268
269 /**
270 *
271 * @param resource|\GdImage $image
272 * @return boolean true if alpha blending was set successfully, false otherwise
273 */
274 protected function trySettingAlphaBlending($image)
275 {
276 if (function_exists('imagealphablending')) {
277 // TODO: Should we set second parameter to false instead?
278 // As here: https://www.texelate.co.uk/blog/retaining-png-transparency-with-php-gd
279 // (PS: I have backed up some local changes - to Gd.php, which includes changing that param
280 // to false. But I didn't finish up back then and now I forgot, so need to retest before
281 // changing anything...
282 if (!imagealphablending($image, true)) {
283 $this->logLn('Warning: imagealphablending() failed');
284 return false;
285 }
286 } else {
287 $this->logLn(
288 'Warning: imagealphablending() is not available on your system.' .
289 ' Converting PNGs with transparency might fail on some systems'
290 );
291 return false;
292 }
293
294 if (function_exists('imagesavealpha')) {
295 if (!imagesavealpha($image, true)) {
296 $this->logLn('Warning: imagesavealpha() failed');
297 return false;
298 }
299 } else {
300 $this->logLn(
301 'Warning: imagesavealpha() is not available on your system. ' .
302 'Converting PNGs with transparency might fail on some systems'
303 );
304 return false;
305 }
306 return true;
307 }
308
309 protected function errorHandlerWhileCreatingWebP($errno, $errstr, $errfile, $errline)
310 {
311 $this->errorNumberWhileCreating = $errno;
312 $this->errorMessageWhileCreating = $errstr . ' in ' . $errfile . ', line ' . $errline .
313 ', PHP ' . PHP_VERSION . ' (' . PHP_OS . ')';
314 //return false;
315 }
316
317 /**
318 *
319 * @param resource|\GdImage $image
320 * @return void
321 */
322 protected function destroyAndRemove($image)
323 {
324 imagedestroy($image);
325 if (file_exists($this->destination)) {
326 unlink($this->destination);
327 }
328 }
329
330 /**
331 *
332 * @param resource|\GdImage $image
333 * @return void
334 */
335 protected function tryConverting($image)
336 {
337
338 // Danger zone!
339 // Using output buffering to generate image.
340 // In this zone, Do NOT do anything that might produce unwanted output
341 // Do NOT call $this->logLn
342 // --------------------------------- (start of danger zone)
343
344 $addedZeroPadding = false;
345 set_error_handler(array($this, "errorHandlerWhileCreatingWebP"));
346
347 // This line may trigger log, so we need to do it BEFORE ob_start() !
348 $q = $this->getCalculatedQuality();
349
350 ob_start();
351
352 // Adding this try/catch is perhaps not neccessary.
353 // I'm not certain that the error handler takes care of Throwable errors.
354 // and - sorry - was to lazy to find out right now. So for now: better safe than sorry. #320
355 $error = null;
356 $success = false;
357
358 try {
359 // Beware: This call can throw FATAL on windows (cannot be catched)
360 // This for example happens on palette images
361 $success = imagewebp($image, null, $q);
362 } catch (\Exception $e) {
363 $error = $e;
364 } catch (\Throwable $e) {
365 $error = $e;
366 }
367 if (!is_null($error)) {
368 restore_error_handler();
369 ob_end_clean();
370 $this->destroyAndRemove($image);
371 throw $error;
372 }
373 if (!$success) {
374 $this->destroyAndRemove($image);
375 ob_end_clean();
376 restore_error_handler();
377 throw new ConversionFailedException(
378 'Failed creating image. Call to imagewebp() failed.',
379 $this->errorMessageWhileCreating
380 );
381 }
382
383
384 // The following hack solves an `imagewebp` bug
385 // See https://stackoverflow.com/questions/30078090/imagewebp-php-creates-corrupted-webp-files
386 if (ob_get_length() % 2 == 1) {
387 echo "\0";
388 $addedZeroPadding = true;
389 }
390 $output = ob_get_clean();
391 restore_error_handler();
392
393 if ($output == '') {
394 $this->destroyAndRemove($image);
395 throw new ConversionFailedException(
396 'Gd failed: imagewebp() returned empty string'
397 );
398 }
399
400 // --------------------------------- (end of danger zone).
401
402
403 if ($this->errorMessageWhileCreating != '') {
404 switch ($this->errorNumberWhileCreating) {
405 case E_WARNING:
406 $this->logLn('An warning was produced during conversion: ' . $this->errorMessageWhileCreating);
407 break;
408 case E_NOTICE:
409 $this->logLn('An notice was produced during conversion: ' . $this->errorMessageWhileCreating);
410 break;
411 default:
412 $this->destroyAndRemove($image);
413 throw new ConversionFailedException(
414 'An error was produced during conversion',
415 $this->errorMessageWhileCreating
416 );
417 //break;
418 }
419 }
420
421 if ($addedZeroPadding) {
422 $this->logLn(
423 'Fixing corrupt webp by adding a zero byte ' .
424 '(older versions of Gd had a bug, but this hack fixes it)'
425 );
426 }
427
428 $success = file_put_contents($this->destination, $output);
429
430 if (!$success) {
431 $this->destroyAndRemove($image);
432 throw new ConversionFailedException(
433 'Gd failed when trying to save the image. Check file permissions!'
434 );
435 }
436
437 /*
438 Previous code was much simpler, but on a system, the hack was not activated (a file with uneven number of bytes
439 was created). This is puzzeling. And the old code did not provide any insights.
440 Also, perhaps having two subsequent writes to the same file could perhaps cause a problem.
441 In the new code, there is only one write.
442 However, a bad thing about the new code is that the entire webp file is read into memory. This might cause
443 memory overflow with big files.
444 Perhaps we should check the filesize of the original and only use the new code when it is smaller than
445 memory limit set in PHP by a certain factor.
446 Or perhaps only use the new code on older versions of Gd
447 https://wordpress.org/support/topic/images-not-seen-on-chrome/#post-11390284
448
449 Here is the old code:
450
451 $success = imagewebp($image, $this->destination, $this->getCalculatedQuality());
452
453 if (!$success) {
454 throw new ConversionFailedException(
455 'Gd failed when trying to save the image as webp (call to imagewebp() failed). ' .
456 'It probably failed writing file. Check file permissions!'
457 );
458 }
459
460
461 // This hack solves an `imagewebp` bug
462 // See https://stackoverflow.com/questions/30078090/imagewebp-php-creates-corrupted-webp-files
463 if (filesize($this->destination) % 2 == 1) {
464 file_put_contents($this->destination, "\0", FILE_APPEND);
465 }
466 */
467 }
468
469 // Although this method is public, do not call directly.
470 // You should rather call the static convert() function, defined in AbstractConverter, which
471 // takes care of preparing stuff before calling doConvert, and validating after.
472 protected function doActualConvert()
473 {
474 $versionString = gd_info()["GD Version"];
475 $this->logLn('GD Version: ' . $versionString);
476
477 // Btw: Check out processWebp here:
478 // https://github.com/Intervention/image/blob/master/src/Intervention/Image/Gd/Encoder.php
479
480 // Create image resource
481 $image = $this->createImageResource();
482
483 // Try to convert color palette if it is not true color
484 $isItTrueColorNow = $this->tryToMakeTrueColorIfNot($image);
485 if ($isItTrueColorNow === false) {
486 // our tests shows that converting palette fails on all systems,
487 throw new ConversionFailedException(
488 'Cannot convert image because it is a palette image and the palette image cannot ' .
489 'be converted to RGB (which is required). To convert to RGB, we would need ' .
490 'imagepalettetotruecolor(), which is not available on your system. ' .
491 'Our workaround does not have the neccasary functions for converting to RGB either.'
492 );
493 }
494 if (is_null($isItTrueColorNow)) {
495 $isWindows = preg_match('/^win/i', PHP_OS);
496 $isMacDarwin = preg_match('/^darwin/i', PHP_OS); // actually no problem in PHP 7.4 and 8.0
497 if ($isWindows || $isMacDarwin) {
498 throw new ConversionFailedException(
499 'Cannot convert image because it appears to be a palette image and the palette image ' .
500 'cannot be converted to RGB, as you do not have imagepalettetotruecolor() enabled. ' .
501 'Converting palette on ' .
502 ($isWindows ? 'Windows causes FATAL error' : 'Mac causes halt') .
503 'So we abort now'
504 );
505 }
506 }
507
508 if ($this->getMimeTypeOfSource() == 'image/png') {
509 if (function_exists('version_compare')) {
510 if (version_compare($versionString, "2.1.1", "<=")) {
511 $this->logLn(
512 'BEWARE: Your version of Gd looses the alpha chanel when converting to webp.' .
513 'You should upgrade Gd, use another converter or stop converting PNGs. ' .
514 'See: https://github.com/rosell-dk/webp-convert/issues/238'
515 );
516 } elseif (version_compare($versionString, "2.2.4", "<=")) {
517 $this->logLn(
518 'BEWARE: Older versions of Gd looses the alpha chanel when converting to webp.' .
519 'We have not tested if transparency fails on your (rather old) version of Gd. ' .
520 'Please let us know. ' .
521 'See: https://github.com/rosell-dk/webp-convert/issues/238'
522 );
523 }
524 }
525
526 // Try to set alpha blending
527 $this->trySettingAlphaBlending($image);
528 }
529
530 // Try to convert it to webp
531 $this->tryConverting($image);
532
533 // End of story
534 imagedestroy($image);
535 }
536 }
537