Completed
Push — master ( be3b3f...d1dc3f )
by Ismayil
13:04
created

ImageService::getFileFormat()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 2.0009

Importance

Changes 0
Metric Value
cc 2
eloc 16
nc 2
nop 2
dl 0
loc 24
ccs 15
cts 16
cp 0.9375
crap 2.0009
rs 8.9713
c 0
b 0
f 0
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 Elgg\Filesystem\MimeTypeDetector;
10
11
/**
12
 * Image manipulation service
13
 *
14
 * @since 2.3
15
 * @access private
16
 */
17
class ImageService {
18
19
	const JPEG_QUALITY = 75;
20
21
	/**
22
	 * @var ImagineInterface
23
	 */
24
	private $imagine;
25
26
	/**
27
	 * @var Config
28
	 */
29
	private $config;
30
31
	/**
32
	 * Constructor
33
	 *
34
	 * @param ImagineInterface $imagine Imagine interface
35
	 * @param Config           $config  Elgg config
36
	 */
37 1
	public function __construct(ImagineInterface $imagine, Config $config) {
38 1
		$this->imagine = $imagine;
39 1
		$this->config = $config;
40 1
	}
41
42
	/**
43
	 * Crop and resize an image
44
	 *
45
	 * @param string $source      Path to source image
46
	 * @param string $destination Path to destination
47
	 *                            If not set, will modify the source image
48
	 * @param array  $params      An array of cropping/resizing parameters
49
	 *                             - INT 'w' represents the width of the new image
50
	 *                               With upscaling disabled, this is the maximum width
51
	 *                               of the new image (in case the source image is
52
	 *                               smaller than the expected width)
53
	 *                             - INT 'h' represents the height of the new image
54
	 *                               With upscaling disabled, this is the maximum height
55
	 *                             - INT 'x1', 'y1', 'x2', 'y2' represent optional cropping
56
	 *                               coordinates. The source image will first be cropped
57
	 *                               to these coordinates, and then resized to match
58
	 *                               width/height parameters
59
	 *                             - BOOL 'square' - square images will fill the
60
	 *                               bounding box (width x height). In Imagine's terms,
61
	 *                               this equates to OUTBOUND mode
62
	 *                             - BOOL 'upscale' - if enabled, smaller images
63
	 *                               will be upscaled to fit the bounding box.
64
	 * @return bool
65
	 */
66 46
	public function resize($source, $destination = null, array $params = []) {
67
68 46
		if (!isset($destination)) {
69
			$destination = $source;
70
		}
71
72
		try {
73 46
			$image = $this->imagine->open($source);
74
75 46
			$width = $image->getSize()->getWidth();
76 46
			$height = $image->getSize()->getHeight();
77
78 46
			$resize_params = $this->normalizeResizeParameters($width, $height, $params);
79
80 46
			$max_width = elgg_extract('w', $resize_params);
81 46
			$max_height = elgg_extract('h', $resize_params);
82
83 46
			$x1 = (int) elgg_extract('x1', $resize_params, 0);
84 46
			$y1 = (int) elgg_extract('y1', $resize_params, 0);
85 46
			$x2 = (int) elgg_extract('x2', $resize_params, 0);
86 46
			$y2 = (int) elgg_extract('y2', $resize_params, 0);
87
88 46
			if ($x2 > $x1 && $y2 > $y1) {
89 46
				$crop_start = new Point($x1, $y1);
90 46
				$crop_size = new Box($x2 - $x1, $y2 - $y1);
91 46
				$image->crop($crop_start, $crop_size);
92 46
			}
93
94 46
			$target_size = new Box($max_width, $max_height);
95 46
			$thumbnail = $image->resize($target_size);
96
97 46
			$thumbnail->save($destination, [
98 46
				'jpeg_quality' => elgg_extract('jpeg_quality', $params, self::JPEG_QUALITY),
99 46
				'format' => $this->getFileFormat($source, $params),
100 46
			]);
101
102 46
			unset($image);
103 46
			unset($thumbnail);
104 46
		} catch (Exception $ex) {
105 1
			_elgg_services()->logger->error($ex->getMessage());
106 1
			return false;
107
		}
108
109 46
		return true;
110
	}
111
112
	/**
113
	 * Calculate the parameters for resizing an image
114
	 *
115
	 * @param int   $width  Natural width of the image
116
	 * @param int   $height Natural height of the image
117
	 * @param array $params Resize parameters
118
	 *                      - 'w' maximum width of the resized image
119
	 *                      - 'h' maximum height of the resized image
120
	 *                      - 'upscale' allow upscaling
121
	 *                      - 'square' constrain to a square
122
	 *                      - 'x1', 'y1', 'x2', 'y2' cropping coordinates
123
	 *
124
	 * @return array
125
	 * @throws \LogicException
126
	 */
127 46
	public function normalizeResizeParameters($width, $height, array $params = []) {
128
129 46
		$max_width = (int) elgg_extract('w', $params, 100, false);
130 46
		$max_height = (int) elgg_extract('h', $params, 100, false);
131 46
		if (!$max_height || !$max_width) {
132
			throw new \LogicException("Resize width and height parameters are required");
133
		}
134
135 46
		$square = elgg_extract('square', $params, false);
136 46
		$upscale = elgg_extract('upscale', $params, false);
137
138 46
		$x1 = (int) elgg_extract('x1', $params, 0);
139 46
		$y1 = (int) elgg_extract('y1', $params, 0);
140 46
		$x2 = (int) elgg_extract('x2', $params, 0);
141 46
		$y2 = (int) elgg_extract('y2', $params, 0);
142
143 46
		$cropping_mode = $x1 || $y1 || $x2 || $y2;
144
145 46
		if ($cropping_mode) {
146 8
			$crop_width = $x2 - $x1;
147 8
			$crop_height = $y2 - $y1;
148 8
			if ($crop_width <= 0 || $crop_height <= 0 || $crop_width > $width || $crop_height > $height) {
149
				throw new \LogicException("Coordinates [$x1, $y1], [$x2, $y2] are invalid for image cropping");
150
			}
151 8
		} else {
152
			// everything selected if no crop parameters
153 46
			$crop_width = $width;
154 46
			$crop_height = $height;
155
		}
156
157
		// determine cropping offsets
158 46
		if ($square) {
159
			// asking for a square image back
160
			// detect case where someone is passing crop parameters that are not for a square
161 45
			if ($cropping_mode == true && $crop_width != $crop_height) {
162 1
				throw new \LogicException("Coordinates [$x1, $y1], [$x2, $y2] are invalid for a squared image cropping");
163
			}
164
165
			// size of the new square image
166 45
			$max_width = $max_height = min($max_width, $max_height);
167
168
			// find largest square that fits within the selected region
169 45
			$crop_width = $crop_height = min($crop_width, $crop_height);
170
171 45
			if (!$cropping_mode) {
172
				// place square region in the center
173 45
				$x1 = floor(($width - $crop_width) / 2);
174 45
				$y1 = floor(($height - $crop_height) / 2);
175 45
			}
176 45
		} else {
177
			// maintain aspect ratio of original image/crop
178 43
			if ($crop_height / $max_height > $crop_width / $max_width) {
179 19
				$max_width = floor($max_height * $crop_width / $crop_height);
180 19
			} else {
181 26
				$max_height = floor($max_width * $crop_height / $crop_width);
182
			}
183
		}
184
185 46
		if (!$upscale && ($crop_height < $max_height || $crop_width < $max_width)) {
186
			// we cannot upscale and selected area is too small so we decrease size of returned image
187 26
			$max_height = $crop_height;
188 26
			$max_width = $crop_width;
189 26
		}
190
191
		return [
192 46
			'w' => $max_width,
193 46
			'h' => $max_height,
194 46
			'x1' => $x1,
195 46
			'y1' => $y1,
196 46
			'x2' => $x1 + $crop_width,
197 46
			'y2' => $y1 + $crop_height,
198 46
			'square' => $square,
199 46
			'upscale' => $upscale,
200 46
		];
201
	}
202
203
	/**
204
	 * Determine the image file format, this is needed for correct resizing
205
	 *
206
	 * @param string $filename path to the file
207
	 * @param array  $params   array of resizing params (can contain 'format' to set save format)
208
	 *
209
	 * @see https://github.com/Elgg/Elgg/issues/10686
210
	 * @return void|string
211
	 */
212 46
	protected function getFileFormat($filename, $params) {
213
		
214
		$accepted_formats = [
215 46
			'image/jpeg' => 'jpeg',
216 46
			'image/pjpeg' => 'jpeg',
217 46
			'image/png' => 'png',
218 46
			'image/x-png' => 'png',
219 46
			'image/gif' => 'gif',
220 46
			'image/vnd.wap.wbmp' => 'wbmp',
221 46
			'image/x‑xbitmap' => 'xbm',
222 46
			'image/x‑xbm' => 'xbm',
223 46
		];
224
		
225
		// was a valid output format supplied
226 46
		$format = elgg_extract('format', $params);
227 46
		if (in_array($format, $accepted_formats)) {
228
			return $format;
229
		}
230
		
231 46
		$mime_detector = new MimeTypeDetector();
232 46
		$mime = $mime_detector->getType($filename);
233
		
234 46
		return elgg_extract($mime, $accepted_formats);
235
	}
236
}
237