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

Image   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 584
Duplicated Lines 9.59 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 87
lcom 1
cbo 1
dl 56
loc 584
rs 2
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
F resize() 25 143 24
F resizeMulti() 31 181 34
A getBackgroundCanvas() 0 14 3
A getColoredBackgroundCanvas() 0 7 1
A getBlurredBackgroundCanvas() 0 11 1
D write() 0 67 17
A stripHeaders() 0 13 3
A rotateImage() 0 18 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Image often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Image, and based on these observations, apply Extract Interface, too.

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
        //do iterative downsize by halfs (2x2 binning is a common name) on dimensions that are bigger than target
159
        //width and height
160
        while (true) {
161
            $widthReduced = false;
162
            if ($width > $targetWidth) {
163
                $width = (int)($width / 2);
164
                $widthReduced = true;
165
                if ($width < $targetWidth) {
166
                    $width = $targetWidth;
167
                }
168
            }
169
170
            $heightReduced = false;
171
            if ($height > $targetHeight) {
172
                $height = (int)($height / 2);
173
                $heightReduced = true;
174
                if ($height < $targetHeight) {
175
                    $height = $targetHeight;
176
                }
177
            }
178
179
            if (!$widthReduced && !$heightReduced) {
180
                break;
181
            }
182
183
            if ($clone->resizeImage($width, $height, \Imagick::FILTER_BOX, 1.0) !== true) {
184
                //cumbersome to test
185
                throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
186
            }
187
        }
188
189 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...
190
            if ($clone->resizeImage($targetWidth, $targetHeight, \Imagick::FILTER_CUBIC, 1.0, $bestfit) !== true) {
191
                //cumbersome to test
192
                throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
193
            }
194
        }
195
196
        if ($clone->getImageHeight() === $boxHeight && $clone->getImageWidth() === $boxWidth) {
197
            return $clone;
198
        }
199
200
        //put image in box
201
        $canvas = self::getBackgroundCanvas($source, $color, $blurBackground, $blurValue, $boxWidth, $boxHeight);
202
        if ($canvas->compositeImage($clone, \Imagick::COMPOSITE_ATOP, $targetX, $targetY) !== true) {
203
            //cumbersome to test
204
            throw new \Exception('Imagick::compositeImage() did not return true');//@codeCoverageIgnore
205
        }
206
207
        //reason we are not supporting the options in self::write() here is because format, and strip headers are
208
        //only relevant once written Imagick::stripImage() doesnt even have an effect until written
209
        //also the user can just call that function with the resultant $canvas
210
        return $canvas;
211
    }
212
213
    /**
214
     * resizes images into a bounding box. Maintains aspect ratio, extra space filled with given color.
215
     *
216
     * @param \Imagick $source source image to resize. Will not modify
217
     * @param array $boxSizes resulting bounding boxes. Each value should be an array with width and height, both
218
     *                        integers
219
     * @param array $options options
220
     *     string color (default white) background color. Any supported from
221
     *         http://www.imagemagick.org/script/color.php#color_names
222
     *     bool upsize (default false) true to upsize the original image or false to upsize just the bounding box
223
     *     bool bestfit (default false) true to resize with the best fit option.
224
     *     int maxWidth (default 10000) max width allowed for $boxWidth
225
     *     int maxHeight (default 10000) max height allowed for $boxHeight
226
     *     bool blurBackground (default false) true to create a composite resized image placed over an enlarged blurred
227
     *                         image of the original.
228
     *
229
     * @return array array of \Imagick objects resized. Keys maintained from $boxSizes
230
     *
231
     * @throws InvalidArgumentException if $options["color"] was not a string
232
     * @throws InvalidArgumentException if $options["upsize"] was not a bool
233
     * @throws InvalidArgumentException if $options["bestfit"] was not a bool
234
     * @throws InvalidArgumentException if $options["maxWidth"] was not an int
235
     * @throws InvalidArgumentException if $options["maxHeight"] was not an int
236
     * @throws InvalidArgumentException if a width in a $boxSizes value was not an int
237
     * @throws InvalidArgumentException if a height in a $boxSizes value was not an int
238
     * @throws InvalidArgumentException if a $boxSizes width was not between 0 and $options["maxWidth"]
239
     * @throws InvalidArgumentException if a $boxSizes height was not between 0 and $options["maxHeight"]
240
     * @throws \Exception
241
     */
242
    public static function resizeMulti(\Imagick $source, array $boxSizes, array $options = []) : array
243
    {
244
        //algorithm inspired from http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
245
        //use of 2x2 binning is arguably the best quality one will get downsizing and is what lots of hardware does in
246
        //the photography field, while being reasonably fast. Upsizing is more subjective but you can't get much
247
        //better than bicubic which is what is used here.
248
249
        $options = $options + self::DEFAULT_OPTIONS;
250
        $color = $options['color'];
251
        Util::ensure(true, is_string($color), InvalidArgumentException::class, ['$options["color"] was not a string']);
252
253
        $upsize = $options['upsize'];
254
        Util::ensure(true, is_bool($upsize), InvalidArgumentException::class, ['$options["upsize"] was not a bool']);
255
256
        $bestfit = $options['bestfit'];
257
        Util::ensure(true, is_bool($bestfit), InvalidArgumentException::class, ['$options["bestfit"] was not a bool']);
258
259
        $blurBackground = $options['blurBackground'];
260
        Util::ensure(
261
            true,
262
            is_bool($blurBackground),
263
            InvalidArgumentException::class,
264
            ['$options["blurBackground"] was not a bool']
265
        );
266
267
        $blurValue = $options['blurValue'];
268
        Util::ensure(
269
            true,
270
            is_float($blurValue),
271
            InvalidArgumentException::class,
272
            ['$options["blurValue"] was not a float']
273
        );
274
        $maxWidth = $options['maxWidth'];
275
        Util::ensure(true, is_int($maxWidth), InvalidArgumentException::class, ['$options["maxWidth"] was not an int']);
276
277
        $maxHeight = $options['maxHeight'];
278
        Util::ensure(
279
            true,
280
            is_int($maxHeight),
281
            InvalidArgumentException::class,
282
            ['$options["maxHeight"] was not an int']
283
        );
284
285
        foreach ($boxSizes as $boxSizeKey => $boxSize) {
286 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...
287
                throw new InvalidArgumentException('a width in a $boxSizes value was not an int');
288
            }
289
290 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...
291
                throw new InvalidArgumentException('a height in a $boxSizes value was not an int');
292
            }
293
294
            if ($boxSize['width'] > $maxWidth || $boxSize['width'] <= 0) {
295
                throw new InvalidArgumentException('a $boxSizes width was not between 0 and $options["maxWidth"]');
296
            }
297
298
            if ($boxSize['height'] > $maxHeight || $boxSize['height'] <= 0) {
299
                throw new InvalidArgumentException('a $boxSizes height was not between 0 and $options["maxHeight"]');
300
            }
301
        }
302
303
        $results = [];
304
        $cloneCache = [];
305
        foreach ($boxSizes as $boxSizeKey => $boxSize) {
306
            $boxWidth = $boxSize['width'];
307
            $boxHeight = $boxSize['height'];
308
309
            $clone = clone $source;
310
311
            self::rotateImage($clone);
312
313
            $width = $clone->getImageWidth();
314
            $height = $clone->getImageHeight();
315
316
            //ratio over 1 is horizontal, under 1 is vertical
317
            $boxRatio = $boxWidth / $boxHeight;
318
            //height should be positive since I didnt find a way you could get zero into imagick
319
            $originalRatio = $width / $height;
320
321
            $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...
322
            $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...
323
            $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...
324
            $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...
325 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...
326
                $targetWidth = $width;
327
                $targetHeight = $height;
328
                $targetX = ($boxWidth - $width) / 2;
329
                $targetY = ($boxHeight - $height) / 2;
330
            } else {
331
                //if box is more vertical than original
332
                if ($boxRatio < $originalRatio) {
333
                    $targetWidth = $boxWidth;
334
                    $targetHeight = (int)((double)$boxWidth / $originalRatio);
335
                    $targetX = 0;
336
                    $targetY = ($boxHeight - $targetHeight) / 2;
337
                } else {
338
                    $targetWidth = (int)((double)$boxHeight * $originalRatio);
339
                    $targetHeight = $boxHeight;
340
                    $targetX = ($boxWidth - $targetWidth) / 2;
341
                    $targetY = 0;
342
                }
343
            }
344
345
            //do iterative downsize by halfs (2x2 binning is a common name) on dimensions that are bigger than target
346
            //width and height
347
            while (true) {
348
                $widthReduced = false;
349
                $widthIsHalf = false;
350
                if ($width > $targetWidth) {
351
                    $width = (int)($width / 2);
352
                    $widthReduced = true;
353
                    $widthIsHalf = true;
354
                    if ($width < $targetWidth) {
355
                        $width = $targetWidth;
356
                        $widthIsHalf = false;
357
                    }
358
                }
359
360
                $heightReduced = false;
361
                $heightIsHalf = false;
362
                if ($height > $targetHeight) {
363
                    $height = (int)($height / 2);
364
                    $heightReduced = true;
365
                    $heightIsHalf = true;
366
                    if ($height < $targetHeight) {
367
                        $height = $targetHeight;
368
                        $heightIsHalf = false;
369
                    }
370
                }
371
372
                if (!$widthReduced && !$heightReduced) {
373
                    break;
374
                }
375
376
                $cacheKey = "{$width}x{$height}";
377
                if (isset($cloneCache[$cacheKey])) {
378
                    $clone = clone $cloneCache[$cacheKey];
379
                    continue;
380
                }
381
382
                if ($clone->resizeImage($width, $height, \Imagick::FILTER_BOX, 1.0) !== true) {
383
                    //cumbersome to test
384
                    throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
385
                }
386
387
                if ($widthIsHalf && $heightIsHalf) {
388
                    $cloneCache[$cacheKey] = clone $clone;
389
                }
390
            }
391
392 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...
393
                if ($clone->resizeImage($targetWidth, $targetHeight, \Imagick::FILTER_CUBIC, 1.0, $bestfit) !== true) {
394
                    //cumbersome to test
395
                    throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
396
                }
397
            }
398
399
            if ($clone->getImageHeight() === $boxHeight && $clone->getImageWidth() === $boxWidth) {
400
                $results[$boxSizeKey] = $clone;
401
                continue;
402
            }
403
404
            //put image in box
405
            $canvas = self::getBackgroundCanvas($source, $color, $blurBackground, $blurValue, $boxWidth, $boxHeight);
406
            if ($canvas->compositeImage($clone, \Imagick::COMPOSITE_ATOP, $targetX, $targetY) !== true) {
407
                //cumbersome to test
408
                throw new \Exception('Imagick::compositeImage() did not return true');//@codeCoverageIgnore
409
            }
410
411
            //reason we are not supporting the options in self::write() here is because format, and strip headers are
412
            //only relevant once written Imagick::stripImage() doesnt even have an effect until written
413
            //also the user can just call that function with the resultant $canvas
414
            $results[$boxSizeKey] = $canvas;
415
        }
416
417
        foreach ($cloneCache as $clone) {
418
            $clone->destroy();
419
        }
420
421
        return $results;
422
    }
423
424
    private static function getBackgroundCanvas(
425
        \Imagick $source,
426
        string $color,
427
        bool $blurBackground,
428
        float $blurValue,
429
        int $boxWidth,
430
        int $boxHeight
431
    ) : \Imagick {
432
        if ($blurBackground || $color === 'blur') {
433
            return self::getBlurredBackgroundCanvas($source, $blurValue, $boxWidth, $boxHeight);
434
        }
435
436
        return self::getColoredBackgroundCanvas($color, $boxWidth, $boxHeight);
437
    }
438
439
    private static function getColoredBackgroundCanvas(string $color, int $boxWidth, int $boxHeight)
440
    {
441
        $canvas = new \Imagick();
442
        $imageCreated = $canvas->newImage($boxWidth, $boxHeight, $color);
443
        Util::ensure(true, $imageCreated, 'Imagick::newImage() did not return true');
444
        return $canvas;
445
    }
446
447
    private static function getBlurredBackgroundCanvas(
448
        \Imagick $source,
449
        float $blurValue,
450
        int $boxWidth,
451
        int $boxHeight
452
    ) : \Imagick {
453
        $canvas = clone $source;
454
        self::rotateImage($canvas);
455
        $canvas->resizeImage($boxWidth, $boxHeight, \Imagick::FILTER_BOX, $blurValue, false);
456
        return $canvas;
457
    }
458
459
    /**
460
     * write $source to $destPath with $options applied
461
     *
462
     * @param \Imagick $source source image. Will not modify
463
     * @param string $destPath destination image path
464
     * @param array $options options
465
     *     string format        (default jpeg) Any from http://www.imagemagick.org/script/formats.php#supported
466
     *     int    directoryMode (default 0777) chmod mode for any parent directories created
467
     *     int    fileMode      (default 0777) chmod mode for the resized image file
468
     *     bool   stripHeaders  (default true) whether to strip headers (exif, etc). Is only reflected in $destPath,
469
     *                                         not returned clone
470
     *
471
     * @return void
472
     *
473
     * @throws InvalidArgumentException if $destPath was not a string
474
     * @throws InvalidArgumentException if $options["format"] was not a string
475
     * @throws InvalidArgumentException if $options["directoryMode"] was not an int
476
     * @throws InvalidArgumentException if $options["fileMode"] was not an int
477
     * @throws InvalidArgumentException if $options["stripHeaders"] was not a bool
478
     * @throws \Exception
479
     */
480
    public static function write(\Imagick $source, string $destPath, array $options = [])
481
    {
482
        $format = 'jpeg';
483
        if (array_key_exists('format', $options)) {
484
            $format = $options['format'];
485
            if (!is_string($format)) {
486
                throw new InvalidArgumentException('$options["format"] was not a string');
487
            }
488
        }
489
490
        $directoryMode = 0777;
491
        if (array_key_exists('directoryMode', $options)) {
492
            $directoryMode = $options['directoryMode'];
493
            if (!is_int($directoryMode)) {
494
                throw new InvalidArgumentException('$options["directoryMode"] was not an int');
495
            }
496
        }
497
498
        $fileMode = 0777;
499
        if (array_key_exists('fileMode', $options)) {
500
            $fileMode = $options['fileMode'];
501
            if (!is_int($fileMode)) {
502
                throw new InvalidArgumentException('$options["fileMode"] was not an int');
503
            }
504
        }
505
506
        $stripHeaders = true;
507
        if (array_key_exists('stripHeaders', $options)) {
508
            $stripHeaders = $options['stripHeaders'];
509
            if ($stripHeaders !== false && $stripHeaders !== true) {
510
                throw new InvalidArgumentException('$options["stripHeaders"] was not a bool');
511
            }
512
        }
513
514
        $destDir = dirname($destPath);
515
        if (!is_dir($destDir)) {
516
            $oldUmask = umask(0);
517
            if (!mkdir($destDir, $directoryMode, true)) {
518
                //cumbersome to test
519
                throw new \Exception('mkdir() returned false');//@codeCoverageIgnore
520
            }
521
522
            umask($oldUmask);
523
        }
524
525
        $clone = clone $source;
526
527
        if ($clone->setImageFormat($format) !== true) {
528
            //cumbersome to test
529
            throw new \Exception('Imagick::setImageFormat() did not return true');//@codeCoverageIgnore
530
        }
531
532
        if ($stripHeaders && $clone->stripImage() !== true) {
533
            //cumbersome to test
534
            throw new \Exception('Imagick::stripImage() did not return true');//@codeCoverageIgnore
535
        }
536
537
        if ($clone->writeImage($destPath) !== true) {
538
            //cumbersome to test
539
            throw new \Exception('Imagick::writeImage() did not return true');//@codeCoverageIgnore
540
        }
541
542
        if (!chmod($destPath, $fileMode)) {
543
            //cumbersome to test
544
            throw new \Exception('chmod() returned false');//@codeCoverageIgnore
545
        }
546
    }
547
548
    /**
549
     * Strips the headers (exif, etc) from an image at the given path.
550
     *
551
     * @param string $path The image path.
552
     * @return void
553
     * @throws InvalidArgumentException if $path is not a string
554
     * @throws \Exception if there is a failure stripping the headers
555
     * @throws \Exception if there is a failure writing the image back to path
556
     */
557
    public static function stripHeaders(string $path)
558
    {
559
        $imagick = new \Imagick($path);
560
        if ($imagick->stripImage() !== true) {
561
            //cumbersome to test
562
            throw new \Exception('Imagick::stripImage() did not return true');//@codeCoverageIgnore
563
        }
564
565
        if ($imagick->writeImage($path) !== true) {
566
            //cumbersome to test
567
            throw new \Exception('Imagick::writeImage() did not return true');//@codeCoverageIgnore
568
        }
569
    }
570
571
    /**
572
     * @param \Imagick $imagick
573
     */
574
    private static function rotateImage(\Imagick $imagick)
575
    {
576
        $orientation = $imagick->getImageOrientation();
577
        switch ($orientation) {
578
            case \Imagick::ORIENTATION_BOTTOMRIGHT:
579
                $imagick->rotateimage('#fff', 180);
580
                $imagick->stripImage();
581
                break;
582
            case \Imagick::ORIENTATION_RIGHTTOP:
583
                $imagick->rotateimage('#fff', 90);
584
                $imagick->stripImage();
585
                break;
586
            case \Imagick::ORIENTATION_LEFTBOTTOM:
587
                $imagick->rotateimage('#fff', -90);
588
                $imagick->stripImage();
589
                break;
590
        }
591
    }
592
}
593