Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.63% covered (warning)
59.63%
96 / 161
7.14% covered (danger)
7.14%
1 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Gd
59.63% covered (warning)
59.63%
96 / 161
7.14% covered (danger)
7.14%
1 / 14
362.40
0.00% covered (danger)
0.00%
0 / 1
 supportsLossless
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUnsupportedDefaultOptions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkOperationality
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
5.58
 checkConvertability
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 functionsExist
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 makeTrueColorUsingWorkaround
83.33% covered (warning)
83.33%
15 / 18
0.00% covered (danger)
0.00%
0 / 1
9.37
 makeTrueColor
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 createImageResource
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
5.31
 tryToMakeTrueColorIfNot
83.33% covered (warning)
83.33%
15 / 18
0.00% covered (danger)
0.00%
0 / 1
5.12
 trySettingAlphaBlending
53.85% covered (warning)
53.85%
7 / 13
0.00% covered (danger)
0.00%
0 / 1
7.46
 errorHandlerWhileCreatingWebP
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 destroyAndRemove
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 tryConverting
36.17% covered (danger)
36.17%
17 / 47
0.00% covered (danger)
0.00%
0 / 1
56.95
 doActualConvert
61.90% covered (warning)
61.90%
13 / 21
0.00% covered (danger)
0.00%
0 / 1
15.53
1<?php
2
3namespace WebPConvert\Convert\Converters;
4
5use WebPConvert\Convert\Converters\AbstractConverter;
6use WebPConvert\Convert\Exceptions\ConversionFailed\ConverterNotOperational\SystemRequirementsNotMetException;
7use WebPConvert\Convert\Exceptions\ConversionFailed\InvalidInputException;
8use 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 */
17class 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}