Passed
Push — master ( 1ada49...6f88dc )
by Bjørn
02:29
created

Gd   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 396
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 154
dl 0
loc 396
rs 6.96
c 0
b 0
f 0
wmc 53

12 Methods

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