Completed
Pull Request — master (#26)
by Chad
01:12
created

Image::resize()   F

Complexity

Conditions 21
Paths 182

Size

Total Lines 131

Duplication

Lines 25
Ratio 19.08 %

Importance

Changes 0
Metric Value
dl 25
loc 131
rs 2.7866
c 0
b 0
f 0
cc 21
nc 182
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace TraderInteractive\Util;
4
5
use Imagick;
6
use InvalidArgumentException;
7
use TraderInteractive\Util;
8
9
final class Image
10
{
11
    /**
12
     * @var string
13
     */
14
    const DEFAULT_COLOR = 'white';
15
16
    /**
17
     * @var bool
18
     */
19
    const DEFAULT_UPSIZE = false;
20
21
    /**
22
     * @var bool
23
     */
24
    const DEFAULT_BESTFIT = false;
25
26
    /**
27
     * @var int
28
     */
29
    const DEFAULT_MAX_WIDTH = 10000;
30
31
    /**
32
     * @var int
33
     */
34
    const DEFAULT_MAX_HEIGHT = 10000;
35
36
    /**
37
     * @var bool
38
     */
39
    const DEFAULT_BLUR_BACKGROUND = false;
40
41
    /**
42
     * @var float
43
     */
44
    const DEFAULT_BLUR_VALUE = 15.0;
45
46
    /**
47
     * @var array
48
     */
49
    const DEFAULT_OPTIONS = [
50
        'color' => self::DEFAULT_COLOR,
51
        'upsize' => self::DEFAULT_UPSIZE,
52
        'bestfit' => self::DEFAULT_BESTFIT,
53
        'maxWidth' => self::DEFAULT_MAX_WIDTH,
54
        'maxHeight' => self::DEFAULT_MAX_HEIGHT,
55
        'blurBackground' => self::DEFAULT_BLUR_BACKGROUND,
56
        'blurValue' => self::DEFAULT_BLUR_VALUE,
57
    ];
58
59
    /**
60
     * @param Imagick $source    The image magick object to resize
61
     * @param int     $boxWidth  The final width of the image.
62
     * @param int     $boxHeight The final height of the image.
63
     * @param array   $options   Options for the resize operation.
64
     *
65
     * @return Imagick
66
     *
67
     * @throws \Exception Thrown if options are invalid.
68
     */
69
    public static function resize(Imagick $source, int $boxWidth, int $boxHeight, array $options = []) : Imagick
70
    {
71
        $options += self::DEFAULT_OPTIONS;
72
73
        //algorithm inspired from http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
74
        //use of 2x2 binning is arguably the best quality one will get downsizing and is what lots of hardware does in
75
        //the photography field, while being reasonably fast. Upsizing is more subjective but you can't get much
76
        //better than bicubic which is what is used here.
77
78
        $color = $options['color'];
79
        Util::ensure(true, is_string($color), InvalidArgumentException::class, ['$options["color"] was not a string']);
80
81
        $upsize = $options['upsize'];
82
        Util::ensure(true, is_bool($upsize), InvalidArgumentException::class, ['$options["upsize"] was not a bool']);
83
84
        $bestfit = $options['bestfit'];
85
        Util::ensure(true, is_bool($bestfit), InvalidArgumentException::class, ['$options["bestfit"] was not a bool']);
86
87
        $blurBackground = $options['blurBackground'];
88
        Util::ensure(
89
            true,
90
            is_bool($blurBackground),
91
            InvalidArgumentException::class,
92
            ['$options["blurBackground"] was not a bool']
93
        );
94
95
        $blurValue = $options['blurValue'];
96
        Util::ensure(
97
            true,
98
            is_float($blurValue),
99
            InvalidArgumentException::class,
100
            ['$options["blurValue"] was not a float']
101
        );
102
        $maxWidth = $options['maxWidth'];
103
        Util::ensure(true, is_int($maxWidth), InvalidArgumentException::class, ['$options["maxWidth"] was not an int']);
104
105
        $maxHeight = $options['maxHeight'];
106
        Util::ensure(
107
            true,
108
            is_int($maxHeight),
109
            InvalidArgumentException::class,
110
            ['$options["maxHeight"] was not an int']
111
        );
112
113
114
        if ($boxWidth > $maxWidth || $boxWidth <= 0) {
115
            throw new InvalidArgumentException('a $boxSizes width was not between 0 and $options["maxWidth"]');
116
        }
117
118
        if ($boxHeight > $maxHeight || $boxHeight <= 0) {
119
            throw new InvalidArgumentException('a $boxSizes height was not between 0 and $options["maxHeight"]');
120
        }
121
122
        $clone = clone $source;
123
124
        self::rotateImage($clone);
125
126
        $width = $clone->getImageWidth();
127
        $height = $clone->getImageHeight();
128
129
        //ratio over 1 is horizontal, under 1 is vertical
130
        $boxRatio = $boxWidth / $boxHeight;
131
        //height should be positive since I didnt find a way you could get zero into imagick
132
        $originalRatio = $width / $height;
133
134
        $targetWidth = null;
0 ignored issues
show
Unused Code introduced by
$targetWidth is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
135
        $targetHeight = null;
0 ignored issues
show
Unused Code introduced by
$targetHeight is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
136
        $targetX = null;
0 ignored issues
show
Unused Code introduced by
$targetX is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
137
        $targetY = null;
0 ignored issues
show
Unused Code introduced by
$targetY is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
138 View Code Duplication
        if ($width < $boxWidth && $height < $boxHeight && !$upsize) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
139
            $targetWidth = $width;
140
            $targetHeight = $height;
141
            $targetX = ($boxWidth - $width) / 2;
142
            $targetY = ($boxHeight - $height) / 2;
143
        } else {
144
            //if box is more vertical than original
145
            if ($boxRatio < $originalRatio) {
146
                $targetWidth = $boxWidth;
147
                $targetHeight = (int)((double)$boxWidth / $originalRatio);
148
                $targetX = 0;
149
                $targetY = ($boxHeight - $targetHeight) / 2;
150
            } else {
151
                $targetWidth = (int)((double)$boxHeight * $originalRatio);
152
                $targetHeight = $boxHeight;
153
                $targetX = ($boxWidth - $targetWidth) / 2;
154
                $targetY = 0;
155
            }
156
        }
157
158
        $widthReduced = false;
159
        if ($width > $targetWidth) {
160
            $width = $targetWidth;
161
            $widthReduced = true;
162
        }
163
164
        $heightReduced = false;
165
        if ($height > $targetHeight) {
166
            $height = $targetHeight;
167
            $heightReduced = true;
168
        }
169
170
        if ($widthReduced || $heightReduced) {
171
            if ($clone->resizeImage($width, $height, \Imagick::FILTER_BOX, 1.0) !== true) {
172
                //cumbersome to test
173
                throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
174
            }
175
        }
176
177 View Code Duplication
        if ($upsize && ($width < $targetWidth || $height < $targetHeight)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
178
            if ($clone->resizeImage($targetWidth, $targetHeight, \Imagick::FILTER_CUBIC, 1.0, $bestfit) !== true) {
179
                //cumbersome to test
180
                throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
181
            }
182
        }
183
184
        if ($clone->getImageHeight() === $boxHeight && $clone->getImageWidth() === $boxWidth) {
185
            return $clone;
186
        }
187
188
        //put image in box
189
        $canvas = self::getBackgroundCanvas($clone, $color, $blurBackground, $blurValue, $boxWidth, $boxHeight);
190
        if ($canvas->compositeImage($clone, \Imagick::COMPOSITE_ATOP, $targetX, $targetY) !== true) {
191
            //cumbersome to test
192
            throw new \Exception('Imagick::compositeImage() did not return true');//@codeCoverageIgnore
193
        }
194
195
        //reason we are not supporting the options in self::write() here is because format, and strip headers are
196
        //only relevant once written Imagick::stripImage() doesnt even have an effect until written
197
        //also the user can just call that function with the resultant $canvas
198
        return $canvas;
199
    }
200
201
    /**
202
     * resizes images into a bounding box. Maintains aspect ratio, extra space filled with given color.
203
     *
204
     * @param \Imagick $source source image to resize. Will not modify
205
     * @param array $boxSizes resulting bounding boxes. Each value should be an array with width and height, both
206
     *                        integers
207
     * @param array $options options
208
     *     string color (default white) background color. Any supported from
209
     *         http://www.imagemagick.org/script/color.php#color_names
210
     *     bool upsize (default false) true to upsize the original image or false to upsize just the bounding box
211
     *     bool bestfit (default false) true to resize with the best fit option.
212
     *     int maxWidth (default 10000) max width allowed for $boxWidth
213
     *     int maxHeight (default 10000) max height allowed for $boxHeight
214
     *     bool blurBackground (default false) true to create a composite resized image placed over an enlarged blurred
215
     *                         image of the original.
216
     *
217
     * @return array array of \Imagick objects resized. Keys maintained from $boxSizes
218
     *
219
     * @throws InvalidArgumentException if $options["color"] was not a string
220
     * @throws InvalidArgumentException if $options["upsize"] was not a bool
221
     * @throws InvalidArgumentException if $options["bestfit"] was not a bool
222
     * @throws InvalidArgumentException if $options["maxWidth"] was not an int
223
     * @throws InvalidArgumentException if $options["maxHeight"] was not an int
224
     * @throws InvalidArgumentException if a width in a $boxSizes value was not an int
225
     * @throws InvalidArgumentException if a height in a $boxSizes value was not an int
226
     * @throws InvalidArgumentException if a $boxSizes width was not between 0 and $options["maxWidth"]
227
     * @throws InvalidArgumentException if a $boxSizes height was not between 0 and $options["maxHeight"]
228
     * @throws \Exception
229
     */
230
    public static function resizeMulti(\Imagick $source, array $boxSizes, array $options = []) : array
231
    {
232
        //algorithm inspired from http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
233
        //use of 2x2 binning is arguably the best quality one will get downsizing and is what lots of hardware does in
234
        //the photography field, while being reasonably fast. Upsizing is more subjective but you can't get much
235
        //better than bicubic which is what is used here.
236
237
        $options = $options + self::DEFAULT_OPTIONS;
238
        $color = $options['color'];
239
        Util::ensure(true, is_string($color), InvalidArgumentException::class, ['$options["color"] was not a string']);
240
241
        $upsize = $options['upsize'];
242
        Util::ensure(true, is_bool($upsize), InvalidArgumentException::class, ['$options["upsize"] was not a bool']);
243
244
        $bestfit = $options['bestfit'];
245
        Util::ensure(true, is_bool($bestfit), InvalidArgumentException::class, ['$options["bestfit"] was not a bool']);
246
247
        $blurBackground = $options['blurBackground'];
248
        Util::ensure(
249
            true,
250
            is_bool($blurBackground),
251
            InvalidArgumentException::class,
252
            ['$options["blurBackground"] was not a bool']
253
        );
254
255
        $blurValue = $options['blurValue'];
256
        Util::ensure(
257
            true,
258
            is_float($blurValue),
259
            InvalidArgumentException::class,
260
            ['$options["blurValue"] was not a float']
261
        );
262
        $maxWidth = $options['maxWidth'];
263
        Util::ensure(true, is_int($maxWidth), InvalidArgumentException::class, ['$options["maxWidth"] was not an int']);
264
265
        $maxHeight = $options['maxHeight'];
266
        Util::ensure(
267
            true,
268
            is_int($maxHeight),
269
            InvalidArgumentException::class,
270
            ['$options["maxHeight"] was not an int']
271
        );
272
273
        foreach ($boxSizes as $boxSizeKey => $boxSize) {
274 View Code Duplication
            if (!isset($boxSize['width']) || !is_int($boxSize['width'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
275
                throw new InvalidArgumentException('a width in a $boxSizes value was not an int');
276
            }
277
278 View Code Duplication
            if (!isset($boxSize['height']) || !is_int($boxSize['height'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279
                throw new InvalidArgumentException('a height in a $boxSizes value was not an int');
280
            }
281
282
            if ($boxSize['width'] > $maxWidth || $boxSize['width'] <= 0) {
283
                throw new InvalidArgumentException('a $boxSizes width was not between 0 and $options["maxWidth"]');
284
            }
285
286
            if ($boxSize['height'] > $maxHeight || $boxSize['height'] <= 0) {
287
                throw new InvalidArgumentException('a $boxSizes height was not between 0 and $options["maxHeight"]');
288
            }
289
        }
290
291
        $results = [];
292
        $cloneCache = [];
293
        foreach ($boxSizes as $boxSizeKey => $boxSize) {
294
            $boxWidth = $boxSize['width'];
295
            $boxHeight = $boxSize['height'];
296
297
            $clone = clone $source;
298
299
            self::rotateImage($clone);
300
301
            $width = $clone->getImageWidth();
302
            $height = $clone->getImageHeight();
303
304
            //ratio over 1 is horizontal, under 1 is vertical
305
            $boxRatio = $boxWidth / $boxHeight;
306
            //height should be positive since I didnt find a way you could get zero into imagick
307
            $originalRatio = $width / $height;
308
309
            $targetWidth = null;
0 ignored issues
show
Unused Code introduced by
$targetWidth is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
310
            $targetHeight = null;
0 ignored issues
show
Unused Code introduced by
$targetHeight is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
311
            $targetX = null;
0 ignored issues
show
Unused Code introduced by
$targetX is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
312
            $targetY = null;
0 ignored issues
show
Unused Code introduced by
$targetY is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
313 View Code Duplication
            if ($width < $boxWidth && $height < $boxHeight && !$upsize) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
314
                $targetWidth = $width;
315
                $targetHeight = $height;
316
                $targetX = ($boxWidth - $width) / 2;
317
                $targetY = ($boxHeight - $height) / 2;
318
            } else {
319
                //if box is more vertical than original
320
                if ($boxRatio < $originalRatio) {
321
                    $targetWidth = $boxWidth;
322
                    $targetHeight = (int)((double)$boxWidth / $originalRatio);
323
                    $targetX = 0;
324
                    $targetY = ($boxHeight - $targetHeight) / 2;
325
                } else {
326
                    $targetWidth = (int)((double)$boxHeight * $originalRatio);
327
                    $targetHeight = $boxHeight;
328
                    $targetX = ($boxWidth - $targetWidth) / 2;
329
                    $targetY = 0;
330
                }
331
            }
332
333
            //do iterative downsize by halfs (2x2 binning is a common name) on dimensions that are bigger than target
334
            //width and height
335
            while (true) {
336
                $widthReduced = false;
337
                $widthIsHalf = false;
338
                if ($width > $targetWidth) {
339
                    $width = (int)($width / 2);
340
                    $widthReduced = true;
341
                    $widthIsHalf = true;
342
                    if ($width < $targetWidth) {
343
                        $width = $targetWidth;
344
                        $widthIsHalf = false;
345
                    }
346
                }
347
348
                $heightReduced = false;
349
                $heightIsHalf = false;
350
                if ($height > $targetHeight) {
351
                    $height = (int)($height / 2);
352
                    $heightReduced = true;
353
                    $heightIsHalf = true;
354
                    if ($height < $targetHeight) {
355
                        $height = $targetHeight;
356
                        $heightIsHalf = false;
357
                    }
358
                }
359
360
                if (!$widthReduced && !$heightReduced) {
361
                    break;
362
                }
363
364
                $cacheKey = "{$width}x{$height}";
365
                if (isset($cloneCache[$cacheKey])) {
366
                    $clone = clone $cloneCache[$cacheKey];
367
                    continue;
368
                }
369
370
                if ($clone->resizeImage($width, $height, \Imagick::FILTER_BOX, 1.0) !== true) {
371
                    //cumbersome to test
372
                    throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
373
                }
374
375
                if ($widthIsHalf && $heightIsHalf) {
376
                    $cloneCache[$cacheKey] = clone $clone;
377
                }
378
            }
379
380 View Code Duplication
            if ($upsize && ($width < $targetWidth || $height < $targetHeight)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
381
                if ($clone->resizeImage($targetWidth, $targetHeight, \Imagick::FILTER_CUBIC, 1.0, $bestfit) !== true) {
382
                    //cumbersome to test
383
                    throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
384
                }
385
            }
386
387
            if ($clone->getImageHeight() === $boxHeight && $clone->getImageWidth() === $boxWidth) {
388
                $results[$boxSizeKey] = $clone;
389
                continue;
390
            }
391
392
            //put image in box
393
            $canvas = self::getBackgroundCanvas($source, $color, $blurBackground, $blurValue, $boxWidth, $boxHeight);
394
            if ($canvas->compositeImage($clone, \Imagick::COMPOSITE_ATOP, $targetX, $targetY) !== true) {
395
                //cumbersome to test
396
                throw new \Exception('Imagick::compositeImage() did not return true');//@codeCoverageIgnore
397
            }
398
399
            //reason we are not supporting the options in self::write() here is because format, and strip headers are
400
            //only relevant once written Imagick::stripImage() doesnt even have an effect until written
401
            //also the user can just call that function with the resultant $canvas
402
            $results[$boxSizeKey] = $canvas;
403
        }
404
405
        foreach ($cloneCache as $clone) {
406
            $clone->destroy();
407
        }
408
409
        return $results;
410
    }
411
412
    private static function getBackgroundCanvas(
413
        \Imagick $source,
414
        string $color,
415
        bool $blurBackground,
416
        float $blurValue,
417
        int $boxWidth,
418
        int $boxHeight
419
    ) : \Imagick {
420
        if ($blurBackground || $color === 'blur') {
421
            return self::getBlurredBackgroundCanvas($source, $blurValue, $boxWidth, $boxHeight);
422
        }
423
424
        return self::getColoredBackgroundCanvas($color, $boxWidth, $boxHeight);
425
    }
426
427
    private static function getColoredBackgroundCanvas(string $color, int $boxWidth, int $boxHeight)
428
    {
429
        $canvas = new \Imagick();
430
        $imageCreated = $canvas->newImage($boxWidth, $boxHeight, $color);
431
        Util::ensure(true, $imageCreated, 'Imagick::newImage() did not return true');
432
        return $canvas;
433
    }
434
435
    private static function getBlurredBackgroundCanvas(
436
        \Imagick $source,
437
        float $blurValue,
438
        int $boxWidth,
439
        int $boxHeight
440
    ) : \Imagick {
441
        $canvas = clone $source;
442
        self::rotateImage($canvas);
443
        $canvas->resizeImage($boxWidth, $boxHeight, \Imagick::FILTER_BOX, $blurValue, false);
444
        return $canvas;
445
    }
446
447
    /**
448
     * write $source to $destPath with $options applied
449
     *
450
     * @param \Imagick $source source image. Will not modify
451
     * @param string $destPath destination image path
452
     * @param array $options options
453
     *     string format        (default jpeg) Any from http://www.imagemagick.org/script/formats.php#supported
454
     *     int    directoryMode (default 0777) chmod mode for any parent directories created
455
     *     int    fileMode      (default 0777) chmod mode for the resized image file
456
     *     bool   stripHeaders  (default true) whether to strip headers (exif, etc). Is only reflected in $destPath,
457
     *                                         not returned clone
458
     *
459
     * @return void
460
     *
461
     * @throws InvalidArgumentException if $destPath was not a string
462
     * @throws InvalidArgumentException if $options["format"] was not a string
463
     * @throws InvalidArgumentException if $options["directoryMode"] was not an int
464
     * @throws InvalidArgumentException if $options["fileMode"] was not an int
465
     * @throws InvalidArgumentException if $options["stripHeaders"] was not a bool
466
     * @throws \Exception
467
     */
468
    public static function write(\Imagick $source, string $destPath, array $options = [])
469
    {
470
        $format = 'jpeg';
471
        if (array_key_exists('format', $options)) {
472
            $format = $options['format'];
473
            if (!is_string($format)) {
474
                throw new InvalidArgumentException('$options["format"] was not a string');
475
            }
476
        }
477
478
        $directoryMode = 0777;
479
        if (array_key_exists('directoryMode', $options)) {
480
            $directoryMode = $options['directoryMode'];
481
            if (!is_int($directoryMode)) {
482
                throw new InvalidArgumentException('$options["directoryMode"] was not an int');
483
            }
484
        }
485
486
        $fileMode = 0777;
487
        if (array_key_exists('fileMode', $options)) {
488
            $fileMode = $options['fileMode'];
489
            if (!is_int($fileMode)) {
490
                throw new InvalidArgumentException('$options["fileMode"] was not an int');
491
            }
492
        }
493
494
        $stripHeaders = true;
495
        if (array_key_exists('stripHeaders', $options)) {
496
            $stripHeaders = $options['stripHeaders'];
497
            if ($stripHeaders !== false && $stripHeaders !== true) {
498
                throw new InvalidArgumentException('$options["stripHeaders"] was not a bool');
499
            }
500
        }
501
502
        $destDir = dirname($destPath);
503
        if (!is_dir($destDir)) {
504
            $oldUmask = umask(0);
505
            if (!mkdir($destDir, $directoryMode, true)) {
506
                //cumbersome to test
507
                throw new \Exception('mkdir() returned false');//@codeCoverageIgnore
508
            }
509
510
            umask($oldUmask);
511
        }
512
513
        $clone = clone $source;
514
515
        if ($clone->setImageFormat($format) !== true) {
516
            //cumbersome to test
517
            throw new \Exception('Imagick::setImageFormat() did not return true');//@codeCoverageIgnore
518
        }
519
520
        if ($stripHeaders && $clone->stripImage() !== true) {
521
            //cumbersome to test
522
            throw new \Exception('Imagick::stripImage() did not return true');//@codeCoverageIgnore
523
        }
524
525
        if ($clone->writeImage($destPath) !== true) {
526
            //cumbersome to test
527
            throw new \Exception('Imagick::writeImage() did not return true');//@codeCoverageIgnore
528
        }
529
530
        if (!chmod($destPath, $fileMode)) {
531
            //cumbersome to test
532
            throw new \Exception('chmod() returned false');//@codeCoverageIgnore
533
        }
534
    }
535
536
    /**
537
     * Strips the headers (exif, etc) from an image at the given path.
538
     *
539
     * @param string $path The image path.
540
     * @return void
541
     * @throws InvalidArgumentException if $path is not a string
542
     * @throws \Exception if there is a failure stripping the headers
543
     * @throws \Exception if there is a failure writing the image back to path
544
     */
545
    public static function stripHeaders(string $path)
546
    {
547
        $imagick = new \Imagick($path);
548
        if ($imagick->stripImage() !== true) {
549
            //cumbersome to test
550
            throw new \Exception('Imagick::stripImage() did not return true');//@codeCoverageIgnore
551
        }
552
553
        if ($imagick->writeImage($path) !== true) {
554
            //cumbersome to test
555
            throw new \Exception('Imagick::writeImage() did not return true');//@codeCoverageIgnore
556
        }
557
    }
558
559
    /**
560
     * @param \Imagick $imagick
561
     */
562
    private static function rotateImage(\Imagick $imagick)
563
    {
564
        $orientation = $imagick->getImageOrientation();
565
        switch ($orientation) {
566
            case \Imagick::ORIENTATION_BOTTOMRIGHT:
567
                $imagick->rotateimage('#fff', 180);
568
                $imagick->stripImage();
569
                break;
570
            case \Imagick::ORIENTATION_RIGHTTOP:
571
                $imagick->rotateimage('#fff', 90);
572
                $imagick->stripImage();
573
                break;
574
            case \Imagick::ORIENTATION_LEFTBOTTOM:
575
                $imagick->rotateimage('#fff', -90);
576
                $imagick->stripImage();
577
                break;
578
        }
579
    }
580
}
581