Completed
Push — master ( 36e2d9...033daa )
by Bjørn
03:15
created

Gd::checkConvertability()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

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