Elgg  Version master
ImageService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
9 use Elgg\Traits\Loggable;
10 use Imagine\Filter\Basic\Autorotate;
11 use Imagine\Image\Box;
12 use Imagine\Image\ImagineInterface;
13 use Imagine\Image\Point;
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 the 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 }
$params
Saves global plugin settings.
Definition: save.php:13
$source
Exception thrown if an argument is not of the expected type.
Exception that represents error in the program logic.
Exception thrown to indicate range errors during program execution.
Public service related to MIME type detection.
Image manipulation service.
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:256
$format
Definition: date.php:37
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