Passed
Push — master ( 6c41a6...c3c8a7 )
by Bjørn
02:45
created

Gd   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Test Coverage

Coverage 11.36%

Importance

Changes 0
Metric Value
eloc 156
dl 0
loc 429
ccs 20
cts 176
cp 0.1136
rs 7.92
c 0
b 0
f 0
wmc 51

13 Methods

Rating   Name   Duplication   Size   Complexity  
B makeTrueColorUsingWorkaround() 0 47 8
A createImageResource() 0 24 5
B tryConverting() 0 81 9
A destroyAndRemove() 0 5 2
A trySettingAlphaBlending() 0 28 5
A functionsExist() 0 8 3
A checkOperationality() 0 9 3
A makeTrueColor() 0 7 2
A errorHandlerWhileCreatingWebP() 0 5 1
A doActualConvert() 0 25 2
A checkConvertability() 0 16 5
A getOptionDefinitionsExtra() 0 3 1
A tryToMakeTrueColorIfNot() 0 22 5

How to fix   Complexity   

Complex Class

Complex classes like Gd often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Gd, and based on these observations, apply Extract Interface, too.

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