Completed
Pull Request — master (#19)
by Chad
03:44 queued 02:27
created

Image   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 340
Duplicated Lines 1.76 %

Coupling/Cohesion

Components 0
Dependencies 0

Importance

Changes 0
Metric Value
wmc 67
lcom 0
cbo 0
dl 6
loc 340
rs 3.0612
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A resize() 0 5 1
F resizeMulti() 6 184 44
C write() 0 67 17
A stripHeaders() 0 13 3

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
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, int $boxWidth, int $boxHeight, array $options = []) : \Imagick
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 = []) : array
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
                case \Imagick::ORIENTATION_BOTTOMRIGHT:
107
                    $clone->rotateimage('#fff', 180);
108
                    $clone->stripImage();
109
                    break;
110
                case \Imagick::ORIENTATION_RIGHTTOP:
111
                    $clone->rotateimage('#fff', 90);
112
                    $clone->stripImage();
113
                    break;
114
                case \Imagick::ORIENTATION_LEFTBOTTOM:
115
                    $clone->rotateimage('#fff', -90);
116
                    $clone->stripImage();
117
                    break;
118
            }
119
120
            $width = $clone->getImageWidth();
121
            $height = $clone->getImageHeight();
122
123
            //ratio over 1 is horizontal, under 1 is vertical
124
            $boxRatio = $boxWidth / $boxHeight;
125
            $originalRatio = $width / $height;//height should be positive since I didnt find a way you could get zero into imagick
126
127
            $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...
128
            $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...
129
            $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...
130
            $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...
131
            if ($width < $boxWidth && $height < $boxHeight && !$upsize) {
132
                $targetWidth = $width;
133
                $targetHeight = $height;
134
                $targetX = ($boxWidth - $width) / 2;
135
                $targetY = ($boxHeight - $height) / 2;
136
            } else {
137
                //if box is more vertical than original
138
                if ($boxRatio < $originalRatio) {
139
                    $targetWidth = $boxWidth;
140
                    $targetHeight = (int)((double)$boxWidth / $originalRatio);
141
                    $targetX = 0;
142
                    $targetY = ($boxHeight - $targetHeight) / 2;
143
                } else {
144
                    $targetWidth = (int)((double)$boxHeight * $originalRatio);
145
                    $targetHeight = $boxHeight;
146
                    $targetX = ($boxWidth - $targetWidth) / 2;
147
                    $targetY = 0;
148
                }
149
            }
150
151
            //do iterative downsize by halfs (2x2 binning is a common name) on dimensions that are bigger than target width and height
152
            while (true) {
153
                $widthReduced = false;
154
                $widthIsHalf = false;
155
                if ($width > $targetWidth) {
156
                    $width = (int)($width / 2);
157
                    $widthReduced = true;
158
                    $widthIsHalf = true;
159
                    if ($width < $targetWidth) {
160
                        $width = $targetWidth;
161
                        $widthIsHalf = false;
162
                    }
163
                }
164
165
                $heightReduced = false;
166
                $heightIsHalf = false;
167
                if ($height > $targetHeight) {
168
                    $height = (int)($height / 2);
169
                    $heightReduced = true;
170
                    $heightIsHalf = true;
171
                    if ($height < $targetHeight) {
172
                        $height = $targetHeight;
173
                        $heightIsHalf = false;
174
                    }
175
                }
176
177
                if (!$widthReduced && !$heightReduced) {
178
                    break;
179
                }
180
181
                $cacheKey = "{$width}x{$height}";
182
                if (isset($cloneCache[$cacheKey])) {
183
                    $clone = clone $cloneCache[$cacheKey];
184
                    continue;
185
                }
186
187
                if ($clone->resizeImage($width, $height, \Imagick::FILTER_BOX, 1.0) !== true) {
188
                    //cumbersome to test
189
                    throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
190
                }
191
192
                if ($widthIsHalf && $heightIsHalf) {
193
                    $cloneCache[$cacheKey] = clone $clone;
194
                }
195
            }
196
197
            if ($upsize && ($width < $targetWidth || $height < $targetHeight)) {
198
                if ($clone->resizeImage($targetWidth, $targetHeight, \Imagick::FILTER_CUBIC, 1.0) !== true) {
199
                    //cumbersome to test
200
                    throw new \Exception('Imagick::resizeImage() did not return true');//@codeCoverageIgnore
201
                }
202
            }
203
204
            //put image in box
205
            $canvas = new \Imagick();
206
            if ($canvas->newImage($boxWidth, $boxHeight, $color) !== true) {
207
                //cumbersome to test
208
                throw new \Exception('Imagick::newImage() did not return true');//@codeCoverageIgnore
209
            }
210
211
            if ($canvas->compositeImage($clone, \Imagick::COMPOSITE_ATOP, $targetX, $targetY) !== true) {
212
                //cumbersome to test
213
                throw new \Exception('Imagick::compositeImage() did not return true');//@codeCoverageIgnore
214
            }
215
216
            //reason we are not supporting the options in self::write() here is because format, and strip headers are only relevant once written
217
            //Imagick::stripImage() doesnt even have an effect until written
218
            //also the user can just call that function with the resultant $canvas
219
            $results[$boxSizeKey] = $canvas;
220
        }
221
222
        return $results;
223
    }
224
225
    /**
226
     * write $source to $destPath with $options applied
227
     *
228
     * @param \Imagick $source source image. Will not modify
229
     * @param string $destPath destination image path
230
     * @param array $options options
231
     *     string format (default jpeg) format. Any supported from http://www.imagemagick.org/script/formats.php#supported
232
     *     int directoryMode (default 0777) chmod mode for any parent directories created
233
     *     int fileMode (default 0777) chmod mode for the resized image file
234
     *     bool stripHeaders (default true) whether to strip headers (exif, etc). Is only reflected in $destPath, not returned clone
235
     *
236
     * @return void
237
     *
238
     * @throws \InvalidArgumentException if $destPath was not a string
239
     * @throws \InvalidArgumentException if $options["format"] was not a string
240
     * @throws \InvalidArgumentException if $options["directoryMode"] was not an int
241
     * @throws \InvalidArgumentException if $options["fileMode"] was not an int
242
     * @throws \InvalidArgumentException if $options["stripHeaders"] was not a bool
243
     * @throws \Exception
244
     */
245
    public static function write(\Imagick $source, string $destPath, array $options = [])
246
    {
247
        $format = 'jpeg';
248
        if (array_key_exists('format', $options)) {
249
            $format = $options['format'];
250
            if (!is_string($format)) {
251
                throw new \InvalidArgumentException('$options["format"] was not a string');
252
            }
253
        }
254
255
        $directoryMode = 0777;
256
        if (array_key_exists('directoryMode', $options)) {
257
            $directoryMode = $options['directoryMode'];
258
            if (!is_int($directoryMode)) {
259
                throw new \InvalidArgumentException('$options["directoryMode"] was not an int');
260
            }
261
        }
262
263
        $fileMode = 0777;
264
        if (array_key_exists('fileMode', $options)) {
265
            $fileMode = $options['fileMode'];
266
            if (!is_int($fileMode)) {
267
                throw new \InvalidArgumentException('$options["fileMode"] was not an int');
268
            }
269
        }
270
271
        $stripHeaders = true;
272
        if (array_key_exists('stripHeaders', $options)) {
273
            $stripHeaders = $options['stripHeaders'];
274
            if ($stripHeaders !== false && $stripHeaders !== true) {
275
                throw new \InvalidArgumentException('$options["stripHeaders"] was not a bool');
276
            }
277
        }
278
279
        $destDir = dirname($destPath);
280
        if (!is_dir($destDir)) {
281
            $oldUmask = umask(0);
282
            if (!mkdir($destDir, $directoryMode, true)) {
283
                //cumbersome to test
284
                throw new \Exception('mkdir() returned false');//@codeCoverageIgnore
285
            }
286
287
            umask($oldUmask);
288
        }
289
290
        $clone = clone $source;
291
292
        if ($clone->setImageFormat($format) !== true) {
293
            //cumbersome to test
294
            throw new \Exception('Imagick::setImageFormat() did not return true');//@codeCoverageIgnore
295
        }
296
297
        if ($stripHeaders && $clone->stripImage() !== true) {
298
            //cumbersome to test
299
            throw new \Exception('Imagick::stripImage() did not return true');//@codeCoverageIgnore
300
        }
301
302
        if ($clone->writeImage($destPath) !== true) {
303
            //cumbersome to test
304
            throw new \Exception('Imagick::writeImage() did not return true');//@codeCoverageIgnore
305
        }
306
307
        if (!chmod($destPath, $fileMode)) {
308
            //cumbersome to test
309
            throw new \Exception('chmod() returned false');//@codeCoverageIgnore
310
        }
311
    }
312
313
    /**
314
     * Strips the headers (exif, etc) from an image at the given path.
315
     *
316
     * @param string $path The image path.
317
     * @return void
318
     * @throws \InvalidArgumentException if $path is not a string
319
     * @throws \Exception if there is a failure stripping the headers
320
     * @throws \Exception if there is a failure writing the image back to path
321
     */
322
    public static function stripHeaders(string $path)
323
    {
324
        $imagick = new \Imagick($path);
325
        if ($imagick->stripImage() !== true) {
326
            //cumbersome to test
327
            throw new \Exception('Imagick::stripImage() did not return true');//@codeCoverageIgnore
328
        }
329
330
        if ($imagick->writeImage($path) !== true) {
331
            //cumbersome to test
332
            throw new \Exception('Imagick::writeImage() did not return true');//@codeCoverageIgnore
333
        }
334
    }
335
}
336