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 
39  public function __construct(protected Config $config, protected MimeTypeService $mimetype) {
40 
41  switch ($config->image_processor) {
42  case 'imagick':
43  if (extension_loaded('imagick')) {
44  $this->imagine = new \Imagine\Imagick\Imagine();
45  break;
46  }
47 
48  // fallback to GD if Imagick is not loaded
49  default:
50  // default use GD
51  $this->imagine = new \Imagine\Gd\Imagine();
52  break;
53  }
54  }
55 
84  public function resize(string $source, string $destination = null, array $params = []): bool {
85 
86  $destination = $destination ?? $source;
87 
88  try {
89  $resize_params = $this->normalizeResizeParameters($source, $params);
90 
91  $image = $this->imagine->open($source);
92 
93  $max_width = (int) elgg_extract('w', $resize_params);
94  $max_height = (int) elgg_extract('h', $resize_params);
95 
96  $x1 = (int) elgg_extract('x1', $resize_params, 0);
97  $y1 = (int) elgg_extract('y1', $resize_params, 0);
98  $x2 = (int) elgg_extract('x2', $resize_params, 0);
99  $y2 = (int) elgg_extract('y2', $resize_params, 0);
100 
101  if ($x2 > $x1 && $y2 > $y1) {
102  $crop_start = new Point($x1, $y1);
103  $crop_size = new Box($x2 - $x1, $y2 - $y1);
104  $image->crop($crop_start, $crop_size);
105  }
106 
107  $target_size = new Box($max_width, $max_height);
108  $image->resize($target_size);
109 
110  // create new canvas with a background (default: white)
111  $background_color = elgg_extract('background_color', $params, 'ffffff');
112  $thumbnail = $this->imagine->create($image->getSize(), $image->palette()->color($background_color));
113  $thumbnail->paste($image, new Point(0, 0));
114 
115  if (pathinfo($destination, PATHINFO_EXTENSION) === 'webp') {
116  $options = [
117  'webp_quality' => elgg_extract('webp_quality', $params, self::WEBP_QUALITY),
118  ];
119  } else {
120  $options = [
121  'format' => $this->getFileFormat($source, $params),
122  'jpeg_quality' => elgg_extract('jpeg_quality', $params, self::JPEG_QUALITY),
123  ];
124  }
125 
126  $thumbnail->save($destination, $options);
127 
128  unset($image);
129  unset($thumbnail);
130  } catch (\Exception $ex) {
131  $this->getLogger()->error($ex);
132 
133  return false;
134  }
135 
136  return true;
137  }
138 
146  public function fixOrientation($filename) {
147  try {
148  $image = $this->imagine->open($filename);
149  $metadata = $image->metadata();
150  if (!isset($metadata['ifd0.Orientation'])) {
151  // no need to perform an orientation fix
152  return true;
153  }
154 
155  $autorotate = new Autorotate();
156  $autorotate->apply($image)->save($filename);
157 
158  $image->strip()->save($filename);
159 
160  return true;
161  } catch (\Exception $ex) {
162  $this->getLogger()->notice($ex);
163  }
164 
165  return false;
166  }
167 
183  public function normalizeResizeParameters(string $source, array $params = []): array {
184 
185  $image = $this->imagine->open($source);
186 
187  $width = $image->getSize()->getWidth();
188  $height = $image->getSize()->getHeight();
189 
190  $max_width = (int) elgg_extract('w', $params, 100, false);
191  $max_height = (int) elgg_extract('h', $params, 100, false);
192  if (!$max_height || !$max_width) {
193  throw new InvalidArgumentException('Resize width and height parameters are required');
194  }
195 
196  $square = elgg_extract('square', $params, false);
197  $upscale = elgg_extract('upscale', $params, false);
198 
199  $x1 = (int) elgg_extract('x1', $params, 0);
200  $y1 = (int) elgg_extract('y1', $params, 0);
201  $x2 = (int) elgg_extract('x2', $params, 0);
202  $y2 = (int) elgg_extract('y2', $params, 0);
203 
204  $cropping_mode = $x1 || $y1 || $x2 || $y2;
205 
206  if ($cropping_mode) {
207  $crop_width = $x2 - $x1;
208  $crop_height = $y2 - $y1;
209  if ($crop_width <= 0 || $crop_height <= 0 || $crop_width > $width || $crop_height > $height) {
210  throw new RangeException("Coordinates [$x1, $y1], [$x2, $y2] are invalid for image cropping");
211  }
212  } else {
213  // everything selected if no crop parameters
214  $crop_width = $width;
215  $crop_height = $height;
216  }
217 
218  // determine cropping offsets
219  if ($square) {
220  // asking for a square image back
221 
222  // size of the new square image
223  $max_width = min($max_width, $max_height);
224  $max_height = $max_width;
225 
226  // find largest square that fits within the selected region
227  $crop_width = min($crop_width, $crop_height);
228  $crop_height = $crop_width;
229 
230  if (!$cropping_mode) {
231  // place square region in the center
232  $x1 = floor(($width - $crop_width) / 2);
233  $y1 = floor(($height - $crop_height) / 2);
234  }
235  } else {
236  // maintain aspect ratio of original image/crop
237  if ($crop_height / $max_height > $crop_width / $max_width) {
238  $max_width = floor($max_height * $crop_width / $crop_height);
239  } else {
240  $max_height = floor($max_width * $crop_height / $crop_width);
241  }
242  }
243 
244  if (!$upscale && ($crop_height < $max_height || $crop_width < $max_width)) {
245  // we cannot upscale and selected area is too small so we decrease size of returned image
246  $max_height = $crop_height;
247  $max_width = $crop_width;
248  }
249 
250  return [
251  'w' => $max_width,
252  'h' => $max_height,
253  'x1' => $x1,
254  'y1' => $y1,
255  'x2' => $x1 + $crop_width,
256  'y2' => $y1 + $crop_height,
257  'square' => $square,
258  'upscale' => $upscale,
259  ];
260  }
261 
271  protected function getFileFormat($filename, $params) {
272 
273  $accepted_formats = [
274  'image/jpeg' => 'jpeg',
275  'image/pjpeg' => 'jpeg',
276  'image/png' => 'png',
277  'image/x-png' => 'png',
278  'image/gif' => 'gif',
279  'image/vnd.wap.wbmp' => 'wbmp',
280  'image/x‑xbitmap' => 'xbm',
281  'image/x‑xbm' => 'xbm',
282  ];
283 
284  // was a valid output format supplied
285  $format = elgg_extract('format', $params);
286  if (in_array($format, $accepted_formats)) {
287  return $format;
288  }
289 
290  try {
291  return elgg_extract($this->mimetype->getMimeType($filename), $accepted_formats);
292  } catch (InvalidArgumentException $e) {
293  $this->getLogger()->warning($e);
294  }
295  }
296 
302  public function hasWebPSupport(): bool {
303  if ($this->config->webp_enabled === false) {
304  return false;
305  }
306 
307  if ($this->imagine instanceof \Imagine\Imagick\Imagine) {
308  return !empty(\Imagick::queryformats('WEBP*'));
309  } elseif ($this->imagine instanceof \Imagine\Gd\Imagine) {
310  return (bool) elgg_extract('WebP Support', gd_info(), false);
311  }
312 
313  return false;
314  }
315 }
$format
Definition: date.php:37
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
__construct(protected Config $config, protected MimeTypeService $mimetype)
Constructor.
if($item instanceof\ElggEntity) elseif($item instanceof\ElggRiverItem) elseif($item instanceof\ElggRelationship) elseif(is_callable([$item, 'getType']))
Definition: item.php:48
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.
if($who_can_change_language=== 'nobody') elseif($who_can_change_language=== 'admin_only'&&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
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:26
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.