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

Image::resize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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