Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
59.63% |
96 / 161 |
|
7.14% |
1 / 14 |
CRAP | |
0.00% |
0 / 1 |
Gd | |
59.63% |
96 / 161 |
|
7.14% |
1 / 14 |
362.40 | |
0.00% |
0 / 1 |
supportsLossless | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUnsupportedDefaultOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkOperationality | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
5.58 | |||
checkConvertability | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
functionsExist | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
makeTrueColorUsingWorkaround | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
9.37 | |||
makeTrueColor | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
createImageResource | |
76.92% |
10 / 13 |
|
0.00% |
0 / 1 |
5.31 | |||
tryToMakeTrueColorIfNot | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
5.12 | |||
trySettingAlphaBlending | |
53.85% |
7 / 13 |
|
0.00% |
0 / 1 |
7.46 | |||
errorHandlerWhileCreatingWebP | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
destroyAndRemove | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
tryConverting | |
36.17% |
17 / 47 |
|
0.00% |
0 / 1 |
56.95 | |||
doActualConvert | |
61.90% |
13 / 21 |
|
0.00% |
0 / 1 |
15.53 |
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 <it@rosell.dk> |
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 | } |