Test Failed
Push — master ( 975525...307699 )
by Bjørn
02:39
created

Gd::errorHandlerWhileCreatingWebP()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 4
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
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 5
            'use-nice'
33
        ];
34 5
    }
35 1
36
    private $errorMessageWhileCreating = '';
37
    private $errorNumberWhileCreating;
38 4
39 4
    /**
40 4
     * Check (general) operationality of Gd converter.
41
     *
42
     * @throws SystemRequirementsNotMetException  if system requirements are not met
43
     */
44
    public function checkOperationality()
45
    {
46
        if (!extension_loaded('gd')) {
47
            throw new SystemRequirementsNotMetException('Required Gd extension is not available.');
48
        }
49
50 3
        if (!function_exists('imagewebp')) {
51
            throw new SystemRequirementsNotMetException(
52 3
                'Gd has been compiled without webp support.'
53
            );
54 3
        }
55 2
    }
56 1
57 1
    /**
58
     * Check if specific file is convertable with current converter / converter settings.
59
     *
60 1
     * @throws SystemRequirementsNotMetException  if Gd has been compiled without support for image type
61
     */
62 1
    public function checkConvertability()
63 1
    {
64 1
        $mimeType = $this->getMimeTypeOfSource();
65 1
        switch ($mimeType) {
66
            case 'image/png':
67
                if (!function_exists('imagecreatefrompng')) {
68
                    throw new SystemRequirementsNotMetException(
69 1
                        'Gd has been compiled without PNG support and can therefore not convert this PNG image.'
70
                    );
71
                }
72
                break;
73
74
            case 'image/jpeg':
75
                if (!function_exists('imagecreatefromjpeg')) {
76
                    throw new SystemRequirementsNotMetException(
77
                        'Gd has been compiled without Jpeg support and can therefore not convert this jpeg image.'
78
                    );
79
                }
80
        }
81
    }
82
83
    /**
84
     * Find out if all functions exists.
85
     *
86
     * @return boolean
87
     */
88
    private static function functionsExist($functionNamesArr)
89
    {
90
        foreach ($functionNamesArr as $functionName) {
91
            if (!function_exists($functionName)) {
92
                return false;
93
            }
94
        }
95
        return true;
96
    }
97
98
    /**
99
     * Try to convert image pallette to true color on older systems that does not have imagepalettetotruecolor().
100
     *
101
     * The aim is to function as imagepalettetotruecolor, but for older systems.
102
     * So, if the image is already rgb, nothing will be done, and true will be returned
103
     * PS: Got the workaround here: https://secure.php.net/manual/en/function.imagepalettetotruecolor.php
104
     *
105
     * @param  resource  $image
106
     * @return boolean  TRUE if the convertion was complete, or if the source image already is a true color image,
107
     *          otherwise FALSE is returned.
108
     */
109
    private function makeTrueColorUsingWorkaround(&$image)
110
    {
111
        //return $this->makeTrueColor($image);
112
        /*
113
        if (function_exists('imageistruecolor') && imageistruecolor($image)) {
114
            return true;
115
        }*/
116
        if (self::functionsExist(['imagecreatetruecolor', 'imagealphablending', 'imagecolorallocatealpha',
117
                'imagefilledrectangle', 'imagecopy', 'imagedestroy', 'imagesx', 'imagesy'])) {
118
            $dst = imagecreatetruecolor(imagesx($image), imagesy($image));
119
120
            if ($dst === false) {
121
                return false;
122
            }
123
124
            //prevent blending with default black
125
            if (imagealphablending($dst, false) === false) {
126
                return false;
127
            }
128
129
             //change the RGB values if you need, but leave alpha at 127
130
            $transparent = imagecolorallocatealpha($dst, 255, 255, 255, 127);
131
132
            if ($transparent === false) {
133
                return false;
134
            }
135
136
             //simpler than flood fill
137
            if (imagefilledrectangle($dst, 0, 0, imagesx($image), imagesy($image), $transparent) === false) {
138
                return false;
139
            }
140
141
            //restore default blending
142
            if (imagealphablending($dst, true) === false) {
143
                return false;
144
            };
145
146
            if (imagecopy($dst, $image, 0, 0, 0, 0, imagesx($image), imagesy($image)) === false) {
147
                return false;
148
            }
149
            imagedestroy($image);
150
151
            $image = $dst;
152
            return true;
153
        } else {
154
            // The necessary methods for converting color palette are not avalaible
155
            return false;
156
        }
157
    }
158
159
    /**
160
     * Try to convert image pallette to true color.
161
     *
162
     * Try to convert image pallette to true color. If imagepalettetotruecolor() exists, that is used (available from
163
     * PHP >= 5.5.0). Otherwise using workaround found on the net.
164
     *
165
     * @param  resource  $image
166
     * @return boolean  TRUE if the convertion was complete, or if the source image already is a true color image,
167
     *          otherwise FALSE is returned.
168
     */
169
    private function makeTrueColor(&$image)
170
    {
171
        if (function_exists('imagepalettetotruecolor')) {
172
            return imagepalettetotruecolor($image);
173
        } else {
174
            // imagepalettetotruecolor() is not available on this system. Using custom implementation instead
175
            return $this->makeTrueColorUsingWorkaround($image);
176
        }
177
    }
178
179
    /**
180
     * Create Gd image resource from source
181
     *
182
     * @throws  InvalidInputException  if mime type is unsupported or could not be detected
183
     * @throws  ConversionFailedException  if imagecreatefrompng or imagecreatefromjpeg fails
184
     * @return  resource  $image  The created image
185
     */
186
    private function createImageResource()
187
    {
188
        // In case of failure, image will be false
189
190
        $mimeType = $this->getMimeTypeOfSource();
191
192
        if ($mimeType == 'image/png') {
193
            $image = imagecreatefrompng($this->source);
194
            if ($image === false) {
195
                throw new ConversionFailedException(
196
                    'Gd failed when trying to load/create image (imagecreatefrompng() failed)'
197
                );
198
            }
199
            return $image;
200
        }
201
202
        if ($mimeType == 'image/jpeg') {
203
            $image = imagecreatefromjpeg($this->source);
204
            if ($image === false) {
205
                throw new ConversionFailedException(
206
                    'Gd failed when trying to load/create image (imagecreatefromjpeg() failed)'
207
                );
208
            }
209
            return $image;
210
        }
211
212
        /*
213
        throw new InvalidInputException(
214
            'Unsupported mime type:' . $mimeType
215
        );*/
216
    }
217
218
    /**
219
     * Try to make image resource true color if it is not already.
220
     *
221
     * @param  resource  $image  The image to work on
222
     * @return void
223
     */
224
    protected function tryToMakeTrueColorIfNot(&$image)
225
    {
226
        $mustMakeTrueColor = false;
227
        if (function_exists('imageistruecolor')) {
228
            if (imageistruecolor($image)) {
229
                $this->logLn('image is true color');
230
            } else {
231
                $this->logLn('image is not true color');
232
                $mustMakeTrueColor = true;
233
            }
234
        } else {
235
            $this->logLn('It can not be determined if image is true color');
236
            $mustMakeTrueColor = true;
237
        }
238
239
        if ($mustMakeTrueColor) {
240
            $this->logLn('converting color palette to true color');
241
            $success = $this->makeTrueColor($image);
242
            if (!$success) {
243
                $this->logLn(
244
                    'Warning: FAILED converting color palette to true color. ' .
245
                    'Continuing, but this does NOT look good.'
246
                );
247
            }
248
        }
249
    }
250
251
    /**
252
     *
253
     * @param  resource  $image
254
     * @return boolean  true if alpha blending was set successfully, false otherwise
255
     */
256
    protected function trySettingAlphaBlending($image)
257
    {
258
        if (function_exists('imagealphablending')) {
259
            if (!imagealphablending($image, true)) {
260
                $this->logLn('Warning: imagealphablending() failed');
261
                return false;
262
            }
263
        } else {
264
            $this->logLn(
265
                'Warning: imagealphablending() is not available on your system.' .
266
                ' Converting PNGs with transparency might fail on some systems'
267
            );
268
            return false;
269
        }
270
271
        if (function_exists('imagesavealpha')) {
272
            if (!imagesavealpha($image, true)) {
273
                $this->logLn('Warning: imagesavealpha() failed');
274
                return false;
275
            }
276
        } else {
277
            $this->logLn(
278
                'Warning: imagesavealpha() is not available on your system. ' .
279
                'Converting PNGs with transparency might fail on some systems'
280
            );
281
            return false;
282
        }
283
        return true;
284
    }
285
286
    protected function errorHandlerWhileCreatingWebP($errno, $errstr, $errfile, $errline)
287
    {
288
        $this->errorNumberWhileCreating = $errno;
289
        $this->errorMessageWhileCreating = $errstr . ' in ' . $errfile . ', line ' . $errline .
290
            ', PHP ' . PHP_VERSION . ' (' . PHP_OS . ')';
291
        //return false;
292
    }
293
294
    /**
295
     *
296
     * @param  resource  $image
297
     * @return void
298
     */
299
    protected function destroyAndRemove($image)
300
    {
301
        imagedestroy($image);
302
        if (file_exists($this->destination)) {
303
            unlink($this->destination);
304
        }
305
    }
306
307
    /**
308
     *
309
     * @param  resource  $image
310
     * @return void
311
     */
312
    protected function tryConverting($image)
313
    {
314
315
        // Danger zone!
316
        //    Using output buffering to generate image.
317
        //    In this zone, Do NOT do anything that might produce unwanted output
318
        //    Do NOT call $this->logLn
319
        // --------------------------------- (start of danger zone)
320
321
        $addedZeroPadding = false;
322
        set_error_handler(array($this, "errorHandlerWhileCreatingWebP"));
323
324
        // This line may trigger log, so we need to do it BEFORE ob_start() !
325
        $q = $this->getCalculatedQuality();
326
327
        ob_start();
328
329
        //$success = imagewebp($image, $this->destination, $q);
330
        $success = imagewebp($image, null, $q);
331
332
        if (!$success) {
333
            $this->destroyAndRemove($image);
334
            ob_end_clean();
335
            restore_error_handler();
336
            throw new ConversionFailedException(
337
                'Failed creating image. Call to imagewebp() failed.',
338
                $this->errorMessageWhileCreating
339
            );
340
        }
341
342
343
        // The following hack solves an `imagewebp` bug
344
        // See https://stackoverflow.com/questions/30078090/imagewebp-php-creates-corrupted-webp-files
345
        if (ob_get_length() % 2 == 1) {
346
            echo "\0";
347
            $addedZeroPadding = true;
348
        }
349
        $output = ob_get_clean();
350
        restore_error_handler();
351
352
        if ($output == '') {
353
            $this->destroyAndRemove($image);
354
            throw new ConversionFailedException(
355
                'Gd failed: imagewebp() returned empty string'
356
            );
357
        }
358
359
        // --------------------------------- (end of danger zone).
360
361
362
        if ($this->errorMessageWhileCreating != '') {
363
            switch ($this->errorNumberWhileCreating) {
364
                case E_WARNING:
365
                    $this->logLn('An warning was produced during conversion: ' . $this->errorMessageWhileCreating);
366
                    break;
367
                case E_NOTICE:
368
                    $this->logLn('An notice was produced during conversion: ' . $this->errorMessageWhileCreating);
369
                    break;
370
                default:
371
                    $this->destroyAndRemove($image);
372
                    throw new ConversionFailedException(
373
                        'An error was produced during conversion',
374
                        $this->errorMessageWhileCreating
375
                    );
376
                    break;
377
            }
378
        }
379
380
        if ($addedZeroPadding) {
381
            $this->logLn(
382
                'Fixing corrupt webp by adding a zero byte ' .
383
                '(older versions of Gd had a bug, but this hack fixes it)'
384
            );
385
        }
386
387
        $success = file_put_contents($this->destination, $output);
388
389
        if (!$success) {
390
            $this->destroyAndRemove($image);
391
            throw new ConversionFailedException(
392
                'Gd failed when trying to save the image. Check file permissions!'
393
            );
394
        }
395
396
        /*
397
        Previous code was much simpler, but on a system, the hack was not activated (a file with uneven number of bytes
398
        was created). This is puzzeling. And the old code did not provide any insights.
399
        Also, perhaps having two subsequent writes to the same file could perhaps cause a problem.
400
        In the new code, there is only one write.
401
        However, a bad thing about the new code is that the entire webp file is read into memory. This might cause
402
        memory overflow with big files.
403
        Perhaps we should check the filesize of the original and only use the new code when it is smaller than
404
        memory limit set in PHP by a certain factor.
405
        Or perhaps only use the new code on older versions of Gd
406
        https://wordpress.org/support/topic/images-not-seen-on-chrome/#post-11390284
407
408
        Here is the old code:
409
410
        $success = imagewebp($image, $this->destination, $this->getCalculatedQuality());
411
412
        if (!$success) {
413
            throw new ConversionFailedException(
414
                'Gd failed when trying to save the image as webp (call to imagewebp() failed). ' .
415
                'It probably failed writing file. Check file permissions!'
416
            );
417
        }
418
419
420
        // This hack solves an `imagewebp` bug
421
        // See https://stackoverflow.com/questions/30078090/imagewebp-php-creates-corrupted-webp-files
422
        if (filesize($this->destination) % 2 == 1) {
423
            file_put_contents($this->destination, "\0", FILE_APPEND);
424
        }
425
        */
426
    }
427
428
    // Although this method is public, do not call directly.
429
    // You should rather call the static convert() function, defined in AbstractConverter, which
430
    // takes care of preparing stuff before calling doConvert, and validating after.
431
    protected function doActualConvert()
432
    {
433
434
        $this->logLn('GD Version: ' . gd_info()["GD Version"]);
435
436
        // Btw: Check out processWebp here:
437
        // https://github.com/Intervention/image/blob/master/src/Intervention/Image/Gd/Encoder.php
438
439
        // Create image resource
440
        $image = $this->createImageResource();
441
442
        // Try to convert color palette if it is not true color
443
        $this->tryToMakeTrueColorIfNot($image);
444
445
446
        if ($this->getMimeTypeOfSource() == 'image/png') {
447
            // Try to set alpha blending
448
            $this->trySettingAlphaBlending($image);
449
        }
450
451
        // Try to convert it to webp
452
        $this->tryConverting($image);
453
454
        // End of story
455
        imagedestroy($image);
456
    }
457
}
458