Passed
Push — master ( 77b4d2...47232b )
by Bjørn
02:35
created

Gd::tryToMakeTrueColorIfNot()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 22
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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