Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

ImageService   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 246
Duplicated Lines 0 %

Test Coverage

Coverage 84.04%

Importance

Changes 0
Metric Value
dl 0
loc 246
rs 9.6
c 0
b 0
f 0
ccs 79
cts 94
cp 0.8404
wmc 32

5 Methods

Rating   Name   Duplication   Size   Complexity  
B resize() 0 45 6
A fixOrientation() 0 17 4
A __construct() 0 3 1
A getFileFormat() 0 23 2
C normalizeResizeParameters() 0 73 19
1
<?php
2
3
namespace Elgg;
4
5
use Exception;
6
use Imagine\Image\Box;
7
use Imagine\Image\ImagineInterface;
8
use Imagine\Image\Point;
9
use Imagine\Filter\Basic\Autorotate;
10
use Elgg\Filesystem\MimeTypeDetector;
11
12
/**
13
 * Image manipulation service
14
 *
15
 * @since 2.3
16
 * @access private
17
 */
18
class ImageService {
19
	use Loggable;
20
21
	const JPEG_QUALITY = 75;
22
23
	/**
24
	 * @var ImagineInterface
25
	 */
26
	private $imagine;
27
28
	/**
29
	 * @var Config
30
	 */
31
	private $config;
32
33
	/**
34
	 * Constructor
35
	 *
36
	 * @param ImagineInterface $imagine Imagine interface
37
	 * @param Config           $config  Elgg config
38
	 */
39 72
	public function __construct(ImagineInterface $imagine, Config $config) {
40 72
		$this->imagine = $imagine;
41 72
		$this->config = $config;
42 72
	}
43
44
	/**
45
	 * Crop and resize an image
46
	 *
47
	 * @param string $source      Path to source image
48
	 * @param string $destination Path to destination
49
	 *                            If not set, will modify the source image
50
	 * @param array  $params      An array of cropping/resizing parameters
51
	 *                             - INT 'w' represents the width of the new image
52
	 *                               With upscaling disabled, this is the maximum width
53
	 *                               of the new image (in case the source image is
54
	 *                               smaller than the expected width)
55
	 *                             - INT 'h' represents the height of the new image
56
	 *                               With upscaling disabled, this is the maximum height
57
	 *                             - INT 'x1', 'y1', 'x2', 'y2' represent optional cropping
58
	 *                               coordinates. The source image will first be cropped
59
	 *                               to these coordinates, and then resized to match
60
	 *                               width/height parameters
61
	 *                             - BOOL 'square' - square images will fill the
62
	 *                               bounding box (width x height). In Imagine's terms,
63
	 *                               this equates to OUTBOUND mode
64
	 *                             - BOOL 'upscale' - if enabled, smaller images
65
	 *                               will be upscaled to fit the bounding box.
66
	 * @return bool
67
	 */
68 47
	public function resize($source, $destination = null, array $params = []) {
69
70 47
		if (!isset($destination)) {
71
			$destination = $source;
72
		}
73
74
		try {
75 47
			$image = $this->imagine->open($source);
76
77 47
			$width = $image->getSize()->getWidth();
78 47
			$height = $image->getSize()->getHeight();
79
80 47
			$resize_params = $this->normalizeResizeParameters($width, $height, $params);
81
82 47
			$max_width = elgg_extract('w', $resize_params);
83 47
			$max_height = elgg_extract('h', $resize_params);
84
85 47
			$x1 = (int) elgg_extract('x1', $resize_params, 0);
86 47
			$y1 = (int) elgg_extract('y1', $resize_params, 0);
87 47
			$x2 = (int) elgg_extract('x2', $resize_params, 0);
88 47
			$y2 = (int) elgg_extract('y2', $resize_params, 0);
89
90 47
			if ($x2 > $x1 && $y2 > $y1) {
91 47
				$crop_start = new Point($x1, $y1);
92 47
				$crop_size = new Box($x2 - $x1, $y2 - $y1);
93 47
				$image->crop($crop_start, $crop_size);
94
			}
95
96 47
			$target_size = new Box($max_width, $max_height);
97 47
			$thumbnail = $image->resize($target_size);
98
99 47
			$thumbnail->save($destination, [
100 47
				'jpeg_quality' => elgg_extract('jpeg_quality', $params, self::JPEG_QUALITY),
101 47
				'format' => $this->getFileFormat($source, $params),
102
			]);
103
104 47
			unset($image);
105 47
			unset($thumbnail);
106
		} catch (Exception $ex) {
107
			$logger = $this->logger ? $this->logger : _elgg_services()->logger;
108
			$logger->error($ex->getMessage());
109
			return false;
110
		}
111
112 47
		return true;
113
	}
114
	
115
	/**
116
	 * If needed the image will be rotated based on orientation information
117
	 *
118
	 * @param string $filename Path to image
119
	 *
120
	 * @return bool
121
	 */
122 5
	function fixOrientation($filename) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
123
		try {
124 5
			$image = $this->imagine->open($filename);
125 5
			$metadata = $image->metadata();
126 5
			if (!isset($metadata['ifd0.Orientation'])) {
127
				// no need to perform an orientation fix
128 5
				return true;
129
			}
130
			
131
			$autorotate = new Autorotate();
132
			$autorotate->apply($image)->save($filename);
133
			return true;
134
		} catch (Exception $ex) {
135
			$logger = $this->logger ? $this->logger : _elgg_services()->logger;
136
			$logger->notice($ex->getMessage());
137
		}
138
		return false;
139
	}
140
141
	/**
142
	 * Calculate the parameters for resizing an image
143
	 *
144
	 * @param int   $width  Natural width of the image
145
	 * @param int   $height Natural height of the image
146
	 * @param array $params Resize parameters
147
	 *                      - 'w' maximum width of the resized image
148
	 *                      - 'h' maximum height of the resized image
149
	 *                      - 'upscale' allow upscaling
150
	 *                      - 'square' constrain to a square
151
	 *                      - 'x1', 'y1', 'x2', 'y2' cropping coordinates
152
	 *
153
	 * @return array
154
	 * @throws \LogicException
155
	 */
156 48
	public function normalizeResizeParameters($width, $height, array $params = []) {
157
158 48
		$max_width = (int) elgg_extract('w', $params, 100, false);
159 48
		$max_height = (int) elgg_extract('h', $params, 100, false);
160 48
		if (!$max_height || !$max_width) {
161
			throw new \LogicException("Resize width and height parameters are required");
162
		}
163
164 48
		$square = elgg_extract('square', $params, false);
165 48
		$upscale = elgg_extract('upscale', $params, false);
166
167 48
		$x1 = (int) elgg_extract('x1', $params, 0);
168 48
		$y1 = (int) elgg_extract('y1', $params, 0);
169 48
		$x2 = (int) elgg_extract('x2', $params, 0);
170 48
		$y2 = (int) elgg_extract('y2', $params, 0);
171
172 48
		$cropping_mode = $x1 || $y1 || $x2 || $y2;
173
174 48
		if ($cropping_mode) {
175 8
			$crop_width = $x2 - $x1;
176 8
			$crop_height = $y2 - $y1;
177 8
			if ($crop_width <= 0 || $crop_height <= 0 || $crop_width > $width || $crop_height > $height) {
178 8
				throw new \LogicException("Coordinates [$x1, $y1], [$x2, $y2] are invalid for image cropping");
179
			}
180
		} else {
181
			// everything selected if no crop parameters
182 47
			$crop_width = $width;
183 47
			$crop_height = $height;
184
		}
185
186
		// determine cropping offsets
187 48
		if ($square) {
188
			// asking for a square image back
189
			// detect case where someone is passing crop parameters that are not for a square
190 40
			if ($cropping_mode == true && $crop_width != $crop_height) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
191
				throw new \LogicException("Coordinates [$x1, $y1], [$x2, $y2] are invalid for a squared image cropping");
192
			}
193
194
			// size of the new square image
195 40
			$max_width = $max_height = min($max_width, $max_height);
196
197
			// find largest square that fits within the selected region
198 40
			$crop_width = $crop_height = min($crop_width, $crop_height);
199
200 40
			if (!$cropping_mode) {
201
				// place square region in the center
202 39
				$x1 = floor(($width - $crop_width) / 2);
203 40
				$y1 = floor(($height - $crop_height) / 2);
204
			}
205
		} else {
206
			// maintain aspect ratio of original image/crop
207 44
			if ($crop_height / $max_height > $crop_width / $max_width) {
208 20
				$max_width = floor($max_height * $crop_width / $crop_height);
209
			} else {
210 26
				$max_height = floor($max_width * $crop_height / $crop_width);
211
			}
212
		}
213
214 48
		if (!$upscale && ($crop_height < $max_height || $crop_width < $max_width)) {
215
			// we cannot upscale and selected area is too small so we decrease size of returned image
216 45
			$max_height = $crop_height;
217 45
			$max_width = $crop_width;
218
		}
219
220
		return [
221 48
			'w' => $max_width,
222 48
			'h' => $max_height,
223 48
			'x1' => $x1,
224 48
			'y1' => $y1,
225 48
			'x2' => $x1 + $crop_width,
226 48
			'y2' => $y1 + $crop_height,
227 48
			'square' => $square,
228 48
			'upscale' => $upscale,
229
		];
230
	}
231
232
	/**
233
	 * Determine the image file format, this is needed for correct resizing
234
	 *
235
	 * @param string $filename path to the file
236
	 * @param array  $params   array of resizing params (can contain 'format' to set save format)
237
	 *
238
	 * @see https://github.com/Elgg/Elgg/issues/10686
239
	 * @return void|string
240
	 */
241 47
	protected function getFileFormat($filename, $params) {
242
		
243
		$accepted_formats = [
244 47
			'image/jpeg' => 'jpeg',
245
			'image/pjpeg' => 'jpeg',
246
			'image/png' => 'png',
247
			'image/x-png' => 'png',
248
			'image/gif' => 'gif',
249
			'image/vnd.wap.wbmp' => 'wbmp',
250
			'image/x‑xbitmap' => 'xbm',
251
			'image/x‑xbm' => 'xbm',
252
		];
253
		
254
		// was a valid output format supplied
255 47
		$format = elgg_extract('format', $params);
256 47
		if (in_array($format, $accepted_formats)) {
257
			return $format;
258
		}
259
		
260 47
		$mime_detector = new MimeTypeDetector();
261 47
		$mime = $mime_detector->getType($filename);
262
		
263 47
		return elgg_extract($mime, $accepted_formats);
264
	}
265
}
266