Completed
Pull Request — master (#22)
by Chad
01:07
created

Image::stripHeaders()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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