Elgg  Version 6.3
ImageService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
8 use Elgg\Traits\Loggable;
9 use Imagine\Filter\Basic\Autorotate;
10 use Imagine\Image\Box;
11 use Imagine\Image\ImagineInterface;
12 use Imagine\Image\Point;
13 
20 class ImageService {
21 
22  use Loggable;
23 
24  const JPEG_QUALITY = 75;
25  const WEBP_QUALITY = 75;
26 
30  protected $imagine;
31 
38  public function __construct(protected Config $config, protected MimeTypeService $mimetype) {
39 
40  switch ($config->image_processor) {
41  case 'imagick':
42  if (extension_loaded('imagick')) {
43  $this->imagine = new \Imagine\Imagick\Imagine();
44  break;
45  }
46 
47  // fallback to GD if Imagick is not loaded
48  default:
49  // default use GD
50  $this->imagine = new \Imagine\Gd\Imagine();
51  break;
52  }
53  }
54 
83  public function resize(string $source, ?string $destination = null, array $params = []): bool {
84 
85  $destination = $destination ?? $source;
86 
87  try {
88  $resize_params = $this->normalizeResizeParameters($source, $params);
89 
90  $image = $this->imagine->open($source);
91 
92  $max_width = (int) elgg_extract('w', $resize_params);
93  $max_height = (int) elgg_extract('h', $resize_params);
94 
95  $x1 = (int) elgg_extract('x1', $resize_params, 0);
96  $y1 = (int) elgg_extract('y1', $resize_params, 0);
97  $x2 = (int) elgg_extract('x2', $resize_params, 0);
98  $y2 = (int) elgg_extract('y2', $resize_params, 0);
99 
100  if ($x2 > $x1 && $y2 > $y1) {
101  $crop_start = new Point($x1, $y1);
102  $crop_size = new Box($x2 - $x1, $y2 - $y1);
103  $image->crop($crop_start, $crop_size);
104  }
105 
106  $target_size = new Box($max_width, $max_height);
107  $image->resize($target_size);
108 
109  // create new canvas with a background (default: white)
110  $background_color = elgg_extract('background_color', $params, 'ffffff');
111  $thumbnail = $this->imagine->create($image->getSize(), $image->palette()->color($background_color));
112  $thumbnail->paste($image, new Point(0, 0));
113 
114  if (pathinfo($destination, PATHINFO_EXTENSION) === 'webp') {
115  $options = [
116  'webp_quality' => elgg_extract('webp_quality', $params, self::WEBP_QUALITY),
117  ];
118  } else {
119  $options = [
120  'format' => $this->getFileFormat($source, $params),
121  'jpeg_quality' => elgg_extract('jpeg_quality', $params, self::JPEG_QUALITY),
122  ];
123  }
124 
125  $thumbnail->save($destination, $options);
126 
127  unset($image);
128  unset($thumbnail);
129  } catch (\Exception $ex) {
130  $this->getLogger()->error($ex);
131 
132  return false;
133  }
134 
135  return true;
136  }
137 
145  public function fixOrientation($filename): bool {
146  try {
147  $this->assertValidImageDimensions($filename);
148 
149  $image = $this->imagine->open($filename);
150  $metadata = $image->metadata();
151  if (!isset($metadata['ifd0.Orientation'])) {
152  // no need to perform an orientation fix
153  return true;
154  }
155 
156  $autorotate = new Autorotate();
157  $autorotate->apply($image)->save($filename);
158 
159  $image->strip()->save($filename);
160 
161  return true;
162  } catch (\Exception $ex) {
163  $this->getLogger()->notice($ex);
164  }
165 
166  return false;
167  }
168 
184  public function normalizeResizeParameters(string $source, array $params = []): array {
185  $this->assertValidImageDimensions($source);
186 
187  $image = $this->imagine->open($source);
188 
189  $width = $image->getSize()->getWidth();
190  $height = $image->getSize()->getHeight();
191 
192  $max_width = (int) elgg_extract('w', $params, 100, false);
193  $max_height = (int) elgg_extract('h', $params, 100, false);
194  if (!$max_height || !$max_width) {
195  throw new InvalidArgumentException('Resize width and height parameters are required');
196  }
197 
198  $square = elgg_extract('square', $params, false);
199  $upscale = elgg_extract('upscale', $params, false);
200 
201  $x1 = (int) elgg_extract('x1', $params, 0);
202  $y1 = (int) elgg_extract('y1', $params, 0);
203  $x2 = (int) elgg_extract('x2', $params, 0);
204  $y2 = (int) elgg_extract('y2', $params, 0);
205 
206  $cropping_mode = $x1 || $y1 || $x2 || $y2;
207 
208  if ($cropping_mode) {
209  $crop_width = $x2 - $x1;
210  $crop_height = $y2 - $y1;
211  if ($crop_width <= 0 || $crop_height <= 0 || $crop_width > $width || $crop_height > $height) {
212  throw new RangeException("Coordinates [$x1, $y1], [$x2, $y2] are invalid for image cropping");
213  }
214  } else {
215  // everything selected if no crop parameters
216  $crop_width = $width;
217  $crop_height = $height;
218  }
219 
220  // determine cropping offsets
221  if ($square) {
222  // asking for a square image back
223 
224  // size of the new square image
225  $max_width = min($max_width, $max_height);
226  $max_height = $max_width;
227 
228  // find the largest square that fits within the selected region
229  $crop_width = min($crop_width, $crop_height);
230  $crop_height = $crop_width;
231 
232  if (!$cropping_mode) {
233  // place square region in the center
234  $x1 = floor(($width - $crop_width) / 2);
235  $y1 = floor(($height - $crop_height) / 2);
236  }
237  } else {
238  // maintain aspect ratio of original image/crop
239  if ($crop_height / $max_height > $crop_width / $max_width) {
240  $max_width = floor($max_height * $crop_width / $crop_height);
241  } else {
242  $max_height = floor($max_width * $crop_height / $crop_width);
243  }
244  }
245 
246  if (!$upscale && ($crop_height < $max_height || $crop_width < $max_width)) {
247  // we cannot upscale and selected area is too small so we decrease size of returned image
248  $max_height = $crop_height;
249  $max_width = $crop_width;
250  }
251 
252  return [
253  'w' => $max_width,
254  'h' => $max_height,
255  'x1' => $x1,
256  'y1' => $y1,
257  'x2' => $x1 + $crop_width,
258  'y2' => $y1 + $crop_height,
259  'square' => $square,
260  'upscale' => $upscale,
261  ];
262  }
263 
273  protected function getFileFormat($filename, $params) {
274 
275  $accepted_formats = [
276  'image/jpeg' => 'jpeg',
277  'image/pjpeg' => 'jpeg',
278  'image/png' => 'png',
279  'image/x-png' => 'png',
280  'image/gif' => 'gif',
281  'image/vnd.wap.wbmp' => 'wbmp',
282  'image/x‑xbitmap' => 'xbm',
283  'image/x‑xbm' => 'xbm',
284  ];
285 
286  // was a valid output format supplied
287  $format = elgg_extract('format', $params);
288  if (in_array($format, $accepted_formats)) {
289  return $format;
290  }
291 
292  try {
293  return elgg_extract($this->mimetype->getMimeType($filename), $accepted_formats);
294  } catch (InvalidArgumentException $e) {
295  $this->getLogger()->warning($e);
296  }
297  }
298 
304  public function hasWebPSupport(): bool {
305  if ($this->config->webp_enabled === false) {
306  return false;
307  }
308 
309  if ($this->imagine instanceof \Imagine\Imagick\Imagine) {
310  return !empty(\Imagick::queryformats('WEBP*'));
311  } elseif ($this->imagine instanceof \Imagine\Gd\Imagine) {
312  return (bool) elgg_extract('WebP Support', gd_info(), false);
313  }
314 
315  return false;
316  }
317 
329  protected function assertValidImageDimensions(string $path): void {
330  $info = getimagesize($path);
331  if (!is_array($info)) {
332  throw new InvalidArgumentException("Unable to read image data for '{$path}'");
333  }
334 
335  $height = (int) elgg_extract(1, $info);
336  if ($height > $this->config->image_resize_max_height) {
337  throw new RangeException('Image height too large to resize');
338  }
339 
340  $width = (int) elgg_extract(0, $info);
341  if ($width > $this->config->image_resize_max_width) {
342  throw new RangeException('Image width too large to resize');
343  }
344 
345  if (($width * $height) > $this->config->image_resize_max_resolution) {
346  throw new RangeException('Image resolution too large to resize');
347  }
348  }
349 }
$params
Saves global plugin settings.
Definition: save.php:13
$source
Exception thrown if an argument is not of the expected type.
Exception thrown to indicate range errors during program execution.
Public service related to MIME type detection.
Image manipulation service.
assertValidImageDimensions(string $path)
Assert that the source image has valid dimensions.
normalizeResizeParameters(string $source, array $params=[])
Calculate the parameters for resizing an image.
__construct(protected Config $config, protected MimeTypeService $mimetype)
Constructor.
getFileFormat($filename, $params)
Determine the image file format, this is needed for correct resizing.
hasWebPSupport()
Checks if imagine has WebP support.
fixOrientation($filename)
If needed the image will be rotated based on orientation information.
resize(string $source, ?string $destination=null, array $params=[])
Crop and resize an image.
if($who_can_change_language==='nobody') elseif($who_can_change_language==='admin_only' &&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
$image
Definition: image_block.php:26
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:240
$format
Definition: date.php:37
try
Definition: login_as.php:33
$path
Definition: details.php:70
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10
$metadata
Output annotation metadata.
Definition: metadata.php:9