Completed
Push — master ( 5f5fca...2fa1e0 )
by Bjørn
12:28 queued 02:24
created

Gd::getOptionDefinitionsExtra()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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