Completed
Pull Request — master (#21)
by Chad
01:06
created

Image::write()   D

Complexity

Conditions 17
Paths 191

Size

Total Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 67
rs 4.4583
c 0
b 0
f 0
cc 17
nc 191
nop 3

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 TraderInteractive\Util;
6
7
final class Image
8
{
9
    /**
10
     * Calls @see resizeMulti() with $boxWidth and $boxHeight as a single element in $boxSizes
11
     */
12
    public static function resize(\Imagick $source, int $boxWidth, int $boxHeight, array $options = []) : \Imagick
13
    {
14
        $results = self::resizeMulti($source, [['width' => $boxWidth, 'height' => $boxHeight]], $options);
15
        return $results[0];
16
    }
17
18
    /**
19
     * resizes images into a bounding box. Maintains aspect ratio, extra space filled with given color.
20
     *
21
     * @param \Imagick $source source image to resize. Will not modify
22
     * @param array $boxSizes resulting bounding boxes. Each value should be an array with width and height, both
23
     *                        integers
24
     * @param array $options options
25
     *     string color (default white) background color. Any supported from
26
     *         http://www.imagemagick.org/script/color.php#color_names
27
     *     bool upsize (default false) true to upsize the original image or false to upsize just the bounding box
28
     *     int maxWidth (default 10000) max width allowed for $boxWidth
29
     *     int maxHeight (default 10000) max height allowed for $boxHeight
30
     *
31
     * @return array array of \Imagick objects resized. Keys maintained from $boxSizes
32
     *
33
     * @throws \InvalidArgumentException if $options["color"] was not a string
34
     * @throws \InvalidArgumentException if $options["upsize"] was not a bool
35
     * @throws \InvalidArgumentException if $options["bestfit"] was not a bool
36
     * @throws \InvalidArgumentException if $options["maxWidth"] was not an int
37
     * @throws \InvalidArgumentException if $options["maxHeight"] was not an int
38
     * @throws \InvalidArgumentException if a width in a $boxSizes value was not an int
39
     * @throws \InvalidArgumentException if a height in a $boxSizes value was not an int
40
     * @throws \InvalidArgumentException if a $boxSizes width was not between 0 and $options["maxWidth"]
41
     * @throws \InvalidArgumentException if a $boxSizes height was not between 0 and $options["maxHeight"]
42
     * @throws \Exception
43
     */
44
    public static function resizeMulti(\Imagick $source, array $boxSizes, array $options = []) : array
45
    {
46
        //algorithm inspired from http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
47
        //use of 2x2 binning is arguably the best quality one will get downsizing and is what lots of hardware does in
48
        //the photography field, while being reasonably fast. Upsizing is more subjective but you can't get much
49
        //better than bicubic which is what is used here.
50
        $color = 'white';
51
        if (isset($options['color'])) {
52
            $color = $options['color'];
53
            if (!is_string($color)) {
54
                throw new \InvalidArgumentException('$options["color"] was not a string');
55
            }
56
        }
57
58
        $upsize = false;
59 View Code Duplication
        if (isset($options['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...
60
            $upsize = $options['upsize'];
61
            if ($upsize !== true && $upsize !== false) {
62
                throw new \InvalidArgumentException('$options["upsize"] was not a bool');
63
            }
64
        }
65
66
        $bestfit = false;
67 View Code Duplication
        if (isset($options['bestfit'])) {
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...
68
            $bestfit = $options['bestfit'];
69
            if ($bestfit !== true && $bestfit !== false) {
70
                throw new \InvalidArgumentException('$options["bestfit"] was not a bool');
71
            }
72
        }
73
74
        $maxWidth = 10000;
75
        if (isset($options['maxWidth'])) {
76
            $maxWidth = $options['maxWidth'];
77
            if (!is_int($maxWidth)) {
78
                throw new \InvalidArgumentException('$options["maxWidth"] was not an int');
79
            }
80
        }
81
82
        $maxHeight = 10000;
83
        if (isset($options['maxHeight'])) {
84
            $maxHeight = $options['maxHeight'];
85
            if (!is_int($maxHeight)) {
86
                throw new \InvalidArgumentException('$options["maxHeight"] was not an int');
87
            }
88
        }
89
90
        foreach ($boxSizes as $boxSizeKey => $boxSize) {
91 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...
92
                throw new \InvalidArgumentException('a width in a $boxSizes value was not an int');
93
            }
94
95 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...
96
                throw new \InvalidArgumentException('a height in a $boxSizes value was not an int');
97
            }
98
99
            if ($boxSize['width'] > $maxWidth || $boxSize['width'] <= 0) {
100
                throw new \InvalidArgumentException('a $boxSizes width was not between 0 and $options["maxWidth"]');
101
            }
102
103
            if ($boxSize['height'] > $maxHeight || $boxSize['height'] <= 0) {
104
                throw new \InvalidArgumentException('a $boxSizes height was not between 0 and $options["maxHeight"]');
105
            }
106
        }
107
108
        $results = [];
109
        $cloneCache = [];
110
        foreach ($boxSizes as $boxSizeKey => $boxSize) {
111
            $boxWidth = $boxSize['width'];
112
            $boxHeight = $boxSize['height'];
113
114
            $clone = clone $source;
115
116
            $orientation = $clone->getImageOrientation();
117
            switch ($orientation) {
118
                case \Imagick::ORIENTATION_BOTTOMRIGHT:
119
                    $clone->rotateimage('#fff', 180);
120
                    $clone->stripImage();
121
                    break;
122
                case \Imagick::ORIENTATION_RIGHTTOP:
123
                    $clone->rotateimage('#fff', 90);
124
                    $clone->stripImage();
125
                    break;
126
                case \Imagick::ORIENTATION_LEFTBOTTOM:
127
                    $clone->rotateimage('#fff', -90);
128
                    $clone->stripImage();
129
                    break;
130
            }
131
132
            $width = $clone->getImageWidth();
133
            $height = $clone->getImageHeight();
134
135
            //ratio over 1 is horizontal, under 1 is vertical
136
            $boxRatio = $boxWidth / $boxHeight;
137
            //height should be positive since I didnt find a way you could get zero into imagick
138
            $originalRatio = $width / $height;
139
140
            $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...
141
            $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...
142
            $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...
143
            $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...
144
            if ($width < $boxWidth && $height < $boxHeight && !$upsize) {
145
                $targetWidth = $width;
146
                $targetHeight = $height;
147
                $targetX = ($boxWidth - $width) / 2;
148
                $targetY = ($boxHeight - $height) / 2;
149
            } else {
150
                //if box is more vertical than original
151
                if ($boxRatio < $originalRatio) {
152
                    $targetWidth = $boxWidth;
153
                    $targetHeight = (int)((double)$boxWidth / $originalRatio);
154
                    $targetX = 0;
155
                    $targetY = ($boxHeight - $targetHeight) / 2;
156
                } else {
157
                    $targetWidth = (int)((double)$boxHeight * $originalRatio);
158
                    $targetHeight = $boxHeight;
159
                    $targetX = ($boxWidth - $targetWidth) / 2;
160
                    $targetY = 0;
161
                }
162
            }
163
164
            //do iterative downsize by halfs (2x2 binning is a common name) on dimensions that are bigger than target
165
            //width and height
166
            while (true) {
167
                $widthReduced = false;
168
                $widthIsHalf = false;
169
                if ($width > $targetWidth) {
170
                    $width = (int)($width / 2);
171
                    $widthReduced = true;
172
                    $widthIsHalf = true;
173
                    if ($width < $targetWidth) {
174
                        $width = $targetWidth;
175
                        $widthIsHalf = false;
176
                    }
177
                }
178
179
                $heightReduced = false;
180
                $heightIsHalf = false;
181
                if ($height > $targetHeight) {
182
                    $height = (int)($height / 2);
183
                    $heightReduced = true;
184
                    $heightIsHalf = true;
185
                    if ($height < $targetHeight) {
186
                        $height = $targetHeight;
187
                        $heightIsHalf = false;
188
                    }
189
                }
190
191
                if (!$widthReduced && !$heightReduced) {
192
                    break;
193
                }
194
195
                $cacheKey = "{$width}x{$height}";
196
                if (isset($cloneCache[$cacheKey])) {
197
                    $clone = clone $cloneCache[$cacheKey];
198
                    continue;
199
                }
200
201
                if ($clone->resizeImage($width, $height, \Imagick::FILTER_BOX, 1.0) !== true) {
202
                    //cumbersome to test
203
                    throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
204
                }
205
206
                if ($widthIsHalf && $heightIsHalf) {
207
                    $cloneCache[$cacheKey] = clone $clone;
208
                }
209
            }
210
211
            if ($upsize && ($width < $targetWidth || $height < $targetHeight)) {
212
                if ($clone->resizeImage($targetWidth, $targetHeight, \Imagick::FILTER_CUBIC, 1.0, $bestfit) !== true) {
213
                    //cumbersome to test
214
                    throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
215
                }
216
            }
217
218
            //put image in box
219
            $canvas = self::getBackgroundCanvas($clone, $color, $boxWidth, $boxHeight);
220
            if ($canvas->compositeImage($clone, \Imagick::COMPOSITE_ATOP, $targetX, $targetY) !== true) {
221
                //cumbersome to test
222
                throw new \Exception('Imagick::compositeImage() did not return true');//@codeCoverageIgnore
223
            }
224
225
            //reason we are not supporting the options in self::write() here is because format, and strip headers are
226
            //only relevant once written Imagick::stripImage() doesnt even have an effect until written
227
            //also the user can just call that function with the resultant $canvas
228
            $results[$boxSizeKey] = $canvas;
229
        }
230
231
        return $results;
232
    }
233
234
    private static function getBackgroundCanvas(\Imagick $source, string $color, int $boxWidth, $boxHeight)
235
    {
236
        if ($color === 'blur') {
237
            $canvas = new \Imagick();
238
            $canvas->readImageBlob($source->getImageBlob());
239
            $canvas->resizeImage($boxWidth, $boxHeight, \Imagick::FILTER_BOX, 15.0, false);
240
            return $canvas;
241
        }
242
243
        $canvas = new \Imagick();
244
        $imageCreated = $canvas->newImage($boxWidth, $boxHeight, $color);
245
        Util::ensure(true, $imageCreated, 'Imagick::newImage() did not return true');
246
        return $canvas;
247
    }
248
249
    /**
250
     * write $source to $destPath with $options applied
251
     *
252
     * @param \Imagick $source source image. Will not modify
253
     * @param string $destPath destination image path
254
     * @param array $options options
255
     *     string format        (default jpeg) Any from http://www.imagemagick.org/script/formats.php#supported
256
     *     int    directoryMode (default 0777) chmod mode for any parent directories created
257
     *     int    fileMode      (default 0777) chmod mode for the resized image file
258
     *     bool   stripHeaders  (default true) whether to strip headers (exif, etc). Is only reflected in $destPath,
259
     *                                         not returned clone
260
     *
261
     * @return void
262
     *
263
     * @throws \InvalidArgumentException if $destPath was not a string
264
     * @throws \InvalidArgumentException if $options["format"] was not a string
265
     * @throws \InvalidArgumentException if $options["directoryMode"] was not an int
266
     * @throws \InvalidArgumentException if $options["fileMode"] was not an int
267
     * @throws \InvalidArgumentException if $options["stripHeaders"] was not a bool
268
     * @throws \Exception
269
     */
270
    public static function write(\Imagick $source, string $destPath, array $options = [])
271
    {
272
        $format = 'jpeg';
273
        if (array_key_exists('format', $options)) {
274
            $format = $options['format'];
275
            if (!is_string($format)) {
276
                throw new \InvalidArgumentException('$options["format"] was not a string');
277
            }
278
        }
279
280
        $directoryMode = 0777;
281
        if (array_key_exists('directoryMode', $options)) {
282
            $directoryMode = $options['directoryMode'];
283
            if (!is_int($directoryMode)) {
284
                throw new \InvalidArgumentException('$options["directoryMode"] was not an int');
285
            }
286
        }
287
288
        $fileMode = 0777;
289
        if (array_key_exists('fileMode', $options)) {
290
            $fileMode = $options['fileMode'];
291
            if (!is_int($fileMode)) {
292
                throw new \InvalidArgumentException('$options["fileMode"] was not an int');
293
            }
294
        }
295
296
        $stripHeaders = true;
297
        if (array_key_exists('stripHeaders', $options)) {
298
            $stripHeaders = $options['stripHeaders'];
299
            if ($stripHeaders !== false && $stripHeaders !== true) {
300
                throw new \InvalidArgumentException('$options["stripHeaders"] was not a bool');
301
            }
302
        }
303
304
        $destDir = dirname($destPath);
305
        if (!is_dir($destDir)) {
306
            $oldUmask = umask(0);
307
            if (!mkdir($destDir, $directoryMode, true)) {
308
                //cumbersome to test
309
                throw new \Exception('mkdir() returned false');//@codeCoverageIgnore
310
            }
311
312
            umask($oldUmask);
313
        }
314
315
        $clone = clone $source;
316
317
        if ($clone->setImageFormat($format) !== true) {
318
            //cumbersome to test
319
            throw new \Exception('Imagick::setImageFormat() did not return true');//@codeCoverageIgnore
320
        }
321
322
        if ($stripHeaders && $clone->stripImage() !== true) {
323
            //cumbersome to test
324
            throw new \Exception('Imagick::stripImage() did not return true');//@codeCoverageIgnore
325
        }
326
327
        if ($clone->writeImage($destPath) !== true) {
328
            //cumbersome to test
329
            throw new \Exception('Imagick::writeImage() did not return true');//@codeCoverageIgnore
330
        }
331
332
        if (!chmod($destPath, $fileMode)) {
333
            //cumbersome to test
334
            throw new \Exception('chmod() returned false');//@codeCoverageIgnore
335
        }
336
    }
337
338
    /**
339
     * Strips the headers (exif, etc) from an image at the given path.
340
     *
341
     * @param string $path The image path.
342
     * @return void
343
     * @throws \InvalidArgumentException if $path is not a string
344
     * @throws \Exception if there is a failure stripping the headers
345
     * @throws \Exception if there is a failure writing the image back to path
346
     */
347
    public static function stripHeaders(string $path)
348
    {
349
        $imagick = new \Imagick($path);
350
        if ($imagick->stripImage() !== true) {
351
            //cumbersome to test
352
            throw new \Exception('Imagick::stripImage() did not return true');//@codeCoverageIgnore
353
        }
354
355
        if ($imagick->writeImage($path) !== true) {
356
            //cumbersome to test
357
            throw new \Exception('Imagick::writeImage() did not return true');//@codeCoverageIgnore
358
        }
359
    }
360
}
361