Elgg  Version master
ImageService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
14 
21 class ImageService {
22 
23  use Loggable;
24 
25  const JPEG_QUALITY = 75;
26  const WEBP_QUALITY = 75;
27 
31  protected $imagine;
32 
36  protected $config;
37 
41  protected $mimetype;
42 
49  public function __construct(Config $config, MimeTypeService $mimetype) {
50 
51  switch ($config->image_processor) {
52  case 'imagick':
53  if (extension_loaded('imagick')) {
54  $this->imagine = new \Imagine\Imagick\Imagine();
55  break;
56  }
57 
58  // fallback to GD if Imagick is not loaded
59  default:
60  // default use GD
61  $this->imagine = new \Imagine\Gd\Imagine();
62  break;
63  }
64 
65  $this->config = $config;
66  $this->mimetype = $mimetype;
67  }
68 
97  public function resize(string $source, string $destination = null, array $params = []): bool {
98 
99  $destination = $destination ?? $source;
100 
101  try {
102  $resize_params = $this->normalizeResizeParameters($source, $params);
103 
104  $image = $this->imagine->open($source);
105 
106  $max_width = (int) elgg_extract('w', $resize_params);
107  $max_height = (int) elgg_extract('h', $resize_params);
108 
109  $x1 = (int) elgg_extract('x1', $resize_params, 0);
110  $y1 = (int) elgg_extract('y1', $resize_params, 0);
111  $x2 = (int) elgg_extract('x2', $resize_params, 0);
112  $y2 = (int) elgg_extract('y2', $resize_params, 0);
113 
114  if ($x2 > $x1 && $y2 > $y1) {
115  $crop_start = new Point($x1, $y1);
116  $crop_size = new Box($x2 - $x1, $y2 - $y1);
117  $image->crop($crop_start, $crop_size);
118  }
119 
120  $target_size = new Box($max_width, $max_height);
121  $thumbnail = $image->resize($target_size);
122 
123  if (pathinfo($destination, PATHINFO_EXTENSION) === 'webp') {
124  $options = [
125  'webp_quality' => elgg_extract('webp_quality', $params, self::WEBP_QUALITY),
126  ];
127  } else {
128  $options = [
129  'format' => $this->getFileFormat($source, $params),
130  'jpeg_quality' => elgg_extract('jpeg_quality', $params, self::JPEG_QUALITY),
131  ];
132  }
133 
134  $thumbnail->save($destination, $options);
135 
136  unset($image);
137  unset($thumbnail);
138  } catch (\Exception $ex) {
139  $this->getLogger()->error($ex);
140 
141  return false;
142  }
143 
144  return true;
145  }
146 
154  public function fixOrientation($filename) {
155  try {
156  $image = $this->imagine->open($filename);
157  $metadata = $image->metadata();
158  if (!isset($metadata['ifd0.Orientation'])) {
159  // no need to perform an orientation fix
160  return true;
161  }
162 
163  $autorotate = new Autorotate();
164  $autorotate->apply($image)->save($filename);
165 
166  $image->strip()->save($filename);
167 
168  return true;
169  } catch (\Exception $ex) {
170  $this->getLogger()->notice($ex);
171  }
172 
173  return false;
174  }
175 
190  public function normalizeResizeParameters(string $source, array $params = []) {
191 
192  $image = $this->imagine->open($source);
193 
194  $width = $image->getSize()->getWidth();
195  $height = $image->getSize()->getHeight();
196 
197  $max_width = (int) elgg_extract('w', $params, 100, false);
198  $max_height = (int) elgg_extract('h', $params, 100, false);
199  if (!$max_height || !$max_width) {
200  throw new InvalidArgumentException('Resize width and height parameters are required');
201  }
202 
203  $square = elgg_extract('square', $params, false);
204  $upscale = elgg_extract('upscale', $params, false);
205 
206  $x1 = (int) elgg_extract('x1', $params, 0);
207  $y1 = (int) elgg_extract('y1', $params, 0);
208  $x2 = (int) elgg_extract('x2', $params, 0);
209  $y2 = (int) elgg_extract('y2', $params, 0);
210 
211  $cropping_mode = $x1 || $y1 || $x2 || $y2;
212 
213  if ($cropping_mode) {
214  $crop_width = $x2 - $x1;
215  $crop_height = $y2 - $y1;
216  if ($crop_width <= 0 || $crop_height <= 0 || $crop_width > $width || $crop_height > $height) {
217  throw new RangeException("Coordinates [$x1, $y1], [$x2, $y2] are invalid for image cropping");
218  }
219  } else {
220  // everything selected if no crop parameters
221  $crop_width = $width;
222  $crop_height = $height;
223  }
224 
225  // determine cropping offsets
226  if ($square) {
227  // asking for a square image back
228 
229  // size of the new square image
230  $max_width = min($max_width, $max_height);
231  $max_height = $max_width;
232 
233  // find largest square that fits within the selected region
234  $crop_width = min($crop_width, $crop_height);
235  $crop_height = $crop_width;
236 
237  if (!$cropping_mode) {
238  // place square region in the center
239  $x1 = floor(($width - $crop_width) / 2);
240  $y1 = floor(($height - $crop_height) / 2);
241  }
242  } else {
243  // maintain aspect ratio of original image/crop
244  if ($crop_height / $max_height > $crop_width / $max_width) {
245  $max_width = floor($max_height * $crop_width / $crop_height);
246  } else {
247  $max_height = floor($max_width * $crop_height / $crop_width);
248  }
249  }
250 
251  if (!$upscale && ($crop_height < $max_height || $crop_width < $max_width)) {
252  // we cannot upscale and selected area is too small so we decrease size of returned image
253  $max_height = $crop_height;
254  $max_width = $crop_width;
255  }
256 
257  return [
258  'w' => $max_width,
259  'h' => $max_height,
260  'x1' => $x1,
261  'y1' => $y1,
262  'x2' => $x1 + $crop_width,
263  'y2' => $y1 + $crop_height,
264  'square' => $square,
265  'upscale' => $upscale,
266  ];
267  }
268 
278  protected function getFileFormat($filename, $params) {
279 
280  $accepted_formats = [
281  'image/jpeg' => 'jpeg',
282  'image/pjpeg' => 'jpeg',
283  'image/png' => 'png',
284  'image/x-png' => 'png',
285  'image/gif' => 'gif',
286  'image/vnd.wap.wbmp' => 'wbmp',
287  'image/x‑xbitmap' => 'xbm',
288  'image/x‑xbm' => 'xbm',
289  ];
290 
291  // was a valid output format supplied
292  $format = elgg_extract('format', $params);
293  if (in_array($format, $accepted_formats)) {
294  return $format;
295  }
296 
297  try {
298  return elgg_extract($this->mimetype->getMimeType($filename), $accepted_formats);
299  } catch (InvalidArgumentException $e) {
300  $this->getLogger()->warning($e);
301  }
302  }
303 
309  public function hasWebPSupport(): bool {
310  if ($this->config->webp_enabled === false) {
311  return false;
312  }
313 
314  if ($this->imagine instanceof \Imagine\Imagick\Imagine) {
315  return !empty(\Imagick::queryformats('WEBP*'));
316  } elseif ($this->imagine instanceof \Imagine\Gd\Imagine) {
317  return (bool) elgg_extract('WebP Support', gd_info(), false);
318  }
319 
320  return false;
321  }
322 }
$format
Definition: date.php:39
__construct(Config $config, MimeTypeService $mimetype)
Constructor.
Exception thrown to indicate range errors during program execution.
normalizeResizeParameters(string $source, array $params=[])
Calculate the parameters for resizing an image.
$source
Exception thrown if an argument is not of the expected type.
$params
Saves global plugin settings.
Definition: save.php:13
if($item instanceof\ElggEntity) elseif($item instanceof\ElggRiverItem) elseif($item instanceof\ElggRelationship) elseif(is_callable([$item, 'getType']))
Definition: item.php:48
$options
Elgg admin footer.
Definition: footer.php:6
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:254
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
fixOrientation($filename)
If needed the image will be rotated based on orientation information.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
resize(string $source, string $destination=null, array $params=[])
Crop and resize an image.
$image
Definition: image_block.php:25
Image manipulation service.
getFileFormat($filename, $params)
Determine the image file format, this is needed for correct resizing.
getLogger()
Returns logger.
Definition: Loggable.php:37
$metadata
Output annotation metadata.
Definition: metadata.php:9
hasWebPSupport()
Checks if imagine has WebP support.
Public service related to MIME type detection.