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

src/Image.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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'])) {
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'])) {
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
$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;
131
            $targetX = null;
132
            $targetY = null;
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