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; |
|
|
|
|
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
|
|
|
|
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.