Failed Conditions
Pull Request — master (#19)
by Chad
02:43
created

Image::write()   C

Complexity

Conditions 18
Paths 192

Size

Total Lines 71
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

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