Passed
Push — master ( 74ce7c...3af96c )
by Bjørn
03:41
created

Gd::supportsLossless()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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