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

engine/classes/Elgg/ImageService.php (1 issue)

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) {
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